Dataform Block

Anwendungen für Webseiten. Künstliche Intelligenz verwenden.
Volker
Moderator
Moderator
Beiträge: 3143
Registriert: Sa 12. Dez 2020, 22:35

Re: Dataform Block

Ungelesener Beitrag von Volker »

Dann hättest du auch direkt den CSV Import einbauen können ;)
Ich guck mir das heute abend an
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Ich weiß gar nicht, ob man das überhaupt benötigt. Das ist doch eine SQLite Datenbank und da muss dann die CSV wieder eingelesen werden - keine Ahnung wie man das macht - aber ChatGPT kann das sicherlich. Nur - wer braucht das eigentlich?
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Deine KI verwendet in dem Script Hexadecimal-Color-Code (100% to 0% alpha), das versteht doch keine Sau :eek:

https://gist.github.com/lopspower/03fb1cc0ac9f32ef38f4

Kannst Du (unten im HTML) vielleicht die Opacity vom Layer der Lightbox noch ändern von:

Code: Alles auswählen

background:#000000ee;
in verständliches RGBA:

Code: Alles auswählen

background: rgba(0, 0, 0, 0.93);
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Volker,

ich habe jetzt hier nochmals die aktuelle Block-HTML.

Dort habe ich das mit dem Lightbox-Overlay bereits geändert und nun auch noch der Scroll-To-Top Anker. Der sollte ja nicht inter der Menüleiste sein, sondern eben oben am Block. Wenn Du jetzt bei mir auf Seite -2- oder so klickst, dann springt es logisch nach oben und nicht wie zuvor vollkommen unlogisch hinter die Navi:

↗️ https://www.mobirise-tutorials.com/VN-B ... l#DataForm

Code: Alles auswählen

