Commit 0838aae3 authored by Jan Köster's avatar Jan Köster
Browse files

icon lib

parent 765a17f0
Loading
Loading
Loading
Loading
+40 −0
Changes for editor/html/index.html: 40 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -363,6 +363,19 @@
        </div>
    </dialog>

    <!-- Icon Library Dialog (picker, opened from the "icon_browse" property field) -->
    <dialog id="icon-library-dialog">
        <h3 data-i18n="I18N_ICON_LIBRARY">Icon-Bibliothek</h3>
        <div class="set-field">
            <input type="text" id="icon-library-search" data-i18n-placeholder="I18N_SEARCH" placeholder="Suchen...">
        </div>
        <div id="icon-library-grid" class="media-grid"></div>
        <span id="icon-library-status" class="set-status"></span>
        <div class="dialog-actions">
            <button id="btn-icon-library-close" data-i18n="Close">Schlie&szlig;en</button>
        </div>
    </dialog>

    <dialog id="ai-dialog" class="ai-panel">
        <div class="ai-titlebar" id="ai-titlebar">
            <span>&#x1F916; <span data-i18n="I18N_AI">KI-Assistent</span></span>
@@ -565,6 +578,7 @@
                <h3 data-i18n="I18N_TOOLS">Werkzeuge</h3>
                <ul class="set-nav">
                    <li class="set-nav-item active" data-section="templates" data-i18n="I18N_TEMPLATES">Vorlagen</li>
                    <li class="set-nav-item" data-section="icons" data-i18n="I18N_ICONS">Icons</li>
                </ul>
            </div>
            <div class="set-content" id="tools-content">
@@ -573,6 +587,31 @@
                    <p class="hint" data-i18n="I18N_GLOBAL_TEMPLATES_HINT">Per Rechtsklick auf ein Element im Dokumentbaum kannst du es (inkl. Kinder) als globale Vorlage speichern.</p>
                    <div id="tools-template-list"></div>
                </div>
                <div id="tools-icons" class="set-section" style="display:none;">
                    <h4 data-i18n="I18N_ICON_LIBRARY">Icon-Bibliothek</h4>
                    <p class="hint" data-i18n="I18N_ICON_LIBRARY_HINT">Lade ein SVG hoch oder f&uuml;ge SVG-Code ein, um es domainweit in der Icon-Bibliothek verf&uuml;gbar zu machen.</p>
                    <div class="set-field">
                        <label data-i18n="I18N_NAME">Name</label>
                        <input type="text" id="icon-upload-name">
                    </div>
                    <div class="set-field">
                        <label data-i18n="I18N_TAGS">Tags</label>
                        <input type="text" id="icon-upload-tags" data-i18n-placeholder="I18N_TAGS_PLACEHOLDER" placeholder="z. B. pfeil, navigation">
                    </div>
                    <div class="set-field">
                        <label data-i18n="I18N_ICON_FILE">SVG-Datei</label>
                        <input type="file" id="icon-upload-file" accept=".svg,image/svg+xml">
                    </div>
                    <div class="set-field">
                        <label data-i18n="I18N_SVG_MARKUP">SVG-Code</label>
                        <textarea id="icon-upload-svg" rows="4" placeholder="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;...&lt;/svg&gt;"></textarea>
                    </div>
                    <div class="set-field">
                        <button type="button" id="btn-icon-upload" data-i18n="Upload">Hochladen</button>
                    </div>
                    <span id="icon-upload-status" class="set-status"></span>
                    <div id="tools-icon-grid" class="media-grid"></div>
                </div>
            </div>
        </div>
        <div class="dialog-actions">
@@ -587,6 +626,7 @@
    <script src="js/properties.js"></script>
    <script src="js/preview.js"></script>
    <script src="js/media-browser.js"></script>
    <script src="js/icon-library.js"></script>
    <script src="js/survey-manager.js"></script>
    <script src="js/template-manager.js"></script>
    <script src="js/editor.js"></script>
