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

tset

parent ccdbfdaf
Loading
Loading
Loading
Loading
+90 −0
Original line number Diff line number Diff line
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>HtmlCodeEditor test harness</title>
<link rel="stylesheet" href="css/editor.css">
</head>
<body style="overflow:auto;height:auto;display:block;padding:20px;">

<button id="open-btn" type="button">Open HTML editor</button>
<pre id="result" style="white-space:pre-wrap;background:#111;color:#0f0;padding:10px;margin-top:10px;"></pre>

<dialog id="html-code-editor-dialog" class="html-code-editor-dialog">
    <div class="hce-header">
        <h3>HTML-Editor</h3>
    </div>
    <div class="hce-body">
        <div class="hce-main-pane">
            <div class="prop-tabs">
                <button type="button" class="prop-tab active" id="btn-hce-tab-source">Quelltext</button>
                <button type="button" class="prop-tab" id="btn-hce-tab-preview">Vorschau</button>
            </div>
            <div class="hce-tab-panels">
                <div class="hce-tab-panel" id="hce-panel-source">
                    <textarea id="hce-source" spellcheck="false"></textarea>
                </div>
                <div class="hce-tab-panel" id="hce-panel-preview" style="display:none">
                    <div class="hce-font-toolbar">
                        <span class="hce-font-hint" id="hce-font-hint">Text lässt sich direkt bearbeiten. Element anklicken, um zusätzlich die Schrift zu bearbeiten.</span>
                        <div class="hce-font-controls" id="hce-font-controls" style="display:none">
                            <input type="text" id="hce-font-family" list="hce-font-family-list" placeholder="Schriftart">
                            <datalist id="hce-font-family-list">
                                <option value="Arial, sans-serif">
                                <option value="Georgia, serif">
                                <option value="'Courier New', monospace">
                            </datalist>
                            <input type="text" id="hce-font-size" placeholder="Größe">
                            <select id="hce-font-weight">
                                <option value="100">100</option>
                                <option value="200">200</option>
                                <option value="300">300</option>
                                <option value="400">400</option>
                                <option value="500">500</option>
                                <option value="600">600</option>
                                <option value="700">700</option>
                                <option value="800">800</option>
                                <option value="900">900</option>
                            </select>
                            <button type="button" id="hce-font-italic" class="rte-btn" style="font-style:italic" title="Kursiv">I</button>
                            <input type="color" id="hce-font-color" class="hce-color-input" title="Farbe">
                            <button type="button" id="hce-font-clear">Zurücksetzen</button>
                        </div>
                    </div>
                    <div class="hce-preview-frame-wrap">
                        <iframe id="hce-preview-iframe" title="Vorschau"></iframe>
                    </div>
                </div>
            </div>
        </div>
        <div class="hce-tree-pane">
            <div class="hce-tree-actions">
                <button type="button" id="btn-hce-expand-all" title="Alle aufklappen">&#x25BC;</button>
                <button type="button" id="btn-hce-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">Übernehmen</button>
        <button type="button" id="btn-hce-cancel">Abbrechen</button>
    </div>
</dialog>

<script src="js/i18n.js"></script>
<script src="js/html-code-editor.js"></script>
<script>
var SAMPLE = '<div class="hero" id="top">\n' +
    '  <h1>Hello World</h1>\n' +
    '  <p class="lead">Some text.</p>\n' +
    '</div>';
var CSS_CONTEXT = 'padding: 20px;';

document.getElementById('open-btn').addEventListener('click', function() {
    HtmlCodeEditor.open(SAMPLE, CSS_CONTEXT, function(newValue) {
        document.getElementById('result').textContent = newValue;
    });
});
</script>
</body>
</html>
+1 −1
Original line number Diff line number Diff line
@@ -409,7 +409,7 @@
                    </div>
                    <div class="hce-tab-panel" id="hce-panel-preview" style="display:none">
                        <div class="hce-font-toolbar">
                            <span class="hce-font-hint" id="hce-font-hint" data-i18n="I18N_HTML_EDITOR_FONT_HINT">Element in der Vorschau anklicken, um die Schrift zu bearbeiten.</span>
                            <span class="hce-font-hint" id="hce-font-hint" data-i18n="I18N_HTML_EDITOR_FONT_HINT">Text l&auml;sst sich direkt bearbeiten. Element anklicken, um zus&auml;tzlich die Schrift zu bearbeiten.</span>
                            <div class="hce-font-controls" id="hce-font-controls" style="display:none">
                                <input type="text" id="hce-font-family" list="hce-font-family-list" data-i18n-placeholder="I18N_HTML_EDITOR_FONT_FAMILY" placeholder="Schriftart">
                                <datalist id="hce-font-family-list">