<section data-bs-version="5.1" class="vn-dataform" data-showsearch="true" group="Content" plugins="vn-dataform" data-gid="{{formId}}" data-mobiletrigger="{{mobileTrigger}}" data-viewmode="{{viewMode}}" data-perpage="{{perPage}}" data-imgsize="{{imgSize}}" data-imgpos="{{imgPos}}" data-shadowsize="{{shadowSize}}" data-shadowcolor="{{shadowColor}}">
    <mbr-parameters>
        <header>Formular</header>
        <input type="text" title="Formular-ID (eindeutig)" name="formId" value="form-1">
          <input type="checkbox" title="Suche anzeigen" name="showSearch" checked>
        <select title="Ansicht" name="viewMode">
            <option value="cards" selected>Karten</option>
            <option value="table">Tabelle</option>
        </select>
        <input type="range" inline title="Einträge/Seite" name="perPage" min="4" max="20" step="1" value="6">
       

        <header>Handy Admin-Trigger</header>
        <select title="Handy Admin-Trigger" name="mobileTrigger">
            <option value="shake" selected>Schütteln</option>
            <option value="tap4">4x Tippen</option>
            <option value="button">⚙️ Button</option>
        </select>

        <header>Abstände</header>
        <input type="range" inline title="Oben" name="paddingTop" min="2" max="12" step="1" value="7">
        <input type="range" inline title="Unten" name="paddingBottom" min="2" max="12" step="1" value="2">
        <input type="range" inline title="Ecken" name="cardRadius" min="0" max="20" step="2" value="20">

        <header>Schatten</header>
        <input type="range" inline title="Schatten Stärke" name="shadowSize" min="0" max="40" step="4" value="8">
        <input type="color" title="Schatten Farbe" name="shadowColor" value="#bbbbbb" selected>

        <header>Bilder</header>
        <input type="range" inline title="Bildgröße (px)" name="imgSize" min="60" max="300" step="10" value="240">
        <select title="Bild-Position" name="imgPos">
            <option value="top" selected>Oben in Karte</option>
            <option value="row">Als Zeile</option>
        </select>

        <header>Schriftgrößen</header>
        <input type="range" inline title="Label (px)" name="labelSize" min="10" max="22" step="1" value="18">
        <input type="range" inline title="Wert (px)" name="valueSize" min="10" max="22" step="1" value="21">
        <input type="range" inline title="Header (px)" name="headerSize" min="12" max="30" step="1" value="21">

        <header>Farben</header>
        <input type="color" title="Hintergrund" name="bgColor" value="#0a0a0a" selected>
        <input type="color" title="Karte" name="cardColor" value="#272727" selected>
        <input type="color" title="Kopf Hintergrund" name="headBg" value="#08378e" selected>
        <input type="color" title="Kopf Text" name="headText" value="#ffffff" selected>
        <input type="color" title="Label-Farbe" name="labelColor" value="#bed3f9" selected>
        <input type="color" title="Wert-Farbe" name="valueColor" value="#e0e0e0" selected>
        <input type="color" title="Rahmen" name="borderColor" value="#bbbbbb" selected>
        <input type="color" title="Akzent" name="accentColor" value="#0d41a0" selected>
        <input type="color" title="Suche Hintergrund" name="searchBg" value="#001a47" selected>
    </mbr-parameters>

    <div class="df-wrap">
        <!-- Suchbereich -->
        <div class="df-search-wrap" id="df-search-wrap">
            <input type="text" id="df-search" class="df-search-inp" placeholder="🔍 Suche…" autocomplete="off" spellcheck="false">
        </div>

        <!-- Ergebnisse -->
        <div id="df-results"></div>


        <!-- Admin Panel -->
        <div id="df-admin" style="display:none;">

            <!-- Tabs -->
            <div class="df-tabs">
                <button class="df-tab df-tab-active" data-tab="records" onclick="dfTab('records')">📋 Einträge</button>
                <button class="df-tab" data-tab="fields" onclick="dfTab('fields')">🔧 Felder</button>
                <button class="df-tab" data-tab="new" onclick="dfTab('new')">➕ Neu</button>
            </div>

            <!-- Tab: Einträge -->
            <div id="df-tab-records" class="df-tab-content">
                <div id="df-admin-list"></div>
            </div>

            <!-- Tab: Felder definieren -->
            <div id="df-tab-fields" class="df-tab-content" style="display:none;">
                <div style="margin-bottom:0.75rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
                    <button class="df-btn-accent" onclick="dfAddField()">+ Feld</button>
                    <button class="df-btn-primary" onclick="dfSaveFields()">💾 Felder speichern</button>
                </div>
                <div id="df-field-list"></div>
            </div>

            <!-- Tab: Neuer Eintrag -->
            <div id="df-tab-new" class="df-tab-content" style="display:none;max-height:60vh;overflow-y:auto;">
                <div id="df-new-form"></div>
                <button class="df-btn-primary" style="margin-top:1rem;" onclick="dfSaveRecord()">💾 Speichern</button>
            </div>

            <div style="margin-top:1rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
                <a id="df-export-link" href="#" class="df-btn-sm" onclick="dfExport(event)">⬇ CSV</a>
                <button class="df-btn-ghost" onclick="document.getElementById('df-pw-modal').style.display='flex'">🔑 Passwort</button>
                <button class="df-btn-ghost" onclick="dfLogout()">Ausloggen</button>
            </div>
        </div>

        <!-- Dummy im Editor -->
        <div id="df-dummy">
            <div class="df-card">
                <div class="df-card-head">Max Mustermann</div>
                <div class="df-card-body">
                    <div class="df-row"><span class="df-lbl">E-Mail</span><span class="df-val">max@beispiel.de</span></div>
                    <div class="df-row"><span class="df-lbl">Telefon</span><span class="df-val">+49 123 456789</span></div>
                    <div class="df-row"><span class="df-lbl">Stadt</span><span class="df-val">Stuttgart</span></div>
                </div>
            </div>
            <div class="df-card">
                <div class="df-card-head">Erika Musterfrau</div>
                <div class="df-card-body">
                    <div class="df-row"><span class="df-lbl">E-Mail</span><span class="df-val">erika@beispiel.de</span></div>
                    <div class="df-row"><span class="df-lbl">Telefon</span><span class="df-val">+49 987 654321</span></div>
                    <div class="df-row"><span class="df-lbl">Stadt</span><span class="df-val">München</span></div>
                </div>
            </div>
            <p class="df-hint">📊 Vorschau – echte Daten nach Publish</p>
        </div>
    </div>

    <!-- Login Modal -->
    <div id="df-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99999;align-items:center;justify-content:center;">
        <div class="df-modal-box">
            <h3 id="df-modal-title" style="margin:0 0 0.75rem;font-size:1rem;">🔐 Admin-Login</h3>
            <input type="password" id="df-pw" class="df-inp" placeholder="Passwort">
            <p id="df-err" style="display:none;color:#e74c3c;font-size:0.82rem;margin:0.4rem 0 0;"></p>
            <div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
                <button id="df-login-btn" class="df-btn-primary" style="flex:1;">Anmelden</button>
                <button class="df-btn-ghost" style="flex:1;" onclick="document.getElementById('df-modal').style.display='none'">Abbrechen</button>
            </div>
        </div>
    </div>
    <!-- Passwort-Modal -->
    <div id="df-pw-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99999;align-items:center;justify-content:center;">
        <div class="df-modal-box">
            <h3 style="margin:0 0 0.75rem;font-size:1rem;">🔑 Passwort ändern</h3>
            <input type="password" id="df-old-pw" class="df-inp" placeholder="Altes Passwort" style="margin-bottom:0.5rem;">
            <input type="password" id="df-new-pw" class="df-inp" placeholder="Neues Passwort (min. 4 Zeichen)" style="margin-bottom:0.5rem;">
            <input type="password" id="df-new-pw2" class="df-inp" placeholder="Neues Passwort bestätigen">
            <p id="df-pw-err" style="display:none;color:#e74c3c;font-size:0.82rem;margin:0.4rem 0 0;"></p>
            <div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
                <button onclick="dfChangePw()" class="df-btn-primary" style="flex:1;">Speichern</button>
                <button onclick="document.getElementById('df-pw-modal').style.display='none'" class="df-btn-ghost" style="flex:1;">Abbrechen</button>
            </div>
        </div>
    </div>
    <div id="df-toast" style="display:none;position:fixed;bottom:1.5rem;right:1.5rem;padding:0.75rem 1.25rem;border-radius:8px;z-index:99999;font-size:0.88rem;font-weight:600;color:#fff;"></div>

    <!-- Edit Modal -->
    <div id="df-edit-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99998;align-items:center;justify-content:center;">
        <div class="df-modal-box" style="max-width:500px;max-height:85vh;overflow-y:auto;">
            <h3 style="margin:0 0 0.75rem;font-size:1rem;">✏️ Eintrag bearbeiten</h3>
            <div id="df-edit-form"></div>
            <div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
                <button class="df-btn-primary" style="flex:1;" onclick="dfSaveEdit()">💾 Speichern</button>
                <button class="df-btn-ghost" style="flex:1;" onclick="document.getElementById('df-edit-modal').style.display='none'">Abbrechen</button>
            </div>
        </div>
    </div>

    <script>
    (function(){
        var sec = document.currentScript ? document.currentScript.closest('section.vn-dataform') : document.querySelector('section.vn-dataform');
        if (!sec) return;

        var inBuilder = (function(){
            if (document.querySelector('html').classList.contains('is-builder')) return true;
            if (window.location.port === '409' || window.location.protocol === 'file:') return true;
            var all = document.querySelectorAll('link[href],script[src]');
            for (var i=0;i<all.length;i++){
                var h=all[i].getAttribute('href')||all[i].getAttribute('src')||'';
                if (h.indexOf('file:')===0||h.indexOf('AppData')!==-1||h.indexOf('Mobirise')!==-1) return true;
            }
            return false;
        })();
        if (inBuilder) return;

        // Live: Dummy verstecken
        var dummy = document.getElementById('df-dummy');
        if (dummy) dummy.style.display = 'none';

        // Loading-Div dynamisch einfügen
        var ld = document.createElement('div');
        ld.id = 'df-loading';
        ld.className = 'df-loading';
        ld.textContent = '⟳ Lade…';
        document.getElementById('df-results').before(ld);

        var GID      = sec.dataset.gid || 'form-1';
        var VIEWMODE = sec.dataset.viewmode || 'cards';
        var PERPAGE  = parseInt(sec.dataset.perpage) || 10;
        var SEARCH   = sec.dataset.showsearch === 'true' || sec.dataset.showsearch === '1';
        var IMGSIZE  = parseInt(sec.dataset.imgsize) || 120;
        var IMGPOS   = sec.dataset.imgpos || 'top';
        var isAdmin  = false;
        var fields   = [];
        var curPage  = 0;
        var editId   = 0;

        var ep = (function(){
            var links = document.querySelectorAll('link[href]');
            for (var i=0;i<links.length;i++){
                var h=links[i].getAttribute('href')||'';
                if (h.indexOf('http')===0 && h.indexOf('assets/')!==-1)
                    return h.split('assets/')[0]+'assets/vn-dataform/api.php';
            }
            return 'assets/vn-dataform/api.php';
        })();

        function api(payload, cb) {
            fetch(ep + '?action=' + payload.action + '&gid=' + encodeURIComponent(GID), {
                method:'POST', headers:{'Content-Type':'application/json'},
                body: JSON.stringify(payload)
            }).then(function(r){ return r.json(); }).then(cb).catch(function(e){ cb({success:false,error:String(e)}); });
        }

        function toast(msg, err) {
            var t = document.getElementById('df-toast');
            t.textContent = msg; t.style.display = 'block';
            t.style.background = err ? '#e74c3c' : '#1d9e75';
            setTimeout(function(){ t.style.display='none'; }, 3000);
        }

        function esc(s){ return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }

        function fmtDate(val) {
            if (!val) return '';
            // YYYY-MM-DD → TT.MM.JJJJ
            var m = val.match(/^(\d{4})-(\d{2})-(\d{2})$/);
            if (m) return m[3] + '.' + m[2] + '.' + m[1];
            return val;
        }

        // ── Felder laden ──────────────────────────────────
        function loadFields(cb) {
            api({action:'get_fields'}, function(d){
                fields = d.fields || [];
                if (cb) cb();
            });
        }

      // ── Suchefeld ─────────────────────────────────────
    function initSearch() {

        var srch = document.getElementById('df-search');
        if (!srch) return;

        // Suchfeld immer zurücksetzen
        srch.value = "";
        srch.setAttribute("autocomplete", "off");
        srch.setAttribute("autocorrect", "off");
        srch.setAttribute("autocapitalize", "off");
        srch.setAttribute("spellcheck", "false");

        // Browser-Autofill zusätzlich verhindern
        setTimeout(function () {
            srch.value = "";
        }, 50);

        // Suche verstecken wenn Checkbox aus
        if (!SEARCH) {
            var wrap = document.getElementById('df-search-wrap');
            if (wrap) wrap.style.display = 'none';
            return;
        }

        srch.oninput = function () {
            curPage = 0;
            loadRecords();
        };
    }

        // ── Datensätze laden und anzeigen ─────────────────
        function loadRecords() {
            var search = (document.getElementById('df-search')||{value:''}).value;
            api({action:'get_records', search:search, page:parseInt(curPage), limit:parseInt(PERPAGE)}, function(d){
                renderResults(d.records||[], d.total||0);
            });
        }

        function renderResults(records, total) {
            console.log("Seite:", curPage, "Records:", records.length, "Total:", total);
            var ldEl = document.getElementById('df-loading');
            if (ldEl) ldEl.style.display = 'none';
            var div = document.getElementById('df-results');
            if (!records.length) {
                if (total > 0 && curPage > 0) {
                    // Seite existiert nicht mehr → zurück zu Seite 0
                    curPage = 0;
                    loadRecords();
                    return;
                }
                div.innerHTML = '<p class="df-hint">Keine Einträge gefunden.</p>';
                document.getElementById('df-pager').innerHTML = '';
                return;
            }
            var listFields = fields.filter(function(f){ return f.show_in_list == 1; });
            var html = '';

            if (VIEWMODE === 'table') {
                var tableFields = fields.length ? fields : listFields;
                html = '<div class="df-table-wrap"><table class="df-table"><thead><tr>';
                tableFields.forEach(function(f){ html += '<th>'+esc(f.label)+'</th>'; });
                if (isAdmin) html += '<th></th>';
                html += '</tr></thead><tbody>';
                records.forEach(function(r){
                    html += '<tr>';
                    tableFields.forEach(function(f){
                        var tdval = r.data[f.field_key]||'';
                        var tdhtml = f.type==='image' && tdval
                            ? '<img src="'+tdval+'" style="height:'+Math.round(IMGSIZE*0.5)+'px;max-width:120px;border-radius:4px;object-fit:cover;cursor:zoom-in;" class="df-lb-img" data-lbsrc="'+tdval+'">' 
                            : (f.type==='date' ? fmtDate(tdval) : esc(tdval));
                        html += '<td>'+tdhtml+'</td>';
                    });
                    if (isAdmin) html += '<td><button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> <button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button></td>';
                    html += '</tr>';
                });
                html += '</tbody></table></div>';
            } else {
                html = '<div class="df-cards">';
                records.forEach(function(r){
                    var firstField = listFields[0];
                    var title = firstField ? esc(r.data[firstField.field_key]||'—') : '#'+r.id;
                    // Bild-Felder für diese Karte
                    var imgField = fields.find(function(f){ return f.type==='image' && r.data[f.field_key]; });
                    var imgUrl   = imgField ? r.data[imgField.field_key] : '';

                    html += '<div class="df-card">';

                    // Bild oben
                    if (imgUrl && IMGPOS==='top') {
                        html += '<div style="overflow:hidden;max-height:'+IMGSIZE+'px;cursor:zoom-in;" onclick="dfLightbox(this.dataset.lbsrc)" data-lbsrc="'+imgUrl+'">'
                            + '<img src="'+imgUrl+'" class="df-card-img" style="width:100%;height:'+IMGSIZE+'px;object-fit:cover;display:block;">'
                            + '</div>';
                    }

                    html += '<div class="df-card-head">'+title;
                    if (isAdmin) html += '<span style="float:right;"><button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> <button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button></span>';
                    html += '</div><div class="df-card-body">';
                    listFields.slice(1).forEach(function(f){
                        var val = r.data[f.field_key]||'';
                        if (!val) return;
                        var link = '';
                        if (f.type==='email') link='<a href="mailto:'+esc(val)+'" style="color:inherit;">'+esc(val)+'</a>';
                        else if (f.type==='tel') link='<a href="tel:'+esc(val)+'" style="color:inherit;">'+esc(val)+'</a>';
                        else if (f.type==='url') link='<a href="'+esc(val)+'" target="_blank" style="color:inherit;">'+esc(val)+'</a>';
                        else if (f.type==='image') {
                            if (IMGPOS==='top') return; // schon oben gezeigt
                            link='<img src="'+val+'" style="max-width:100%;max-height:'+IMGSIZE+'px;border-radius:4px;object-fit:cover;cursor:zoom-in;" class="df-lb-img" data-lbsrc="'+val+'">';
                        }
                        else if (f.type==='date') link=fmtDate(val);
                        else link=esc(val);
                        html += '<div class="df-row"><span class="df-lbl">'+esc(f.label)+'</span><span class="df-val">'+link+'</span></div>';
                    });
                    html += '</div></div>';
                });
                html += '</div>';
            }
    // ---------------- Pagination ----------------

    var pages = Math.ceil(total / PERPAGE);

    if (pages > 1) {

        var phtml = '<div class="df-pager"><div class="df-pager-inner">';

        // Erste
        phtml += '<button class="df-page-btn'+(curPage==0?' df-page-disabled':'')+'" onclick="'+(curPage==0?'return false':'dfGoPage(0)')+'">« Erste</button>';

        // Zurück
        phtml += '<button class="df-page-btn'+(curPage==0?' df-page-disabled':'')+'" onclick="'+(curPage==0?'return false':'dfGoPage('+(curPage-1)+')')+'">‹ Zurück</button>';

        // Seitenbereich
        var start = Math.max(0, curPage-2);
        var end   = Math.min(pages-1, curPage+2);

        if (start > 0) {
            phtml += '<button class="df-page-btn" onclick="dfGoPage(0)">1</button>';
            if (start > 1)
                phtml += '<span class="df-page-dots">…</span>';
        }

        for (var i=start;i<=end;i++) {
            phtml += '<button class="df-page-btn'+(i==curPage?' df-page-active':'')+'" onclick="dfGoPage('+i+')">'+(i+1)+'</button>';
        }

        if (end < pages-1) {

            if (end < pages-2)
                phtml += '<span class="df-page-dots">…</span>';

            phtml += '<button class="df-page-btn" onclick="dfGoPage('+(pages-1)+')">'+pages+'</button>';
        }

        // Weiter
        phtml += '<button class="df-page-btn'+(curPage>=pages-1?' df-page-disabled':'')+'" onclick="'+(curPage>=pages-1?'return false':'dfGoPage('+(curPage+1)+')')+'">Weiter ›</button>';

        // Letzte
        phtml += '<button class="df-page-btn'+(curPage>=pages-1?' df-page-disabled':'')+'" onclick="'+(curPage>=pages-1?'return false':'dfGoPage('+(pages-1)+')')+'">Letzte »</button>';

        phtml += '</div></div>';

        html += phtml;
    }
    console.log(html);
            div.innerHTML = html;

            // Lightbox-Delegation
            div.querySelectorAll('img.df-lb-img').forEach(function(img){
                img.style.cursor = 'zoom-in';
                img.onclick = function(){ dfLightbox(this.dataset.lbsrc); };
            });
        }
		
        // Scroll an Beginn der Anwendung
        window.dfGoPage = function(p) {

            if (p < 0) p = 0;

            curPage = p;

            loadRecords();

            var el = document.getElementById("df-results");

            if (el) {
                var navOffset = 160; // Anpassen

                var zielPosition =
                    el.getBoundingClientRect().top +
                    window.pageYOffset -
                    navOffset;

                window.scrollTo({
                    top: zielPosition,
                    behavior: "smooth"
                });
            }
        };

        // ── Admin: Datensatz bearbeiten ───────────────────
        window.dfEditRecord = function(id) {
            editId = id;
            api({action:'get_records', page:0, limit:9999}, function(d){
                var rec = (d.records||[]).find(function(r){ return r.id===id; });
                if (!rec) return;
                var html = '';
                fields.forEach(function(f){
                    var v = esc(rec.data[f.field_key]||'');
                    var einp;
                    if (f.type==='textarea') {
                        einp = '<textarea class="df-inp" id="dfe_'+f.field_key+'" rows="4" style="min-height:80px;resize:vertical;">'+v+'</textarea>';
                    } else if (f.type==='image') {
                        einp = '<div style="display:flex;flex-direction:column;gap:0.4rem;">'
                            + (v ? '<img src="'+v+'" style="max-height:60px;border-radius:4px;object-fit:cover;margin-bottom:4px;">' : '')
                            + '<input type="file" accept="image/*" class="df-inp" id="dfe_file_'+f.field_key+'" style="padding:4px;">'
                            + '<input type="hidden" id="dfe_'+f.field_key+'" value="'+v+'">'
                            + '</div>';
                    } else {
                        einp = '<input type="'+f.type+'" class="df-inp" id="dfe_'+f.field_key+'" value="'+v+'">';
                    }
                    html += '<label class="df-field-lbl">'+esc(f.label)+einp+'</label>';
                });
                document.getElementById('df-edit-form').innerHTML = html;
                document.getElementById('df-edit-modal').style.display = 'flex';
            });
        };

        window.dfSaveEdit = function() {
            uploadImages('dfe_', function(){
                var data = {};
                fields.forEach(function(f){
                    var el = document.getElementById('dfe_'+f.field_key);
                    if (el) data[f.field_key] = el.value;
                });
                api({action:'save_record', id:editId, data:data}, function(d){
                    if (d.success) {
                        toast(d.message||'✅ Gespeichert!');
                        document.getElementById('df-edit-modal').style.display='none';
                        loadRecords();
                        renderAdminList();
                    } else toast(d.error||'Fehler', true);
                });
            });
        };

        window.dfDeleteRecord = function(id) {
            if (!confirm('Eintrag löschen?')) return;
            api({action:'delete_record', id:id}, function(d){
                if (d.success) { toast(d.message||'Gelöscht!'); loadRecords(); renderAdminList(); }
                else toast(d.error||'Fehler', true);
            });
        };

        // ── Admin: Eintrags-Liste ─────────────────────────
        function renderAdminList() {
            api({action:'get_records', page:0, limit:999}, function(d){
                var records = d.records||[];
                var div = document.getElementById('df-admin-list');
                if (!records.length) { div.innerHTML='<p class="df-hint">Noch keine Einträge.</p>'; return; }
                var html = '<table class="df-table"><thead><tr>';
                fields.slice(0,3).forEach(function(f){ html+='<th>'+esc(f.label)+'</th>'; });
                html += '<th></th></tr></thead><tbody>';
                records.forEach(function(r){
                    html += '<tr data-id="'+r.id+'">';
                    fields.slice(0,3).forEach(function(f){ html+='<td>'+esc(r.data[f.field_key]||'')+'</td>'; });
                    html += '<td style="white-space:nowrap;">'
                        +'<button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> '
                        +'<button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button>'
                        +'</td></tr>';
                });
                html += '</tbody></table>';
                div.innerHTML = html;
                // Drag & Drop Zeilen
                var dragSrcRow = null;
                div.querySelectorAll('tbody tr').forEach(function(tr){
                    tr.draggable = true;
                    tr.style.cursor = 'grab';
                    tr.addEventListener('dragstart',function(){ dragSrcRow=tr; setTimeout(function(){tr.style.opacity='0.4';},0); });
                    tr.addEventListener('dragend',function(){ tr.style.opacity='1'; div.querySelectorAll('tbody tr').forEach(function(r){r.style.outline='';}); });
                    tr.addEventListener('dragover',function(e){ e.preventDefault(); tr.style.outline='2px solid #1d9e75'; });
                    tr.addEventListener('dragleave',function(){ tr.style.outline=''; });
                    tr.addEventListener('drop',function(e){
                        e.preventDefault(); tr.style.outline='';
                        if (!dragSrcRow || dragSrcRow===tr) return;
                        var tbody = tr.closest('tbody');
                        var rows = Array.from(tbody.querySelectorAll('tr'));
                        var si = rows.indexOf(dragSrcRow), ti = rows.indexOf(tr);
                        if (si < ti) tbody.insertBefore(dragSrcRow, tr.nextSibling);
                        else tbody.insertBefore(dragSrcRow, tr);
                        dragSrcRow = null;
                        // Neue Reihenfolge speichern
                        var newOrder = Array.from(tbody.querySelectorAll('tr')).map(function(r){
                            return parseInt(r.dataset.id);
                        }).filter(function(id){ return !isNaN(id); });
                        if (newOrder.length) {
                            api({action:'reorder', ids:newOrder}, function(d){
                                if (d.success) toast('✅ Reihenfolge gespeichert!');
                            });
                        }
                    });
                });
            });
        }

        // ── Admin: Felder ─────────────────────────────────
        window.dfTab = function(tab) {
            ['records','fields','new'].forEach(function(t){
                document.getElementById('df-tab-'+t).style.display = t===tab ? 'block' : 'none';
                var btn = document.querySelector('[data-tab="'+t+'"]');
                if (btn) btn.classList.toggle('df-tab-active', t===tab);
            });
            if (tab==='records') renderAdminList();
            if (tab==='fields')  renderFieldEditor();
            if (tab==='new')     renderNewForm();
        };

        function renderFieldEditor() {
            var html = '';
            fields.forEach(function(f,i){
                html += '<div class="df-field-row" data-idx="'+i+'">'
                    + '<span class="df-drag-handle">⠿</span>'
                    + '<input type="text" class="df-inp df-fl-name" value="'+esc(f.label)+'" placeholder="Feldname">'
                    + '<select class="df-inp df-fl-type">'
                    + ['text','email','tel','url','number','date','textarea','image'].map(function(t){
                        return '<option value="'+t+'"'+(f.type===t?' selected':'')+'>'+t+'</option>';
                      }).join('')
                    + '</select>'
                    + '<label style="color:#888;font-size:0.78rem;white-space:nowrap;">'
                    + '<input type="checkbox" class="df-fl-req"'+(f.required==1?' checked':'')+'> Pflicht</label>'
                    + '<label style="color:#888;font-size:0.78rem;white-space:nowrap;">'
                    + '<input type="checkbox" class="df-fl-show"'+(f.show_in_list==1?' checked':'')+'> Anzeigen</label>'
                    + '<button onclick="this.closest(\'.df-field-row\').remove()" style="background:#e74c3c;color:#fff;border:none;border-radius:4px;padding:4px 8px;cursor:pointer;">✕</button>'
                    + '</div>';
            });
            document.getElementById('df-field-list').innerHTML = html;
        }

        window.dfAddField = function() {
            var li = document.getElementById('df-field-list');
            var div = document.createElement('div');
            div.className = 'df-field-row';
            div.innerHTML = '<span class="df-drag-handle">⠿</span>'
                + '<input type="text" class="df-inp df-fl-name" placeholder="Feldname">'
                + '<select class="df-inp df-fl-type">'
                + ['text','email','tel','url','number','date','textarea','image'].map(function(t){
                    return '<option value="'+t+'">'+t+'</option>';
                  }).join('')
                + '</select>'
                + '<label style="color:#888;font-size:0.78rem;white-space:nowrap;"><input type="checkbox" class="df-fl-req"> Pflicht</label>'
                + '<label style="color:#888;font-size:0.78rem;white-space:nowrap;"><input type="checkbox" class="df-fl-show" checked> Anzeigen</label>'
                + '<button onclick="this.closest(\'.df-field-row\').remove()" style="background:#e74c3c;color:#fff;border:none;border-radius:4px;padding:4px 8px;cursor:pointer;">✕</button>';
            li.appendChild(div);
        };

        window.dfSaveFields = function() {
            var rows = document.querySelectorAll('#df-field-list .df-field-row');
            var newFields = [];
            rows.forEach(function(row){
                var name = row.querySelector('.df-fl-name').value.trim();
                if (!name) return;
                newFields.push({
                    label:       name,
                    type:        row.querySelector('.df-fl-type').value,
                    required:    row.querySelector('.df-fl-req').checked ? 1 : 0,
                    show_in_list:row.querySelector('.df-fl-show').checked ? 1 : 0,
                });
            });
            api({action:'save_fields', fields:newFields}, function(d){
                if (d.success) {
                    toast(d.message||'✅ Felder gespeichert!');
                    loadFields(function(){ loadRecords(); renderNewForm(); });
                } else toast(d.error||'Fehler', true);
            });
        };

        // ── Admin: Neuer Eintrag ──────────────────────────
        function renderNewForm() {
            editId = 0;
            var html = '';
            fields.forEach(function(f){
                var inp;
                if (f.type==='textarea') {
                    inp = '<textarea class="df-inp" id="dfn_'+f.field_key+'" rows="4" style="min-height:80px;resize:vertical;"></textarea>';
                } else if (f.type==='image') {
                    inp = '<div style="display:flex;flex-direction:column;gap:0.4rem;">'
                        + '<input type="file" accept="image/*" class="df-inp" id="dfn_file_'+f.field_key+'" style="padding:4px;">'
                        + '<input type="hidden" id="dfn_'+f.field_key+'">'
                        + '<img id="dfn_prev_'+f.field_key+'" src style="display:none;max-height:80px;border-radius:4px;margin-top:4px;object-fit:cover;">'
                        + '</div>';
                } else {
                    inp = '<input type="'+f.type+'" class="df-inp" id="dfn_'+f.field_key+'">';
                }
                html += '<label class="df-field-lbl">'+esc(f.label)+(f.required==1?' <span style="color:#e74c3c;">*</span>':'')+inp+'</label>';
            });
            if (!html) html = '<p class="df-hint">Zuerst Felder unter 🔧 Felder anlegen!</p>';
            document.getElementById('df-new-form').innerHTML = html;
        }

        window.dfSaveRecord = function() {
            uploadImages('dfn_', function(){
                var data = {};
                fields.forEach(function(f){
                    var el = document.getElementById('dfn_'+f.field_key);
                    if (el) data[f.field_key] = el.value;
                });
                api({action:'save_record', id:0, data:data}, function(d){
                    if (d.success) {
                        toast(d.message||'✅ Gespeichert!');
                        renderNewForm();
                        loadRecords();
                    } else toast(d.error||'Fehler', true);
                });
            });
        };

        // Alle Bild-Felder vor dem Speichern hochladen
        function uploadImages(prefix, cb) {
            var imgFields = fields.filter(function(f){ return f.type==='image'; });
            var pending = 0;
            if (!imgFields.length) { cb(); return; }
            imgFields.forEach(function(f){
                var fileInp = document.getElementById(prefix+'file_'+f.field_key);
                if (!fileInp || !fileInp.files || !fileInp.files[0]) return;
                pending++;
                var fd = new FormData();
                fd.append('img', fileInp.files[0]);
                fetch(ep + '?action=upload_image&gid=' + encodeURIComponent(GID), {method:'POST', body:fd})
                    .then(function(r){ return r.json(); })
                    .then(function(d){
                        if (d.success) {
                            var hidden = document.getElementById(prefix+f.field_key);
                            if (hidden) hidden.value = d.url;
                        }
                        pending--;
                        if (pending===0) cb();
                    })
                    .catch(function(){ pending--; if(pending===0) cb(); });
            });
            if (pending===0) cb();
        }

        window.dfLightbox = function(url) {
            var lb  = document.getElementById('df-lightbox');
            var img = document.getElementById('df-lightbox-img');
            if (!lb || !img) return;
            img.src = url;
            lb.style.display = 'flex';
        };
        document.addEventListener('keydown', function(e){
            if (e.key==='Escape') {
                var lb = document.getElementById('df-lightbox');
                if (lb) lb.style.display = 'none';
            }
        });

        window.dfExport = function(e) {
            e.preventDefault();

            var exportUrl = ep
                + '?action=export_csv'
                + '&gid=' + encodeURIComponent(GID)
                + '&_=' + Date.now();

            window.location.href = exportUrl;
        };        

        window.dfChangePw = function() {
            var old  = document.getElementById('df-old-pw').value;
            var nw   = document.getElementById('df-new-pw').value;
            var nw2  = document.getElementById('df-new-pw2').value;
            var err  = document.getElementById('df-pw-err');
            if (nw !== nw2) { err.textContent='Passwörter stimmen nicht überein.'; err.style.display='block'; return; }
            api({action:'change_password', old_password:old, new_password:nw}, function(d){
                if (d.success) {
                    toast(d.message||'✅ Passwort geändert!');
                    document.getElementById('df-pw-modal').style.display='none';
                    ['df-old-pw','df-new-pw','df-new-pw2'].forEach(function(id){
                        var el=document.getElementById(id); if(el) el.value='';
                    });
                } else {
                    err.textContent = d.error||'Fehler';
                    err.style.display = 'block';
                }
            });
        };

        window.dfLogout = function() {
            api({action:'logout'}, function(){
                isAdmin=false;
                document.getElementById('df-admin').style.display='none';
                loadRecords();
            });
        };

        // ── Login ─────────────────────────────────────────
        function openLogin() {
            api({action:'check'}, function(d){
                if (d.loggedIn) { isAdmin=true; showAdmin(); return; }
                var modal = document.getElementById('df-modal');
                var title = document.getElementById('df-modal-title');
                var err   = document.getElementById('df-err');
                var pw    = document.getElementById('df-pw');
                if (!modal) return;
                if (title) title.textContent = d.firstRun ? '⚙️ Erst-Einrichtung' : '🔐 Admin-Login';
                if (err) err.style.display = 'none';
                if (pw) pw.value = '';
                modal.style.display = 'flex';
                setTimeout(function(){ if(pw) pw.focus(); }, 100);
            });
        }
        
        /* Login auch über externen Textlink aufrufbar machen */
        window.dfOpenLogin = openLogin;

        function showAdmin() {
            document.getElementById('df-admin').style.display = 'block';
            dfTab('records');
        }

        document.getElementById('df-login-btn').onclick = function(){
            var pw  = document.getElementById('df-pw');
            var err = document.getElementById('df-err');
            api({action:'login', password: pw ? pw.value : ''}, function(d){
                if (d.success) {
                    isAdmin = true;
                    document.getElementById('df-modal').style.display = 'none';
                    showAdmin();
                } else {
                    if (err) { err.textContent = d.error||'Falsches Passwort.'; err.style.display='block'; }
                }
            });
        };

        var pwEl = document.getElementById('df-pw');
        if (pwEl) pwEl.addEventListener('keypress', function(e){ if(e.key==='Enter') document.getElementById('df-login-btn').click(); });

        document.addEventListener('dblclick', function(e){ if(e.ctrlKey){ e.preventDefault(); openLogin(); }});
        document.addEventListener('keydown',  function(e){ if(e.ctrlKey&&e.shiftKey&&(e.key==='A'||e.key==='a')){ e.preventDefault(); openLogin(); }});

        // Handy-Trigger
        var mt = sec.dataset.mobiletrigger || 'shake';
        var isTouch = ('ontouchstart' in window || navigator.maxTouchPoints > 0);
        if (mt==='button' && isTouch) {
            var fab = document.createElement('button');
            fab.innerHTML='⚙️';
            fab.style.cssText='position:fixed;bottom:1.5rem;left:1rem;width:44px;height:44px;border-radius:50%;background:#1d9e75;border:none;color:#fff;font-size:1.2rem;cursor:pointer;z-index:9998;';
            fab.onclick=openLogin;
            document.body.appendChild(fab);
        } else if (mt==='tap4') {
            var _tc=0,_tt=null;
            sec.addEventListener('touchend',function(){
                _tc++;clearTimeout(_tt);
                _tt=setTimeout(function(){_tc=0;},1500);
                if(_tc>=4){_tc=0;openLogin();}
            });
        } else if (mt==='shake') {
            var _sx=0,_sy=0,_sz=0,_st=0;
            if (window.DeviceMotionEvent) {
                window.addEventListener('devicemotion',function(e){
                    var a=e.accelerationIncludingGravity;if(!a)return;
                    var dv=Math.abs((a.x||0)-_sx)+Math.abs((a.y||0)-_sy)+Math.abs((a.z||0)-_sz);
                    _sx=a.x||0;_sy=a.y||0;_sz=a.z||0;
                    if(dv>25){var n=Date.now();if(n-_st>1500){_st=n;openLogin();}}
                });
            }
        }

        // Start
        loadFields(function(){
            initSearch();
            loadRecords();
        });
    })();
    </script>
    
    <!-- Lightbox -->
    <div id="df-lightbox" style="display:none;position:fixed;inset:0;background: rgba(0, 0, 0, 0.8);z-index:99999;align-items:center;justify-content:center;cursor:zoom-out;" onclick="document.getElementById('df-lightbox').style.display='none'">
        <img id="df-lightbox-img" src style="max-width:92%;max-height:92%;border-radius:10px;box-shadow:0 0 40px #000;object-fit:contain;">
    </div>

