Loading config.yaml +4 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,10 @@ BLOGI: # sockets, authdb/mediadb connections, per-domain DB pools. 0 = leave the OS/login-class # default alone. MAXOPENFILES: 65536 # Seconds a connection may sit idle (no read/write activity) before it's closed. # Reclaims fds abandoned by clients that connect and never finish a request. # 0 = disabled (old always-open behavior). IDLECONNECTIONTIMEOUT: 120 DATABASE: DRIVER: "sqlite" CONNECTION: "/var/lib/blogi/blog.db" Loading debian/blogi.default-config.yaml +3 −0 Original line number Diff line number Diff line Loading @@ -14,6 +14,9 @@ BLOGI: # Open-file ceiling, raised via setrlimit() while still root, before any worker forks. # 0 = leave the OS/login-class default alone. MAXOPENFILES: 65536 # Seconds a connection may sit idle (no read/write activity) before it's closed. # 0 = disabled (old always-open behavior). IDLECONNECTIONTIMEOUT: 120 DATABASE: DRIVER: "sqlite" CONNECTION: "/var/lib/blogi/blog.db" Loading editor/html/css/editor.css +164 −0 Original line number Diff line number Diff line Loading @@ -1512,6 +1512,170 @@ dialog label { background: var(--accent-hover); } /* Custom HTML field control (properties panel) */ .html-editor-field { display: flex; flex-direction: column; gap: 6px; } .html-editor-preview { padding: 8px; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); border-radius: 4px; font-family: monospace; font-size: 12px; max-height: 72px; overflow: hidden; white-space: pre-wrap; word-break: break-all; } .html-editor-preview.empty { color: var(--text-muted); font-family: inherit; font-style: italic; } .html-editor-open-btn { align-self: flex-start; padding: 6px 10px; background: var(--accent); color: var(--bg-primary); border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 600; } .html-editor-open-btn:hover { background: var(--accent-hover); } /* Custom HTML source editor (large modal: raw source + element tree) */ .html-code-editor-dialog { width: 1100px; max-width: 95vw; height: 85vh; max-height: 85vh; padding: 0; } .html-code-editor-dialog[open] { display: flex; flex-direction: column; } .hce-header { padding: 0.9rem 1.2rem 0.6rem; border-bottom: 1px solid var(--border); } .hce-header h3 { margin: 0; font-size: 1rem; color: var(--text-primary); } .hce-body { flex: 1; display: flex; min-height: 0; } .hce-source-pane { flex: 1; min-width: 0; display: flex; } #hce-source { flex: 1; width: 100%; padding: 12px; background: var(--bg-primary); color: var(--text-primary); border: none; resize: none; font-family: monospace; font-size: 13px; line-height: 1.5; outline: none; box-sizing: border-box; } .hce-tree-pane { width: 280px; min-width: 280px; display: flex; flex-direction: column; border-left: 1px solid var(--border); background: var(--bg-secondary); } .hce-tree-actions { display: flex; gap: 4px; padding: 8px; border-bottom: 1px solid var(--border); } .hce-tree-actions button { flex: 1; padding: 4px; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); border-radius: 3px; cursor: pointer; font-size: 12px; } .hce-tree-actions button:hover { background: var(--bg-hover); border-color: var(--accent); } #hce-tree { flex: 1; overflow: auto; padding: 4px; } #hce-tree .hint { padding: 8px; color: var(--text-muted); font-size: 12px; } .html-code-editor-dialog .dialog-actions { padding: 0.8rem 1.2rem; border-top: 1px solid var(--border); } @media (max-width: 900px) { .html-code-editor-dialog { width: 100vw; max-width: 100vw; height: 100vh; max-height: 100vh; border-radius: 0; } .hce-body { flex-direction: column; } .hce-tree-pane { width: auto; min-width: 0; max-height: 40%; border-left: none; border-top: 1px solid var(--border); } } /* Color Var / Custom Picker */ .color-mode-toggle { display: flex; Loading editor/html/index.html +27 −0 Original line number Diff line number Diff line Loading @@ -386,6 +386,32 @@ </div> </dialog> <!-- Custom HTML source editor: large modal with a raw-source pane and a read-only element-tree sidebar (parsed live from the source) for navigating the markup. The tree only selects/scrolls the textarea; the textarea remains the single source of truth that gets saved. --> <dialog id="html-code-editor-dialog" class="html-code-editor-dialog"> <div class="hce-header"> <h3 data-i18n="I18N_HTML_EDITOR_TITLE">HTML-Editor</h3> </div> <div class="hce-body"> <div class="hce-source-pane"> <textarea id="hce-source" spellcheck="false"></textarea> </div> <div class="hce-tree-pane"> <div class="hce-tree-actions"> <button type="button" id="btn-hce-expand-all" data-i18n-title="I18N_TITLE_EXPAND_ALL" title="Alle aufklappen">▼</button> <button type="button" id="btn-hce-collapse-all" data-i18n-title="I18N_TITLE_COLLAPSE_ALL" title="Alle zuklappen">▶</button> </div> <div id="hce-tree"></div> </div> </div> <div class="dialog-actions"> <button type="button" id="btn-hce-apply" class="set-save-btn" data-i18n="I18N_HTML_EDITOR_APPLY">Übernehmen</button> <button type="button" id="btn-hce-cancel" data-i18n="I18N_CANCEL">Abbrechen</button> </div> </dialog> <!-- Publish Dialog --> <dialog id="publish-dialog"> <h3 data-i18n="I18N_PUBLISH">Veröffentlichen</h3> Loading Loading @@ -713,6 +739,7 @@ <script src="js/icon-library.js"></script> <script src="js/survey-manager.js"></script> <script src="js/template-manager.js"></script> <script src="js/html-code-editor.js"></script> <script src="js/editor.js"></script> </body> </html> editor/html/js/html-code-editor.js 0 → 100644 +254 −0 Original line number Diff line number Diff line /******************************************************************************* * blogi-webedit HTML Code Editor * * Large modal for editing a raw HTML snippet (e.g. the CustomHtml widget's * html_content field): a source textarea plus a read-only element tree * sidebar parsed live from that source. The tree is a navigation aid only — * clicking a node selects/scrolls the matching span in the textarea; the * textarea stays the single source of truth that gets saved. *******************************************************************************/ var HtmlCodeEditor = (function() { 'use strict'; var VOID_ELEMENTS = { area: 1, base: 1, br: 1, col: 1, embed: 1, hr: 1, img: 1, input: 1, link: 1, meta: 1, param: 1, source: 1, track: 1, wbr: 1 }; var RAW_TEXT_ELEMENTS = { script: 1, style: 1 }; var TAG_RE = /^<(\/?)([a-zA-Z][a-zA-Z0-9:-]*)((?:\s+[^\s"'>\/]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]+))?)*)\s*(\/?)>/; var _dialog = null; var _sourceArea = null; var _bound = false; var _onApply = null; var _collapsed = {}; var _debounceTimer = null; // --- Best-effort HTML tag scanner ------------------------------------ // Not a spec-compliant parser: it exists only to drive the tree view and // click-to-select, so it tolerates unclosed/mismatched tags instead of // rejecting them. Comments and <script>/<style> raw text are skipped // wholesale so their contents can never be mistaken for markup. function _parseHtml(src) { var root = { tag: null, attrs: '', children: [], start: 0, end: src.length }; var stack = [root]; var i = 0; var n = src.length; while (i < n) { var lt = src.indexOf('<', i); if (lt === -1) break; if (src.substr(lt, 4) === '<!--') { var endC = src.indexOf('-->', lt + 4); i = endC === -1 ? n : endC + 3; continue; } var m = TAG_RE.exec(src.slice(lt)); if (!m) { i = lt + 1; continue; } var isClose = m[1] === '/'; var tagName = m[2].toLowerCase(); var selfClose = m[4] === '/' || !!VOID_ELEMENTS[tagName]; var tagEnd = lt + m[0].length; if (isClose) { for (var s = stack.length - 1; s > 0; s--) { if (stack[s].tag === tagName) { while (stack.length - 1 >= s) { stack.pop().end = tagEnd; } break; } } i = tagEnd; continue; } var node = { tag: tagName, attrs: (m[3] || '').trim(), children: [], start: lt, end: tagEnd }; stack[stack.length - 1].children.push(node); i = tagEnd; if (selfClose) continue; if (RAW_TEXT_ELEMENTS[tagName]) { var closeRe = new RegExp('</' + tagName + '\\s*>', 'i'); var cm = closeRe.exec(src.slice(i)); node.end = cm ? (i + cm.index + cm[0].length) : n; i = node.end; continue; } stack.push(node); } return root.children; } function _nodeLabel(node) { var label = node.tag; var idMatch = /\bid\s*=\s*"([^"]*)"|\bid\s*=\s*'([^']*)'/.exec(node.attrs); var classMatch = /\bclass\s*=\s*"([^"]*)"|\bclass\s*=\s*'([^']*)'/.exec(node.attrs); if (idMatch) label += '#' + (idMatch[1] || idMatch[2]); if (classMatch) { var classes = (classMatch[1] || classMatch[2]).trim().split(/\s+/).filter(Boolean); if (classes.length) label += '.' + classes.join('.'); } return label; } function _scrollToOffset(offset) { var textBefore = _sourceArea.value.substring(0, offset); var lineNum = textBefore.split('\n').length - 1; var lineHeight = parseFloat(getComputedStyle(_sourceArea).lineHeight); if (!lineHeight || isNaN(lineHeight)) lineHeight = 18; _sourceArea.scrollTop = Math.max(0, lineNum * lineHeight - _sourceArea.clientHeight / 2); } function _selectRange(start, end) { _sourceArea.focus(); _sourceArea.setSelectionRange(start, end); _scrollToOffset(start); } function _renderTreeNode(node, path) { var hasChildren = node.children && node.children.length > 0; var isCollapsed = hasChildren && !!_collapsed[path]; var div = document.createElement('div'); div.className = 'tree-node' + (isCollapsed ? ' collapsed' : ''); var header = document.createElement('div'); header.className = 'tree-node-header'; var toggle = document.createElement('span'); toggle.className = 'tree-node-toggle'; if (hasChildren) { toggle.textContent = isCollapsed ? '▶' : '▼'; toggle.classList.add('has-children'); toggle.addEventListener('click', function(e) { e.stopPropagation(); if (_collapsed[path]) delete _collapsed[path]; else _collapsed[path] = true; _renderTree(); }); } else { toggle.textContent = '·'; } header.appendChild(toggle); var name = document.createElement('span'); name.className = 'tree-node-name'; name.textContent = _nodeLabel(node); header.appendChild(name); header.addEventListener('click', function(e) { e.stopPropagation(); _selectRange(node.start, node.end); }); div.appendChild(header); if (hasChildren) { var childWrap = document.createElement('div'); childWrap.className = 'tree-children'; for (var i = 0; i < node.children.length; i++) { childWrap.appendChild(_renderTreeNode(node.children[i], path + '.' + i)); } div.appendChild(childWrap); } return div; } function _renderTree() { var container = document.getElementById('hce-tree'); container.innerHTML = ''; var tree = _parseHtml(_sourceArea.value); if (!tree.length) { var hint = document.createElement('p'); hint.className = 'hint'; hint.textContent = (typeof I18n !== 'undefined') ? I18n.t('I18N_HTML_EDITOR_TREE_EMPTY', 'Keine Elemente gefunden.') : 'Keine Elemente gefunden.'; container.appendChild(hint); return; } for (var i = 0; i < tree.length; i++) { container.appendChild(_renderTreeNode(tree[i], String(i))); } } function _collectPaths(nodes, prefix, out) { for (var i = 0; i < nodes.length; i++) { var p = prefix === '' ? String(i) : prefix + '.' + i; if (nodes[i].children && nodes[i].children.length) { out.push(p); _collectPaths(nodes[i].children, p, out); } } } function _expandAll() { _collapsed = {}; _renderTree(); } function _collapseAll() { var paths = []; _collectPaths(_parseHtml(_sourceArea.value), '', paths); _collapsed = {}; paths.forEach(function(p) { _collapsed[p] = true; }); _renderTree(); } function _ensureElements() { _dialog = document.getElementById('html-code-editor-dialog'); _sourceArea = document.getElementById('hce-source'); } function _bind() { if (_bound) return; _bound = true; document.getElementById('btn-hce-expand-all').addEventListener('click', _expandAll); document.getElementById('btn-hce-collapse-all').addEventListener('click', _collapseAll); _sourceArea.addEventListener('input', function() { if (_debounceTimer) clearTimeout(_debounceTimer); _debounceTimer = setTimeout(_renderTree, 250); }); document.getElementById('btn-hce-apply').addEventListener('click', function() { if (_onApply) _onApply(_sourceArea.value); _dialog.close(); }); document.getElementById('btn-hce-cancel').addEventListener('click', function() { _dialog.close(); }); } function open(value, onApply) { _ensureElements(); _bind(); _onApply = onApply; _collapsed = {}; _sourceArea.value = value || ''; _renderTree(); _dialog.showModal(); _sourceArea.focus(); } return { open: open }; })(); Loading
config.yaml +4 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,10 @@ BLOGI: # sockets, authdb/mediadb connections, per-domain DB pools. 0 = leave the OS/login-class # default alone. MAXOPENFILES: 65536 # Seconds a connection may sit idle (no read/write activity) before it's closed. # Reclaims fds abandoned by clients that connect and never finish a request. # 0 = disabled (old always-open behavior). IDLECONNECTIONTIMEOUT: 120 DATABASE: DRIVER: "sqlite" CONNECTION: "/var/lib/blogi/blog.db" Loading
debian/blogi.default-config.yaml +3 −0 Original line number Diff line number Diff line Loading @@ -14,6 +14,9 @@ BLOGI: # Open-file ceiling, raised via setrlimit() while still root, before any worker forks. # 0 = leave the OS/login-class default alone. MAXOPENFILES: 65536 # Seconds a connection may sit idle (no read/write activity) before it's closed. # 0 = disabled (old always-open behavior). IDLECONNECTIONTIMEOUT: 120 DATABASE: DRIVER: "sqlite" CONNECTION: "/var/lib/blogi/blog.db" Loading
editor/html/css/editor.css +164 −0 Original line number Diff line number Diff line Loading @@ -1512,6 +1512,170 @@ dialog label { background: var(--accent-hover); } /* Custom HTML field control (properties panel) */ .html-editor-field { display: flex; flex-direction: column; gap: 6px; } .html-editor-preview { padding: 8px; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); border-radius: 4px; font-family: monospace; font-size: 12px; max-height: 72px; overflow: hidden; white-space: pre-wrap; word-break: break-all; } .html-editor-preview.empty { color: var(--text-muted); font-family: inherit; font-style: italic; } .html-editor-open-btn { align-self: flex-start; padding: 6px 10px; background: var(--accent); color: var(--bg-primary); border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 600; } .html-editor-open-btn:hover { background: var(--accent-hover); } /* Custom HTML source editor (large modal: raw source + element tree) */ .html-code-editor-dialog { width: 1100px; max-width: 95vw; height: 85vh; max-height: 85vh; padding: 0; } .html-code-editor-dialog[open] { display: flex; flex-direction: column; } .hce-header { padding: 0.9rem 1.2rem 0.6rem; border-bottom: 1px solid var(--border); } .hce-header h3 { margin: 0; font-size: 1rem; color: var(--text-primary); } .hce-body { flex: 1; display: flex; min-height: 0; } .hce-source-pane { flex: 1; min-width: 0; display: flex; } #hce-source { flex: 1; width: 100%; padding: 12px; background: var(--bg-primary); color: var(--text-primary); border: none; resize: none; font-family: monospace; font-size: 13px; line-height: 1.5; outline: none; box-sizing: border-box; } .hce-tree-pane { width: 280px; min-width: 280px; display: flex; flex-direction: column; border-left: 1px solid var(--border); background: var(--bg-secondary); } .hce-tree-actions { display: flex; gap: 4px; padding: 8px; border-bottom: 1px solid var(--border); } .hce-tree-actions button { flex: 1; padding: 4px; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); border-radius: 3px; cursor: pointer; font-size: 12px; } .hce-tree-actions button:hover { background: var(--bg-hover); border-color: var(--accent); } #hce-tree { flex: 1; overflow: auto; padding: 4px; } #hce-tree .hint { padding: 8px; color: var(--text-muted); font-size: 12px; } .html-code-editor-dialog .dialog-actions { padding: 0.8rem 1.2rem; border-top: 1px solid var(--border); } @media (max-width: 900px) { .html-code-editor-dialog { width: 100vw; max-width: 100vw; height: 100vh; max-height: 100vh; border-radius: 0; } .hce-body { flex-direction: column; } .hce-tree-pane { width: auto; min-width: 0; max-height: 40%; border-left: none; border-top: 1px solid var(--border); } } /* Color Var / Custom Picker */ .color-mode-toggle { display: flex; Loading
editor/html/index.html +27 −0 Original line number Diff line number Diff line Loading @@ -386,6 +386,32 @@ </div> </dialog> <!-- Custom HTML source editor: large modal with a raw-source pane and a read-only element-tree sidebar (parsed live from the source) for navigating the markup. The tree only selects/scrolls the textarea; the textarea remains the single source of truth that gets saved. --> <dialog id="html-code-editor-dialog" class="html-code-editor-dialog"> <div class="hce-header"> <h3 data-i18n="I18N_HTML_EDITOR_TITLE">HTML-Editor</h3> </div> <div class="hce-body"> <div class="hce-source-pane"> <textarea id="hce-source" spellcheck="false"></textarea> </div> <div class="hce-tree-pane"> <div class="hce-tree-actions"> <button type="button" id="btn-hce-expand-all" data-i18n-title="I18N_TITLE_EXPAND_ALL" title="Alle aufklappen">▼</button> <button type="button" id="btn-hce-collapse-all" data-i18n-title="I18N_TITLE_COLLAPSE_ALL" title="Alle zuklappen">▶</button> </div> <div id="hce-tree"></div> </div> </div> <div class="dialog-actions"> <button type="button" id="btn-hce-apply" class="set-save-btn" data-i18n="I18N_HTML_EDITOR_APPLY">Übernehmen</button> <button type="button" id="btn-hce-cancel" data-i18n="I18N_CANCEL">Abbrechen</button> </div> </dialog> <!-- Publish Dialog --> <dialog id="publish-dialog"> <h3 data-i18n="I18N_PUBLISH">Veröffentlichen</h3> Loading Loading @@ -713,6 +739,7 @@ <script src="js/icon-library.js"></script> <script src="js/survey-manager.js"></script> <script src="js/template-manager.js"></script> <script src="js/html-code-editor.js"></script> <script src="js/editor.js"></script> </body> </html>
editor/html/js/html-code-editor.js 0 → 100644 +254 −0 Original line number Diff line number Diff line /******************************************************************************* * blogi-webedit HTML Code Editor * * Large modal for editing a raw HTML snippet (e.g. the CustomHtml widget's * html_content field): a source textarea plus a read-only element tree * sidebar parsed live from that source. The tree is a navigation aid only — * clicking a node selects/scrolls the matching span in the textarea; the * textarea stays the single source of truth that gets saved. *******************************************************************************/ var HtmlCodeEditor = (function() { 'use strict'; var VOID_ELEMENTS = { area: 1, base: 1, br: 1, col: 1, embed: 1, hr: 1, img: 1, input: 1, link: 1, meta: 1, param: 1, source: 1, track: 1, wbr: 1 }; var RAW_TEXT_ELEMENTS = { script: 1, style: 1 }; var TAG_RE = /^<(\/?)([a-zA-Z][a-zA-Z0-9:-]*)((?:\s+[^\s"'>\/]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]+))?)*)\s*(\/?)>/; var _dialog = null; var _sourceArea = null; var _bound = false; var _onApply = null; var _collapsed = {}; var _debounceTimer = null; // --- Best-effort HTML tag scanner ------------------------------------ // Not a spec-compliant parser: it exists only to drive the tree view and // click-to-select, so it tolerates unclosed/mismatched tags instead of // rejecting them. Comments and <script>/<style> raw text are skipped // wholesale so their contents can never be mistaken for markup. function _parseHtml(src) { var root = { tag: null, attrs: '', children: [], start: 0, end: src.length }; var stack = [root]; var i = 0; var n = src.length; while (i < n) { var lt = src.indexOf('<', i); if (lt === -1) break; if (src.substr(lt, 4) === '<!--') { var endC = src.indexOf('-->', lt + 4); i = endC === -1 ? n : endC + 3; continue; } var m = TAG_RE.exec(src.slice(lt)); if (!m) { i = lt + 1; continue; } var isClose = m[1] === '/'; var tagName = m[2].toLowerCase(); var selfClose = m[4] === '/' || !!VOID_ELEMENTS[tagName]; var tagEnd = lt + m[0].length; if (isClose) { for (var s = stack.length - 1; s > 0; s--) { if (stack[s].tag === tagName) { while (stack.length - 1 >= s) { stack.pop().end = tagEnd; } break; } } i = tagEnd; continue; } var node = { tag: tagName, attrs: (m[3] || '').trim(), children: [], start: lt, end: tagEnd }; stack[stack.length - 1].children.push(node); i = tagEnd; if (selfClose) continue; if (RAW_TEXT_ELEMENTS[tagName]) { var closeRe = new RegExp('</' + tagName + '\\s*>', 'i'); var cm = closeRe.exec(src.slice(i)); node.end = cm ? (i + cm.index + cm[0].length) : n; i = node.end; continue; } stack.push(node); } return root.children; } function _nodeLabel(node) { var label = node.tag; var idMatch = /\bid\s*=\s*"([^"]*)"|\bid\s*=\s*'([^']*)'/.exec(node.attrs); var classMatch = /\bclass\s*=\s*"([^"]*)"|\bclass\s*=\s*'([^']*)'/.exec(node.attrs); if (idMatch) label += '#' + (idMatch[1] || idMatch[2]); if (classMatch) { var classes = (classMatch[1] || classMatch[2]).trim().split(/\s+/).filter(Boolean); if (classes.length) label += '.' + classes.join('.'); } return label; } function _scrollToOffset(offset) { var textBefore = _sourceArea.value.substring(0, offset); var lineNum = textBefore.split('\n').length - 1; var lineHeight = parseFloat(getComputedStyle(_sourceArea).lineHeight); if (!lineHeight || isNaN(lineHeight)) lineHeight = 18; _sourceArea.scrollTop = Math.max(0, lineNum * lineHeight - _sourceArea.clientHeight / 2); } function _selectRange(start, end) { _sourceArea.focus(); _sourceArea.setSelectionRange(start, end); _scrollToOffset(start); } function _renderTreeNode(node, path) { var hasChildren = node.children && node.children.length > 0; var isCollapsed = hasChildren && !!_collapsed[path]; var div = document.createElement('div'); div.className = 'tree-node' + (isCollapsed ? ' collapsed' : ''); var header = document.createElement('div'); header.className = 'tree-node-header'; var toggle = document.createElement('span'); toggle.className = 'tree-node-toggle'; if (hasChildren) { toggle.textContent = isCollapsed ? '▶' : '▼'; toggle.classList.add('has-children'); toggle.addEventListener('click', function(e) { e.stopPropagation(); if (_collapsed[path]) delete _collapsed[path]; else _collapsed[path] = true; _renderTree(); }); } else { toggle.textContent = '·'; } header.appendChild(toggle); var name = document.createElement('span'); name.className = 'tree-node-name'; name.textContent = _nodeLabel(node); header.appendChild(name); header.addEventListener('click', function(e) { e.stopPropagation(); _selectRange(node.start, node.end); }); div.appendChild(header); if (hasChildren) { var childWrap = document.createElement('div'); childWrap.className = 'tree-children'; for (var i = 0; i < node.children.length; i++) { childWrap.appendChild(_renderTreeNode(node.children[i], path + '.' + i)); } div.appendChild(childWrap); } return div; } function _renderTree() { var container = document.getElementById('hce-tree'); container.innerHTML = ''; var tree = _parseHtml(_sourceArea.value); if (!tree.length) { var hint = document.createElement('p'); hint.className = 'hint'; hint.textContent = (typeof I18n !== 'undefined') ? I18n.t('I18N_HTML_EDITOR_TREE_EMPTY', 'Keine Elemente gefunden.') : 'Keine Elemente gefunden.'; container.appendChild(hint); return; } for (var i = 0; i < tree.length; i++) { container.appendChild(_renderTreeNode(tree[i], String(i))); } } function _collectPaths(nodes, prefix, out) { for (var i = 0; i < nodes.length; i++) { var p = prefix === '' ? String(i) : prefix + '.' + i; if (nodes[i].children && nodes[i].children.length) { out.push(p); _collectPaths(nodes[i].children, p, out); } } } function _expandAll() { _collapsed = {}; _renderTree(); } function _collapseAll() { var paths = []; _collectPaths(_parseHtml(_sourceArea.value), '', paths); _collapsed = {}; paths.forEach(function(p) { _collapsed[p] = true; }); _renderTree(); } function _ensureElements() { _dialog = document.getElementById('html-code-editor-dialog'); _sourceArea = document.getElementById('hce-source'); } function _bind() { if (_bound) return; _bound = true; document.getElementById('btn-hce-expand-all').addEventListener('click', _expandAll); document.getElementById('btn-hce-collapse-all').addEventListener('click', _collapseAll); _sourceArea.addEventListener('input', function() { if (_debounceTimer) clearTimeout(_debounceTimer); _debounceTimer = setTimeout(_renderTree, 250); }); document.getElementById('btn-hce-apply').addEventListener('click', function() { if (_onApply) _onApply(_sourceArea.value); _dialog.close(); }); document.getElementById('btn-hce-cancel').addEventListener('click', function() { _dialog.close(); }); } function open(value, onApply) { _ensureElements(); _bind(); _onApply = onApply; _collapsed = {}; _sourceArea.value = value || ''; _renderTree(); _dialog.showModal(); _sourceArea.focus(); } return { open: open }; })();