+147 −13
Original line number Diff line number Diff line
@@ -3,8 +3,8 @@
 *
 * Large modal for editing a raw HTML snippet (e.g. the CustomHtml widget's
 * html_content field): a source tab (raw textarea) and a preview tab (a live
 * rendered iframe with a font-editing toolbar), plus a read-only element tree
 * sidebar shared by both tabs.
 * rendered, directly editable iframe with a font-editing toolbar), plus a
 * read-only element tree sidebar shared by both tabs.
 *
 * The textarea is always the single source of truth. The preview iframe is
 * rebuilt from it (srcdoc, no sandbox — same trust level as the RTE fields
@@ -13,7 +13,12 @@
 * 2nd child of the 1st root node) that maps it back to the parsed node that
 * produced it, so clicking an element in the preview can both scroll/select
 * it in the source textarea and patch its inline style attribute in place
 * when a font property is changed from the toolbar.
 * when a font property is changed from the toolbar. The preview's own text
 * is contentEditable — typed changes are synced back by serializing the
 * whole wrapper's innerHTML into the textarea (same model the existing
 * rich-text fields already use), rather than trying to patch a byte range,
 * since contentEditable is free to restructure the DOM in ways no offset
 * tracking survives.
 *******************************************************************************/

var HtmlCodeEditor = (function() {
@@ -27,14 +32,52 @@ var HtmlCodeEditor = (function() {
    var TAG_RE = /^<(\/?)([a-zA-Z][a-zA-Z0-9:-]*)((?:\s+[^\s"'>\/]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]+))?)*)\s*(\/?)>/;
    var FONT_WEIGHT_ALIASES = { normal: '400', bold: '700' };

    // Debounced actions that mutate the source: schedule() replaces any
    // still-pending run, flush() runs a pending one immediately (used before
    // Apply / leaving the preview tab, so a still-debounced edit is never
    // silently lost), cancel() drops a pending run without executing it
    // (used on open()/Cancel, so a previous field's edit can never bleed
    // into the next session).
    function _makeDebouncer(delay) {
        var timer = null;
        var pending = null;
        return {
            schedule: function(fn) {
                if (timer) clearTimeout(timer);
                pending = fn;
                timer = setTimeout(function() {
                    timer = null;
                    var f = pending;
                    pending = null;
                    f();
                }, delay);
            },
            flush: function() {
                if (!timer) return;
                clearTimeout(timer);
                timer = null;
                var f = pending;
                pending = null;
                f();
            },
            cancel: function() {
                clearTimeout(timer);
                timer = null;
                pending = null;
            }
        };
    }

    var _dialog = null;
    var _sourceArea = null;
    var _bound = false;
    var _onApply = null;
    var _cssContext = '';
    var _collapsed = {};
    var _debounceTimer = null;
    var _fontDebounceTimer = null;
    var _sourceDebouncer = _makeDebouncer(250);
    var _fontFamilyDebouncer = _makeDebouncer(300);
    var _fontSizeDebouncer = _makeDebouncer(300);
    var _textSyncDebouncer = _makeDebouncer(400);
    var _activeTab = 'source';
    var _currentTree = [];
    var _pathToNode = {};
@@ -290,7 +333,8 @@ var HtmlCodeEditor = (function() {
        var css = 'body{margin:0;padding:16px;background:#1e1e2e;color:#cdd6f4;' +
            'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;}' +
            '.hce-widget-scope{' + (_cssContext || '') + '}' +
            '[data-hce-path]{cursor:pointer;}' +
            '.hce-widget-scope{outline:none;}' +
            '[data-hce-path]:hover{outline:1px dashed rgba(137,180,250,0.5);outline-offset:1px;}' +
            '.hce-selected{outline:2px solid #89b4fa!important;outline-offset:1px;}';

        return '<!doctype html><html><head><meta charset="utf-8"><style>' + css + '</style></head>' +
@@ -309,6 +353,19 @@ var HtmlCodeEditor = (function() {
        var iframe = document.getElementById('hce-preview-iframe');
        var doc = iframe && iframe.contentDocument;
        if (!doc) return;

        // Makes the preview's text directly editable — a click still also
        // resolves the closest [data-hce-path] element below for the font
        // toolbar, the two interactions coexist since neither calls
        // preventDefault().
        var wrapper = doc.querySelector('.hce-widget-scope');
        if (wrapper) {
            wrapper.contentEditable = 'true';
            wrapper.addEventListener('input', function() {
                _textSyncDebouncer.schedule(_syncTextFromPreview);
            });
        }

        doc.addEventListener('click', function(e) {
            var target = e.target.closest && e.target.closest('[data-hce-path]');
            if (!target) {
@@ -319,6 +376,60 @@ var HtmlCodeEditor = (function() {
        });
    }

    // Re-stamps data-hce-path over the live preview DOM to match the just-
    // rebuilt _currentTree/_pathToNode model. Needed after a text edit that
    // added/removed/reordered elements (Enter splitting a paragraph, a
    // backspace merging two, ...) — the model's paths are recomputed by
    // structural position, so without this the surviving elements' now-stale
    // path attributes could point a later font edit at the wrong node. Walks
    // .children (elements only, same as the tag scanner) in the same order
    // _rebuildModel assigned paths in, so it stays purely a bookkeeping pass
    // — no visible effect, no rebuild, cursor/scroll untouched.
    function _reassignPreviewPaths(container) {
        (function walk(el, prefix) {
            var children = el.children;
            for (var i = 0; i < children.length; i++) {
                var path = _childPath(prefix, i);
                children[i].setAttribute('data-hce-path', path);
                walk(children[i], path);
            }
        })(container, '');
    }

    // Serializes the whole preview back into the source textarea. Runs
    // debounced while the user types (see _bindPreviewClicks) rather than
    // trying to patch just the edited node's byte range — contentEditable is
    // free to restructure the DOM (Enter splitting a paragraph, browsers
    // wrapping text in a fresh <div>, ...) in ways the offset-based node
    // model can't track, so the whole subtree is re-read instead. Only reads
    // the live iframe DOM via cloneNode (never mutates it) beyond the path
    // reassignment above, so the user's cursor position/scroll survives.
    function _syncTextFromPreview() {
        var iframe = document.getElementById('hce-preview-iframe');
        var doc = iframe && iframe.contentDocument;
        var wrapper = doc && doc.querySelector('.hce-widget-scope');
        if (!wrapper) return;

        var clone = wrapper.cloneNode(true);
        var tagged = clone.querySelectorAll('[data-hce-path]');
        for (var i = 0; i < tagged.length; i++) tagged[i].removeAttribute('data-hce-path');

        _sourceArea.value = clone.innerHTML;
        _rebuildModel(_sourceArea.value);
        _reassignPreviewPaths(wrapper);

        // The edit may have deleted the element the font toolbar is showing
        // (e.g. the user backspaced the whole paragraph away) — its path no
        // longer resolves in the rebuilt model, so drop the stale selection
        // rather than let a further font edit silently no-op.
        if (_selectedPath && !_pathToNode[_selectedPath]) {
            _selectedPath = null;
            _showFontControls(false);
        }

        _renderTree();
    }

    function _rgbToHex(rgb) {
        var m = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(rgb || '');
        if (!m) return '#000000';
@@ -428,6 +539,10 @@ var HtmlCodeEditor = (function() {

    function _switchTab(tab) {
        if (_activeTab === tab) return;
        // Leaving the preview tab with a still-debounced font/text edit
        // pending would otherwise silently drop it once the source tab (or
        // Apply) reads _sourceArea.value before the timer fires.
        if (_activeTab === 'preview') _flushPreviewEdits();
        _activeTab = tab;
        document.getElementById('btn-hce-tab-source').classList.toggle('active', tab === 'source');
        document.getElementById('btn-hce-tab-preview').classList.toggle('active', tab === 'preview');
@@ -436,6 +551,15 @@ var HtmlCodeEditor = (function() {
        if (tab === 'preview') _renderPreview();
    }

    // Order matters: flushing the font debouncers first lets _applyFontProp
    // update the live DOM node's style, so the text-sync flush right after
    // captures that style in the innerHTML it reads.
    function _flushPreviewEdits() {
        _fontFamilyDebouncer.flush();
        _fontSizeDebouncer.flush();
        _textSyncDebouncer.flush();
    }

    // --- Wiring -------------------------------------------------------------

    function _ensureElements() {
@@ -453,23 +577,20 @@ var HtmlCodeEditor = (function() {
        document.getElementById('btn-hce-tab-preview').addEventListener('click', function() { _switchTab('preview'); });

        _sourceArea.addEventListener('input', function() {
            if (_debounceTimer) clearTimeout(_debounceTimer);
            _debounceTimer = setTimeout(function() {
            _sourceDebouncer.schedule(function() {
                _rebuildModel(_sourceArea.value);
                _renderTree();
                if (_activeTab === 'preview') _renderPreview();
            }, 250);
            });
        });

        document.getElementById('hce-font-family').addEventListener('input', function() {
            var v = this.value;
            if (_fontDebounceTimer) clearTimeout(_fontDebounceTimer);
            _fontDebounceTimer = setTimeout(function() { _applyFontProp('font-family', v); }, 300);
            _fontFamilyDebouncer.schedule(function() { _applyFontProp('font-family', v); });
        });
        document.getElementById('hce-font-size').addEventListener('input', function() {
            var v = this.value;
            if (_fontDebounceTimer) clearTimeout(_fontDebounceTimer);
            _fontDebounceTimer = setTimeout(function() { _applyFontProp('font-size', v); }, 300);
            _fontSizeDebouncer.schedule(function() { _applyFontProp('font-size', v); });
        });
        document.getElementById('hce-font-weight').addEventListener('change', function() {
            _applyFontProp('font-weight', this.value);
@@ -485,10 +606,16 @@ var HtmlCodeEditor = (function() {
        document.getElementById('hce-font-clear').addEventListener('click', _clearFontStyles);

        document.getElementById('btn-hce-apply').addEventListener('click', function() {
            _flushPreviewEdits();
            _sourceDebouncer.cancel();
            if (_onApply) _onApply(_sourceArea.value);
            _dialog.close();
        });
        document.getElementById('btn-hce-cancel').addEventListener('click', function() {
            _sourceDebouncer.cancel();
            _fontFamilyDebouncer.cancel();
            _fontSizeDebouncer.cancel();
            _textSyncDebouncer.cancel();
            _dialog.close();
        });
    }
@@ -496,6 +623,13 @@ var HtmlCodeEditor = (function() {
    function open(value, cssContext, onApply) {
        _ensureElements();
        _bind();
        // Drop (without running) any edit still debounced from a previous
        // open() call — it belongs to whatever field that session was
        // editing and must never bleed into this one.
        _sourceDebouncer.cancel();
        _fontFamilyDebouncer.cancel();
        _fontSizeDebouncer.cancel();
        _textSyncDebouncer.cancel();
        _onApply = onApply;
        _cssContext = cssContext || '';
        _collapsed = {};
+2 −2
Original line number Diff line number Diff line
@@ -277,7 +277,7 @@ namespace blogi {
            {"I18N_HTML_EDITOR_TREE_EMPTY", "Keine Elemente gefunden."},
            {"I18N_HTML_EDITOR_TAB_SOURCE", "Quelltext"},
            {"I18N_HTML_EDITOR_TAB_PREVIEW", "Vorschau"},
            {"I18N_HTML_EDITOR_FONT_HINT", "Element in der Vorschau anklicken, um die Schrift zu bearbeiten."},
            {"I18N_HTML_EDITOR_FONT_HINT", "Text l&auml;sst sich direkt bearbeiten. Element anklicken, um zus&auml;tzlich die Schrift zu bearbeiten."},
            {"I18N_HTML_EDITOR_FONT_FAMILY", "Schriftart"},
            {"I18N_HTML_EDITOR_FONT_SIZE", "Gr&ouml;&szlig;e"},
            {"I18N_HTML_EDITOR_FONT_RESET", "Zur&uuml;cksetzen"},
@@ -818,7 +818,7 @@ namespace blogi {
            {"I18N_HTML_EDITOR_TREE_EMPTY", "No elements found."},
            {"I18N_HTML_EDITOR_TAB_SOURCE", "Source"},
            {"I18N_HTML_EDITOR_TAB_PREVIEW", "Preview"},
            {"I18N_HTML_EDITOR_FONT_HINT", "Click an element in the preview to edit its font."},
            {"I18N_HTML_EDITOR_FONT_HINT", "Text can be edited directly. Click an element to also edit its font."},
            {"I18N_HTML_EDITOR_FONT_FAMILY", "Font family"},
            {"I18N_HTML_EDITOR_FONT_SIZE", "Size"},
            {"I18N_HTML_EDITOR_FONT_RESET", "Reset"},