</section>
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Und auch nochmals die aktuelle Datei api.php, die ich ja auch angepasst habe für den CSV-Export:

Code: Alles auswählen

<?php
declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');
date_default_timezone_set('Europe/Berlin');

if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

/*
 * Datenverzeichnis
 */
$dataDir = __DIR__ . '/data';

if (!is_dir($dataDir)) {
    mkdir($dataDir, 0755, true);
}

file_put_contents(
    $dataDir . '/.htaccess',
    "Deny from all\n",
    LOCK_EX
);

/*
 * Galerie-/Formular-ID bereinigen
 */
$gid = preg_replace(
    '/[^a-z0-9_-]/i',
    '',
    $_GET['gid'] ?? 'default'
);

if ($gid === '') {
    $gid = 'default';
}

$dbFile = $dataDir . '/' . $gid . '.sqlite';
$pwFile = $dataDir . '/' . $gid . '.hash';

/*
 * JSON-Antwort ausgeben
 */
function respond(bool $ok, array $extra = []): void
{
    echo json_encode(
        array_merge(['success' => $ok], $extra),
        JSON_UNESCAPED_UNICODE
    );
    exit;
}

/*
 * Adminstatus prüfen
 */
function isAdmin(string $gid): bool
{
    return !empty($_SESSION['vndf_admin_' . $gid]);
}

