Commit ccddbfbe authored by jan.koester's avatar jan.koester
Browse files

test

parent 2532a1d5
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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"
+3 −0
Original line number Diff line number Diff line
@@ -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"
+164 −0
Original line number Diff line number Diff line
@@ -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;
+27 −0
Original line number Diff line number Diff line
@@ -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">&#x25BC;</button>
                    <button type="button" id="btn-hce-collapse-all" data-i18n-title="I18N_TITLE_COLLAPSE_ALL" title="Alle zuklappen">&#x25B6;</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">&Uuml;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&ouml;ffentlichen</h3>
@@ -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>
+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