+21 −0
Changes for editor/html/js/api.js: 21 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -588,6 +588,27 @@ var EditorApi = (function() {
            return request('DELETE', '/api/element-template/delete/' + encodeURIComponent(id));
        },

        // Icon library (reusable SVG snippets)
        listIcons: function() {
            return request('GET', '/api/icons');
        },

        getIcon: function(id) {
            return request('GET', '/api/icon/' + encodeURIComponent(id));
        },

        saveIcon: function(name, tags, svg) {
            return request('POST', '/api/icon/save', { name: name, tags: tags, svg: svg });
        },

        updateIcon: function(id, name, tags, svg) {
            return request('POST', '/api/icon/update', { id: id, name: name, tags: tags, svg: svg });
        },

        deleteIcon: function(id) {
            return request('DELETE', '/api/icon/delete/' + encodeURIComponent(id));
        },

        publishSurvey: function(surveyId, connId) {
            return requestWithReconnect(connId, 'POST', '/api/survey/publish',
                { survey_id: surveyId, conn_id: connId });
+1 −0
Changes for editor/html/js/editor.js: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@
            try { bindElementContextMenu(); } catch(e) { console.error('bindElementContextMenu:', e); }
            try { SurveyManager.init(); } catch(e) { console.error('SurveyManager.init:', e); }
            try { TemplateManager.init(refreshDocument); } catch(e) { console.error('TemplateManager.init:', e); }
            try { IconLibrary.init(); } catch(e) { console.error('IconLibrary.init:', e); }
            try { Preview.bindToggle(); } catch(e) { console.error('Preview.bindToggle:', e); }
            try { Canvas.bindMessageListener(); } catch(e) { console.error('Canvas.bindMessageListener:', e); }
            try { initProjectSelector(); } catch(e) { console.error('initProjectSelector:', e); }
+278 −0
Changes for editor/html/js/icon-library.js: 278 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
 * blogi-webedit Icon Library
 * Domain-wide library of reusable SVG icons: upload/manage them under
 * "Werkzeuge" > "Icons", and pick one from the Svg widget's "icon_browse"
 * property field (source_type = "icon").
 *******************************************************************************/

var IconLibrary = (function() {
    'use strict';

    var _onSelect = null;
    var _pickerDialog = null;
    var _pickerGrid = null;
    var _pickerSearch = null;
    var _pickerStatus = null;
    var _pickerIcons = [];
    var _pickerBound = false;

    var _manageGrid = null;
    var _manageStatus = null;
    var _uploadName = null;
    var _uploadTags = null;
    var _uploadFile = null;
    var _uploadSvg = null;

    function _fileToText(file) {
        return new Promise(function(resolve, reject) {
            var reader = new FileReader();
            reader.onload = function() { resolve(reader.result || ''); };
            reader.onerror = reject;
            reader.readAsText(file);
        });
    }

    function _matches(icon, term) {
        if (!term) return true;
        term = term.toLowerCase();
        return (icon.name || '').toLowerCase().indexOf(term) >= 0 ||
               (icon.tags || '').toLowerCase().indexOf(term) >= 0;
    }

    function _renderIconPreview(svg) {
        var preview = document.createElement('div');
        preview.className = 'icon-tile-preview';
        preview.style.cssText = 'width:100%;height:80px;display:flex;align-items:center;justify-content:center;';
        preview.innerHTML = svg;
        var svgEl = preview.querySelector('svg');
        if (svgEl) {
            svgEl.style.maxWidth = '100%';
            svgEl.style.maxHeight = '100%';
        }
        return preview;
    }

    // --- Picker (selection mode) ---

    function _ensurePickerElements() {
        _pickerDialog = document.getElementById('icon-library-dialog');
        _pickerGrid = document.getElementById('icon-library-grid');
        _pickerSearch = document.getElementById('icon-library-search');
        _pickerStatus = document.getElementById('icon-library-status');
    }

    function _bindPicker() {
        if (_pickerBound) return;
        _pickerBound = true;

        document.getElementById('btn-icon-library-close').addEventListener('click', function() {
            _pickerDialog.close();
        });

        _pickerSearch.addEventListener('input', function() {
            _renderPickerGrid(_pickerIcons.filter(function(ic) { return _matches(ic, _pickerSearch.value); }));
        });
    }

    function _renderPickerGrid(icons) {
        _pickerGrid.innerHTML = '';
        if (icons.length === 0) {
            _pickerStatus.textContent = I18n.t('I18N_NO_ICONS', 'Keine Icons vorhanden.');
            return;
        }
        _pickerStatus.textContent = '';
        icons.forEach(function(icon) {
            _pickerGrid.appendChild(_createPickerTile(icon));
        });
    }

    function _createPickerTile(icon) {
        var div = document.createElement('div');
        div.className = 'media-grid-item';
        div.dataset.id = icon.id;
        div.appendChild(_renderIconPreview(icon.svg));

        var name = document.createElement('div');
        name.className = 'media-name';
        name.textContent = icon.name;
        name.title = icon.name;
        div.appendChild(name);

        div.addEventListener('click', function() {
            if (_onSelect) _onSelect(icon);
            _pickerDialog.close();
        });

        return div;
    }

    function open(onSelect) {
        _onSelect = onSelect;
        _ensurePickerElements();
        _bindPicker();

        _pickerGrid.innerHTML = '';
        _pickerSearch.value = '';
        _pickerStatus.textContent = I18n.t('I18N_LOADING', 'Lade...');

        EditorApi.listIcons().then(function(resp) {
            _pickerIcons = resp.icons || [];
            _renderPickerGrid(_pickerIcons);
        }).catch(function() {
            _pickerStatus.textContent = I18n.t('I18N_FAILED_LOAD_ICONS', 'Icons konnten nicht geladen werden');
        });

        _pickerDialog.showModal();
    }

    function getIcon(id) {
        return EditorApi.getIcon(id);
    }

    // --- Management (Werkzeuge > Icons: upload / rename / delete) ---

    function _loadManageGrid() {
        _manageGrid.innerHTML = '<p class="hint">' + I18n.t('I18N_LOADING', 'Lade...') + '</p>';
        EditorApi.listIcons().then(function(resp) {
            _renderManageGrid(resp.icons || []);
        }).catch(function() {
            _manageGrid.innerHTML = '<p class="hint">' +
                I18n.t('I18N_FAILED_LOAD_ICONS', 'Icons konnten nicht geladen werden') + '</p>';
        });
    }

    function _renderManageGrid(icons) {
        _manageGrid.innerHTML = '';
        if (icons.length === 0) {
            _manageGrid.innerHTML = '<p class="hint">' +
                I18n.t('I18N_NO_ICONS', 'Keine Icons vorhanden.') + '</p>';
            return;
        }
        icons.forEach(function(icon) {
            _manageGrid.appendChild(_createManageTile(icon));
        });
    }

    function _createManageTile(icon) {
        var div = document.createElement('div');
        div.className = 'media-grid-item';
        div.appendChild(_renderIconPreview(icon.svg));

        var name = document.createElement('div');
        name.className = 'media-name';
        name.textContent = icon.name;
        name.title = icon.tags ? icon.name + ' (' + icon.tags + ')' : icon.name;
        div.appendChild(name);

        var actions = document.createElement('div');
        actions.className = 'tools-tpl-actions';

        var btnRename = document.createElement('button');
        btnRename.className = 'set-save-btn';
        btnRename.textContent = I18n.t('I18N_RENAME_TEMPLATE', 'Umbenennen');
        btnRename.addEventListener('click', function() { _renameIcon(icon); });
        actions.appendChild(btnRename);

        var btnDelete = document.createElement('button');
        btnDelete.className = 'set-save-btn tools-tpl-delete';
        btnDelete.textContent = I18n.t('I18N_DELETE_TEMPLATE', 'Löschen');
        btnDelete.addEventListener('click', function() { _deleteIcon(icon.id); });
        actions.appendChild(btnDelete);

        div.appendChild(actions);
        return div;
    }

    function _renameIcon(icon) {
        var name = prompt(I18n.t('I18N_ICON_NAME_PROMPT', 'Name des Icons:'), icon.name || '');
        if (!name) return;
        var tags = prompt(I18n.t('I18N_ICON_TAGS_PROMPT', 'Tags (kommagetrennt):'), icon.tags || '');
        EditorApi.updateIcon(icon.id, name, tags || '', icon.svg).then(_loadManageGrid).catch(function(err) {
            alert(I18n.t('I18N_ACTION_FAILED', 'Aktion fehlgeschlagen') + ': ' + ((err && err.error) || ''));
        });
    }

    function _deleteIcon(id) {
        if (!confirm(I18n.t('I18N_DELETE_ICON_CONFIRM', 'Icon löschen?'))) return;
        EditorApi.deleteIcon(id).then(_loadManageGrid).catch(function(err) {
            alert(I18n.t('I18N_ACTION_FAILED', 'Aktion fehlgeschlagen') + ': ' + ((err && err.error) || ''));
        });
    }

    function _uploadIcon() {
        var name = _uploadName.value.trim();
        var tags = _uploadTags.value.trim();
        var file = _uploadFile.files && _uploadFile.files[0];

        if (!name) {
            _manageStatus.textContent = I18n.t('I18N_ICON_NAME_REQUIRED', 'Bitte einen Namen angeben');
            _manageStatus.className = 'set-status error';
            return;
        }

        var svgPromise = file ? _fileToText(file) : Promise.resolve(_uploadSvg.value);

        svgPromise.then(function(svg) {
            svg = (svg || '').trim();
            if (!svg) {
                _manageStatus.textContent = I18n.t('I18N_ICON_SVG_REQUIRED',
                    'Bitte eine SVG-Datei wählen oder SVG-Code einfügen');
                _manageStatus.className = 'set-status error';
                return;
            }
            _manageStatus.textContent = I18n.t('I18N_UPLOADING', 'Lädt hoch...');
            _manageStatus.className = 'set-status';
            return EditorApi.saveIcon(name, tags, svg).then(function() {
                _uploadName.value = '';
                _uploadTags.value = '';
                _uploadFile.value = '';
                _uploadSvg.value = '';
                _manageStatus.textContent = '';
                _loadManageGrid();
            });
        }).catch(function(err) {
            _manageStatus.textContent = I18n.t('I18N_UPLOAD_FAILED', 'Upload fehlgeschlagen') +
                ': ' + ((err && err.error) || '');
            _manageStatus.className = 'set-status error';
        });
    }

    function _bindToolsNav() {
        // editor.js's generic "Nav section switching" handler resolves section
        // ids as "settings-" + data-section, which only matches settings-dialog
        // tabs. Bind a dedicated, correctly-scoped handler for the tools-dialog's
        // own tabs (Vorlagen / Icons) instead of relying on that global one.
        var navItems = document.querySelectorAll('#tools-dialog .set-nav-item');
        for (var i = 0; i < navItems.length; i++) {
            navItems[i].addEventListener('click', function() {
                for (var j = 0; j < navItems.length; j++) navItems[j].classList.remove('active');
                this.classList.add('active');
                var sections = document.querySelectorAll('#tools-dialog .set-section');
                for (var k = 0; k < sections.length; k++) sections[k].style.display = 'none';
                var target = document.getElementById('tools-' + this.getAttribute('data-section'));
                if (target) target.style.display = 'block';
                if (this.getAttribute('data-section') === 'icons') _loadManageGrid();
            });
        }
    }

    function init() {
        _manageGrid = document.getElementById('tools-icon-grid');
        _manageStatus = document.getElementById('icon-upload-status');
        _uploadName = document.getElementById('icon-upload-name');
        _uploadTags = document.getElementById('icon-upload-tags');
        _uploadFile = document.getElementById('icon-upload-file');
        _uploadSvg = document.getElementById('icon-upload-svg');

        var uploadBtn = document.getElementById('btn-icon-upload');
        if (uploadBtn) uploadBtn.addEventListener('click', _uploadIcon);

        _bindToolsNav();
    }

    return {
        init: init,
        open: open,
        getIcon: getIcon
    };
})();
+44 −0
Changes for editor/html/js/properties.js: 44 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -830,6 +830,50 @@ var PropertiesPanel = (function() {
                    group.appendChild(wrapper);
                    panel.appendChild(group);
                    continue;
                } else if (field.type === 'icon_browse') {
                    var iconWrapper = document.createElement('div');
                    iconWrapper.className = 'media-browse-field icon-browse-field';
                    var iconPreview = document.createElement('span');
                    iconPreview.className = 'icon-browse-preview';
                    iconPreview.style.cssText = 'display:inline-flex;width:28px;height:28px;vertical-align:middle;margin-right:6px;';
                    input = document.createElement('input');
                    input.type = 'hidden';
                    input.name = field.key;
                    input.value = value;
                    var iconBrowseBtn = document.createElement('button');
                    iconBrowseBtn.type = 'button';
                    iconBrowseBtn.textContent = I18n.t('I18N_CHOOSE_ICON', 'Icon wählen');
                    (function(inp, preview) {
                        function refreshPreview() {
                            if (inp.value && typeof IconLibrary !== 'undefined') {
                                IconLibrary.getIcon(inp.value).then(function(ic) {
                                    preview.innerHTML = ic && ic.svg ? ic.svg : '';
                                }).catch(function() { preview.innerHTML = ''; });
                            } else {
                                preview.innerHTML = '';
                            }
                        }
                        refreshPreview();
                        iconBrowseBtn.addEventListener('click', function() {
                            if (typeof IconLibrary === 'undefined') return;
                            IconLibrary.open(function(icon) {
                                inp.value = icon.id;
                                preview.innerHTML = icon.svg || '';
                                var svgInput = form.querySelector('textarea[name="svg_markup"]');
                                if (svgInput) {
                                    svgInput.value = icon.svg || '';
                                    svgInput.dispatchEvent(new Event('change'));
                                }
                                inp.dispatchEvent(new Event('change'));
                            });
                        });
                    })(input, iconPreview);
                    iconWrapper.appendChild(iconPreview);
                    iconWrapper.appendChild(input);
                    iconWrapper.appendChild(iconBrowseBtn);
                    group.appendChild(iconWrapper);
                    panel.appendChild(group);
                    continue;
                } else if (field.type === 'faq_items') {
                    var faqWrap = document.createElement('div');
                    faqWrap.className = 'faq-items-editor';
Loading