/*
 * Öffentlichen URL-Pfad des aktuellen PHP-Verzeichnisses ermitteln.
 *
 * Beispiel:
 * SCRIPT_NAME:
 * /VN-Block-Galerie/assets/vn-dataform/dataform.php
 *
 * Ergebnis:
 * /VN-Block-Galerie/assets/vn-dataform
 */
function getPublicScriptDirectory(): string
{
    $scriptName = $_SERVER['SCRIPT_NAME']
        ?? $_SERVER['PHP_SELF']
        ?? '';

    $scriptName = str_replace('\\', '/', $scriptName);

    /*
     * Querystrings vorsorglich entfernen.
     */
    $scriptPath = parse_url($scriptName, PHP_URL_PATH);

    if (!is_string($scriptPath) || $scriptPath === '') {
        return '';
    }

    $directory = dirname($scriptPath);
    $directory = str_replace('\\', '/', $directory);

    /*
     * dirname('/') kann "/" ergeben.
     */
    if ($directory === '/' || $directory === '.') {
        return '';
    }

    return '/' . trim($directory, '/');
}

/*
 * Datenbank initialisieren
 */
$db = new PDO('sqlite:' . $dbFile);

$db->setAttribute(
    PDO::ATTR_ERRMODE,
    PDO::ERRMODE_EXCEPTION
);

$db->exec(
    "CREATE TABLE IF NOT EXISTS vndf_fields (
        id INTEGER PRIMARY KEY,
        sort_order INTEGER DEFAULT 0,
        field_key TEXT UNIQUE,
        label TEXT,
        type TEXT DEFAULT 'text',
        required INTEGER DEFAULT 0,
        searchable INTEGER DEFAULT 1,
        show_in_list INTEGER DEFAULT 1
    )"
);

$db->exec(
    "CREATE TABLE IF NOT EXISTS vndf_records (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        sort_order INTEGER DEFAULT 0,
        created_at TEXT DEFAULT (datetime('now','localtime')),
        updated_at TEXT DEFAULT (datetime('now','localtime')),
        data TEXT
    )"
);

/*
 * sort_order-Spalte bei älteren Datenbanken ergänzen.
 */
try {
    $db->exec(
        "ALTER TABLE vndf_records
         ADD COLUMN sort_order INTEGER DEFAULT 0"
    );
} catch (Exception $e) {
    /*
     * Die Spalte existiert bereits.
     */
}

/*
 * JSON-Eingabe lesen
 */
$rawInput = file_get_contents('php://input');
$input    = json_decode($rawInput ?: '', true);

if (!is_array($input)) {
    $input = [];
}

$action = $_GET['action']
    ?? $input['action']
    ?? '';

/*
 * ─────────────────────────────────────────────
 * Authentifizierung
 * ─────────────────────────────────────────────
 */

if ($action === 'check') {
    respond(
        true,
        [
            'loggedIn' => isAdmin($gid),
            'firstRun' => !file_exists($pwFile)
        ]
    );
}

if ($action === 'login') {
    $pw = (string)($input['password'] ?? '');

    /*
     * Beim ersten Aufruf Passwort anlegen.
     */
    if (!file_exists($pwFile)) {
        if (strlen($pw) < 4) {
            respond(
                false,
                ['error' => 'Min. 4 Zeichen.']
            );
        }

        $passwordHash = password_hash(
            $pw,
            PASSWORD_BCRYPT
        );

        if ($passwordHash === false) {
            respond(
                false,
                ['error' => 'Passwort konnte nicht gespeichert werden.']
            );
        }

        if (
            file_put_contents(
                $pwFile,
                $passwordHash,
                LOCK_EX
            ) === false
        ) {
            respond(
                false,
                ['error' => 'Passwortdatei konnte nicht geschrieben werden.']
            );
        }

        $_SESSION['vndf_admin_' . $gid] = true;

        respond(
            true,
            ['firstRun' => true]
        );
    }

    $storedHash = trim(
        (string)file_get_contents($pwFile)
    );

    if (
        $storedHash !== ''
        && password_verify($pw, $storedHash)
    ) {
        $_SESSION['vndf_admin_' . $gid] = true;
        respond(true);
    }

    respond(
        false,
        ['error' => 'Falsches Passwort.']
    );
}

if ($action === 'logout') {
    unset($_SESSION['vndf_admin_' . $gid]);
    respond(true);
}

/*
 * ─────────────────────────────────────────────
 * Felder
 * ─────────────────────────────────────────────
 */

if ($action === 'get_fields') {
    $rows = $db
        ->query(
            "SELECT *
             FROM vndf_fields
             ORDER BY sort_order, id"
        )
        ->fetchAll(PDO::FETCH_ASSOC);

    respond(
        true,
        ['fields' => $rows]
    );
}

if ($action === 'save_fields') {
    if (!isAdmin($gid)) {
        respond(
            false,
            ['error' => 'Nicht autorisiert.']
        );
    }

    $fields = $input['fields'] ?? [];

    if (!is_array($fields)) {
        $fields = [];
    }

    $db->beginTransaction();

    try {
        $db->exec(
            "DELETE FROM vndf_fields"
        );

        $stmt = $db->prepare(
            "INSERT INTO vndf_fields (
                sort_order,
                field_key,
                label,
                type,
                required,
                searchable,
                show_in_list
            ) VALUES (?, ?, ?, ?, ?, ?, ?)"
        );

        foreach ($fields as $i => $f) {
            if (!is_array($f)) {
                continue;
            }

            $label = trim(
                (string)($f['label'] ?? '')
            );

            if ($label === '') {
                $label = 'Feld ' . ($i + 1);
            }

            $key = preg_replace(
                '/[^a-z0-9_]/i',
                '_',
                strtolower($label)
            );

            if ($key === null || $key === '') {
                $key = 'feld_' . $i;
            }

            $stmt->execute(
                [
                    $i,
                    $key,
                    $label,
                    $f['type'] ?? 'text',
                    (int)($f['required'] ?? 0),
                    (int)($f['searchable'] ?? 1),
                    (int)($f['show_in_list'] ?? 1)
                ]
            );
        }

        $db->commit();
    } catch (Throwable $e) {
        if ($db->inTransaction()) {
            $db->rollBack();
        }

        respond(
            false,
            ['error' => 'Felder konnten nicht gespeichert werden.']
        );
    }

    respond(
        true,
        ['message' => '✅ Felder gespeichert!']
    );
}

/*
 * ─────────────────────────────────────────────
 * Datensätze
 * ─────────────────────────────────────────────
 */

if ($action === 'get_records') {
    $search = trim(
        (string)($input['search'] ?? '')
    );

    $page = max(
        0,
        (int)($input['page'] ?? 0)
    );

    $limit = max(
        5,
        min(
            100,
            (int)($input['limit'] ?? 20)
        )
    );

    $all = $db
        ->query(
            "SELECT *
             FROM vndf_records
             ORDER BY sort_order ASC, id ASC"
        )
        ->fetchAll(PDO::FETCH_ASSOC);

    $rows = [];

    foreach ($all as $row) {
        $data = json_decode(
            $row['data'] ?? '{}',
            true
        );

        if (!is_array($data)) {
            $data = [];
        }

        if ($search !== '') {
            $found = false;

            foreach ($data as $value) {
                if (
                    stripos(
                        (string)$value,
                        $search
                    ) !== false
                ) {
                    $found = true;
                    break;
                }
            }

            if (!$found) {
                continue;
            }
        }

        $rows[] = [
            'id'         => $row['id'],
            'created_at' => $row['created_at'],
            'data'       => $data
        ];
    }

    $total = count($rows);

    $slice = array_slice(
        $rows,
        $page * $limit,
        $limit
    );

    respond(
        true,
        [
            'records' => $slice,
            'total'   => $total,
            'page'    => $page,
            'limit'   => $limit
        ]
    );
}

if ($action === 'save_record') {
    if (!isAdmin($gid)) {
        respond(
            false,
            ['error' => 'Nicht autorisiert.']
        );
    }

    $data = $input['data'] ?? [];

    if (!is_array($data)) {
        $data = [];
    }

    $id = (int)($input['id'] ?? 0);

    $json = json_encode(
        $data,
        JSON_UNESCAPED_UNICODE
    );

    if ($json === false) {
        respond(
            false,
            ['error' => 'Datensatz konnte nicht verarbeitet werden.']
        );
    }

    if ($id > 0) {
        $stmt = $db->prepare(
            "UPDATE vndf_records
             SET data = ?,
                 updated_at = datetime('now','localtime')
             WHERE id = ?"
        );

        $stmt->execute(
            [$json, $id]
        );
    } else {
        $stmt = $db->prepare(
            "INSERT INTO vndf_records (data)
             VALUES (?)"
        );

        $stmt->execute(
            [$json]
        );

        $id = (int)$db->lastInsertId();
    }

    respond(
        true,
        [
            'message' => '✅ Gespeichert!',
            'id'      => $id
        ]
    );
}

if ($action === 'delete_record') {
    if (!isAdmin($gid)) {
        respond(
            false,
            ['error' => 'Nicht autorisiert.']
        );
    }

    $id = (int)($input['id'] ?? 0);

    if ($id > 0) {
        $stmt = $db->prepare(
            "DELETE FROM vndf_records
             WHERE id = ?"
        );

        $stmt->execute(
            [$id]
        );
    }

    respond(
        true,
        ['message' => '🗑 Gelöscht!']
    );
}

/*
 * ─────────────────────────────────────────────
 * CSV-Export
 * ─────────────────────────────────────────────
 */

if ($action === 'export_csv') {
    if (!isAdmin($gid)) {
        respond(
            false,
            ['error' => 'Nicht autorisiert.']
        );
    }

    $fields = $db
        ->query(
            "SELECT *
             FROM vndf_fields
             ORDER BY sort_order, id"
        )
        ->fetchAll(PDO::FETCH_ASSOC);

    $records = $db
        ->query(
            "SELECT *
             FROM vndf_records
             ORDER BY sort_order ASC, id ASC"
        )
        ->fetchAll(PDO::FETCH_ASSOC);

    header(
        'Content-Type: text/csv; charset=utf-8'
    );

    header(
        'Content-Disposition: attachment; filename="' .
        $gid .
        '_export.csv"'
    );

    $out = fopen(
        'php://output',
        'w'
    );

    if ($out === false) {
        respond(
            false,
            ['error' => 'CSV-Datei konnte nicht erstellt werden.']
        );
    }

    /*
     * UTF-8-BOM für Excel.
     */
    fprintf(
        $out,
        chr(0xEF) .
        chr(0xBB) .
        chr(0xBF)
    );

    fputcsv(
        $out,
        array_column($fields, 'label'),
        ';'
    );

    foreach ($records as $record) {
        $data = json_decode(
            $record['data'] ?? '{}',
            true
        );

        if (!is_array($data)) {
            $data = [];
        }

        $csvRow = array_map(
            static function (array $field) use ($data): string {
                return (string)(
                    $data[$field['field_key']] ?? ''
                );
            },
            $fields
        );

        fputcsv(
            $out,
            $csvRow,
            ';'
        );
    }

    fclose($out);
    exit;
}

/*
 * ─────────────────────────────────────────────
 * Bild hochladen
 * ─────────────────────────────────────────────
 */

if ($action === 'upload_image') {
    if (!isAdmin($gid)) {
        respond(
            false,
            ['error' => 'Nicht autorisiert.']
        );
    }

    if (
        !isset($_FILES['img'])
        || !is_array($_FILES['img'])
        || empty($_FILES['img']['tmp_name'])
    ) {
        respond(
            false,
            ['error' => 'Keine Datei.']
        );
    }

    $uploadError = (int)(
        $_FILES['img']['error']
        ?? UPLOAD_ERR_NO_FILE
    );

    if ($uploadError !== UPLOAD_ERR_OK) {
        respond(
            false,
            ['error' => 'Fehler beim Hochladen der Datei. Fehlercode: ' . $uploadError]
        );
    }

    $tmpFile = (string)$_FILES['img']['tmp_name'];

    if (!is_uploaded_file($tmpFile)) {
        respond(
            false,
            ['error' => 'Ungültige Upload-Datei.']
        );
    }

    /*
     * Dateityp anhand des tatsächlichen MIME-Typs prüfen.
     */
    $allowedTypes = [
        'image/jpeg' => 'jpg',
        'image/png'  => 'png',
        'image/gif'  => 'gif',
        'image/webp' => 'webp'
    ];

    $mimeType = '';

    if (function_exists('finfo_open')) {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);

        if ($finfo !== false) {
            $detectedType = finfo_file(
                $finfo,
                $tmpFile
            );

            if (is_string($detectedType)) {
                $mimeType = $detectedType;
            }

            finfo_close($finfo);
        }
    }

    /*
     * Fallback, falls Fileinfo nicht verfügbar ist.
     */
    if ($mimeType === '') {
        $imageInfo = @getimagesize($tmpFile);

        if (
            is_array($imageInfo)
            && isset($imageInfo['mime'])
        ) {
            $mimeType = (string)$imageInfo['mime'];
        }
    }

    if (!isset($allowedTypes[$mimeType])) {
        respond(
            false,
            ['error' => 'Ungültiges Bildformat.']
        );
    }

    $extension = $allowedTypes[$mimeType];

    /*
     * Physischer Upload-Ordner:
     * assets/vn-dataform/uploads/<gid>/
     */
    $uploadDir = __DIR__
        . '/uploads/'
        . $gid;

    if (
        !is_dir($uploadDir)
        && !mkdir($uploadDir, 0755, true)
        && !is_dir($uploadDir)
    ) {
        respond(
            false,
            ['error' => 'Upload-Ordner konnte nicht angelegt werden.']
        );
    }

    /*
     * Sicheren, eindeutigen Dateinamen erzeugen.
     */
    try {
        $randomName = bin2hex(
            random_bytes(8)
        );
    } catch (Throwable $e) {
        $randomName = uniqid('', true);
        $randomName = preg_replace(
            '/[^a-z0-9]/i',
            '',
            $randomName
        );
    }

    $fileName = 'img_'
        . $randomName
        . '.'
        . $extension;

    $target = $uploadDir
        . '/'
        . $fileName;

    if (
        !move_uploaded_file(
            $tmpFile,
            $target
        )
    ) {
        respond(
            false,
            ['error' => 'Upload fehlgeschlagen.']
        );
    }

    /*
     * Öffentliche URL aus dem URL-Pfad des PHP-Scripts erzeugen.
     *
     * Es wird ausdrücklich NICHT mehr mit DOCUMENT_ROOT
     * oder realpath() gearbeitet.
     */
    $scriptDirectory = getPublicScriptDirectory();

    $publicUrl = $scriptDirectory
        . '/uploads/'
        . rawurlencode($gid)
        . '/'
        . rawurlencode($fileName);

    /*
     * Sicherstellen, dass die URL mit genau einem Slash beginnt.
     */
    $publicUrl = '/'
        . ltrim($publicUrl, '/');

    respond(
        true,
        [
            'url'     => $publicUrl,
            'message' => '✅ Hochgeladen!'
        ]
    );
}

/*
 * ─────────────────────────────────────────────
 * Passwort ändern
 * ─────────────────────────────────────────────
 */

if ($action === 'change_password') {
    if (!isAdmin($gid)) {
        respond(
            false,
            ['error' => 'Nicht autorisiert.']
        );
    }

    $oldPw = (string)(
        $input['old_password'] ?? ''
    );

    $newPw = (string)(
        $input['new_password'] ?? ''
    );

    $hash = file_exists($pwFile)
        ? trim((string)file_get_contents($pwFile))
        : '';

    if (
        $hash !== ''
        && !password_verify($oldPw, $hash)
    ) {
        respond(
            false,
            ['error' => 'Altes Passwort falsch.']
        );
    }

    if (strlen($newPw) < 4) {
        respond(
            false,
            ['error' => 'Min. 4 Zeichen.']
        );
    }

    $newHash = password_hash(
        $newPw,
        PASSWORD_BCRYPT
    );

    if ($newHash === false) {
        respond(
            false,
            ['error' => 'Neues Passwort konnte nicht erzeugt werden.']
        );
    }

    if (
        file_put_contents(
            $pwFile,
            $newHash,
            LOCK_EX
        ) === false
    ) {
        respond(
            false,
            ['error' => 'Neues Passwort konnte nicht gespeichert werden.']
        );
    }

    respond(
        true,
        ['message' => '✅ Passwort geändert!']
    );
}

/*
 * ─────────────────────────────────────────────
 * Reihenfolge speichern
 * ─────────────────────────────────────────────
 */

if ($action === 'reorder') {
    if (!isAdmin($gid)) {
        respond(
            false,
            ['error' => 'Nicht autorisiert.']
        );
    }

    $ids = $input['ids'] ?? [];

    if (!is_array($ids)) {
        $ids = [];
    }

    $stmt = $db->prepare(
        "UPDATE vndf_records
         SET sort_order = ?
         WHERE id = ?"
    );

    $db->beginTransaction();

    try {
        foreach ($ids as $i => $id) {
            $stmt->execute(
                [
                    $i,
                    (int)$id
                ]
            );
        }

        $db->commit();
    } catch (Throwable $e) {
        if ($db->inTransaction()) {
            $db->rollBack();
        }

        respond(
            false,
            ['error' => 'Reihenfolge konnte nicht gespeichert werden.']
        );
    }

    respond(
        true,
        ['message' => '✅ Reihenfolge gespeichert!']
    );
}

/*
 * Keine bekannte Aktion gefunden.
 */
respond(
    false,
    ['error' => 'Unbekannte Aktion: ' . $action]
);
Volker
Moderator
Moderator
Beiträge: 3143
Registriert: Sa 12. Dez 2020, 22:35

Re: Dataform Block

Ungelesener Beitrag von Volker »

So,
beides nun im aktuellen Download HTML und api ;)

Lasse ich jetzt auch so !
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Ja - habe ich gesehen und neu importiert.

Glaube auch nicht, dass da noch weitere Anpassungen notwendig sind. Die gemachten waren aber dringend notwendig.
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Moin Volker,

nur falls Du das noch anpassen wolltest, der Style der Lytebox ist so falsch. Dadurch wird das Bild immer größer als der sichtbare Bereich sein und daher kann man dann z.B. die Ecken nicht abrunden. Das habe ich bemerkt als ich denen einen Border-Radius von 30px geben wollte und nichts geschah. Natürlich haben die per Default dort eingetragenen 10px daher auch keinerlei Wirkung :(

Zusätzlich habe ich:

Code: Alles auswählen

onclick="this.style.display='none'"
verwendet. Das ist sicherer als erneut über die feste ID nach der Lightbox zu suchen.


Hier der alte Lightbox Code zum Vergleich:

Code: Alles auswählen

    <!-- Lightbox -->
    <div id="df-lightbox" style="display:none;position:fixed;inset:0;background: rgba(0, 0, 0, 0.8);z-index:99999;align-items:center;justify-content:center;cursor:zoom-out;" onclick="document.getElementById('df-lightbox').style.display='none'">
        <img id="df-lightbox-img" src style="max-width:92%;max-height:92%;border-radius:10px;box-shadow:0 0 40px #000;object-fit:contain;">
    </div>

Hier der von mir angepasste neue Code der Lightbox:

↗️ https://www.mobirise-tutorials.com/VN-B ... l#DataForm

Code: Alles auswählen

    <!-- Lightbox EDIT Tommy -->
    <div id="df-lightbox"
         style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.8);z-index:99999;align-items:center;justify-content:center;cursor:zoom-out;"
         onclick="this.style.display='none'">

        <img id="df-lightbox-img"
             src
             style="display:block;
                    width:auto;
                    height:auto;
                    max-width:min(1000px,92vw);
                    max-height:92vh;
                    border-radius:30px;
                    border: 2px solid #bbbbbb;
                    box-shadow:0 0 40px #000;">
    </div>
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Volker,

noch was, das wollte ich schon bei der VN-Tabelle bemerken und nun hier ber der VN-DataForm auch. Du hast da zwar in den Blockwerkzeugen die ID angelegt, die aber macht gar keinen Sinn (wie z.B. bei der VN-Gallery). Das Script ist nicht so ausgelegt, dass man die Anwendung mehrfach auf der gleichen Seite verwenden kann.

Die Formular-ID trennt die Datenbestände verschiedener Anwendungen. Die derzeitige Blockversion ist jedoch für eine einzelne Instanz pro Seite ausgelegt. Für mehrere Anwendungen auf derselben Seite müssten zusätzlich alle DOM-IDs und JavaScript-Funktionen je Block eindeutig gekapselt werden.
Volker
Moderator
Moderator
Beiträge: 3143
Registriert: Sa 12. Dez 2020, 22:35

Re: Dataform Block

Ungelesener Beitrag von Volker »

Stimmt Tommy :D

Das kommt halt vom kopieren ;) Stört jetzt aber auch keinen - oder ?
Ich bau die Blöcke ja hier im Stunden Takt, da nehm ich immer den Block und mach daraus einen neuen. Da kommt es schonmal vor das Inhalte da stehen die eigentlich nicht gebraucht werden ;)

Kann ja auch jeder leicht selber raus nehmen in dem er das <input type="text" title="Tabellen-ID (eindeutig!)" name="tableId" value="tabelle-1"> einach löscht ;)
Benutzeravatar
Tommy Herrmann
Site Admin
Site Admin
Beiträge: 9178
Registriert: So 6. Dez 2020, 07:37
Wohnort: Berlin
Kontaktdaten:

Re: Dataform Block

Ungelesener Beitrag von Tommy Herrmann »

Ja klar kann das passieren, deshalb mache ich Dich ja darauf aufmerksam. Besser wäre, wenn Du bei weiteren Anwendungen, die auf diesem Konzept basieren, das dann auch entfernst.

Was heißt "stören"? Solange das so steht werden aber Benutzer mit Sicherheit auch mehrere Anwendungen auf einer Seite einbauen und das funktioniert dann nicht. Deswegen weise ich in meinen Tutorials auch extra darauf hin.
Antworten

Wer ist online?

Mitglieder in diesem Forum: Tommy Herrmann, Volker und 4 Gäste