OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

ticket-form.js

21 KB
/* OpenNIT – Ticket-Formular JS (extern eingebunden, nicht inline) */
(function () {
    'use strict';

    /* ----------------------------------------------------------------
       Durchsuchbare Combo-Box
       ---------------------------------------------------------------- */
    function opennitInitCombo(wrapper) {
        var search   = wrapper.querySelector('.opennit-combo-search');
        var dropdown = wrapper.querySelector('.opennit-combo-dropdown');
        var hidden   = wrapper.querySelector('.opennit-combo-hidden');
        if (!search || !dropdown || !hidden) return;

        var allOpts = Array.from(dropdown.querySelectorAll('.opennit-combo-option'));
        var focused = null, selected = null;

        /* Dropdown-Pfeil als sichtbare Affordance (klickbar) – signalisiert,
           dass man das Feld auch ohne Suchbegriff aufklappen kann. */
        var chevron = document.createElement('span');
        chevron.className = 'opennit-combo-chevron';
        chevron.setAttribute('aria-hidden', 'true');
        chevron.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';
        wrapper.appendChild(chevron);

        /* ARIA: echte Combobox-/Listbox-Semantik für Screenreader & Tastatur */
        var comboUid = 'opennit-combo-' + Math.random().toString(36).slice(2, 8);
        dropdown.id = comboUid + '-listbox';
        dropdown.setAttribute('role', 'listbox');
        search.setAttribute('role', 'combobox');
        search.setAttribute('aria-haspopup', 'listbox');
        search.setAttribute('aria-expanded', 'false');
        search.setAttribute('aria-controls', dropdown.id);
        search.setAttribute('aria-autocomplete', 'list');
        allOpts.forEach(function (opt, i) {
            opt.id = dropdown.id + '-opt-' + i;
            opt.setAttribute('role', 'option');
            opt.setAttribute('aria-selected', 'false');
        });

        /* Portal: Dropdown an <body> hängen – kein overflow:hidden eines Vorfahren stört.
           position:absolute statt fixed, weil body{overflow-x:hidden} (Elementor) in Chrome
           dazu führt, dass fixed-Elemente relativ zu body positioniert werden. */
        document.body.appendChild(dropdown);

        Object.assign(dropdown.style, {
            position:      'absolute',
            zIndex:        '2147483647',
            background:    '#ffffff',
            border:        '1px solid #3b82f6',
            borderTop:     'none',
            borderRadius:  '0 0 6px 6px',
            maxHeight:     '210px',
            overflowY:     'auto',
            overflowX:     'hidden',
            boxShadow:     '0 4px 12px rgba(0,0,0,.15)',
            boxSizing:     'border-box',
            margin:        '0',
            padding:       '0',
            transform:     'none',
            opacity:       '1',
            visibility:    'visible',
            pointerEvents: 'auto',
            display:       'none',
            fontFamily:    '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif',
            fontSize:      '14px',
            lineHeight:    '1.5',
            color:         '#1f2937',
            right:         'auto',
            bottom:        'auto',
        });

        allOpts.forEach(function (opt) {
            Object.assign(opt.style, {
                padding:      '8px 12px',
                cursor:       'pointer',
                color:        '#1f2937',
                background:   'transparent',
                borderBottom: '1px solid #f3f4f6',
                boxSizing:    'border-box',
                fontSize:     '14px',
                lineHeight:   '1.5',
                margin:       '0',
                width:        '100%',
                textAlign:    'left',
                fontWeight:   'normal',
                whiteSpace:   'normal',
            });
            opt.style.setProperty('display', 'block', 'important');
        });

        function positionDropdown() {
            var r = search.getBoundingClientRect();
            dropdown.style.left  = (r.left  + window.pageXOffset) + 'px';
            dropdown.style.top   = (r.bottom + window.pageYOffset) + 'px';
            dropdown.style.width = r.width + 'px';
        }

        function show() {
            positionDropdown();
            dropdown.style.display    = 'block';
            dropdown.style.visibility = 'visible';
            dropdown.style.opacity    = '1';
            wrapper.classList.add('opennit-combo-open');
            search.setAttribute('aria-expanded', 'true');
        }
        function hide() {
            dropdown.style.display = 'none';
            focused = null;
            wrapper.classList.remove('opennit-combo-open');
            search.setAttribute('aria-expanded', 'false');
            search.removeAttribute('aria-activedescendant');
        }

        /* Klick auf den Pfeil öffnet/schließt die Liste (toggle) */
        chevron.addEventListener('mousedown', function (e) {
            e.preventDefault(); // verhindert Blur des Inputs
            if (dropdown.style.display === 'block') {
                hide();
            } else {
                search.focus();
                filter('');
                show();
            }
        });

        window.addEventListener('scroll', function () {
            if (dropdown.style.display === 'block') positionDropdown();
        }, true);
        window.addEventListener('resize', function () {
            if (dropdown.style.display === 'block') positionDropdown();
        });

        /* Tiefe einer Option aus dem "– "-Präfix ableiten (U+2013 + Leerzeichen = 2 JS-Zeichen) */
        function getOptDepth(opt) {
            var text = opt.textContent;
            var d = 0;
            while (text.charCodeAt(d * 2) === 0x2013) d++;
            return d;
        }

        function filter(q) {
            var lq = q.toLowerCase();

            if (!lq) {
                allOpts.forEach(function (o) { o.style.setProperty('display', 'block', 'important'); });
                return;
            }

            /* Matching-Optionen + ihre Elternknoten sichtbar machen */
            var visible = {};
            allOpts.forEach(function (o, i) {
                if (o.textContent.toLowerCase().indexOf(lq) === -1) return;
                visible[i] = true;
                /* Eltern aufsteigend suchen (DFS-Reihenfolge garantiert Eltern vor Kindern) */
                var targetDepth = getOptDepth(o) - 1;
                for (var j = i - 1; j >= 0 && targetDepth >= 0; j--) {
                    if (getOptDepth(allOpts[j]) === targetDepth) {
                        visible[j] = true;
                        targetDepth--;
                    }
                }
            });

            allOpts.forEach(function (o, i) {
                o.style.setProperty('display', visible[i] ? 'block' : 'none', 'important');
            });
        }

        function setOptState(opt, state) {
            if (state === 'hover') {
                opt.style.background = '#eff6ff'; opt.style.color = '#1d4ed8';
            } else if (state === 'selected') {
                opt.style.background = '#dbeafe'; opt.style.color = '#1d4ed8'; opt.style.fontWeight = '600';
            } else {
                opt.style.background = 'transparent'; opt.style.color = '#1f2937'; opt.style.fontWeight = 'normal';
            }
        }

        function selectOpt(opt) {
            allOpts.forEach(function (o) {
                o.classList.remove('opennit-combo-selected', 'opennit-combo-hover');
                o.setAttribute('aria-selected', 'false');
                setOptState(o, 'normal');
            });
            opt.classList.add('opennit-combo-selected');
            opt.setAttribute('aria-selected', 'true');
            setOptState(opt, 'selected');
            selected = opt;
            hidden.value = opt.getAttribute('data-value');
            search.value = opt.classList.contains('opennit-combo-option-none') ? '' : opt.textContent.trim();
            search.placeholder = opt.classList.contains('opennit-combo-option-none')
                ? search.getAttribute('data-placeholder')
                : opt.textContent.trim();
            filter('');
            hide();
        }

        function getVisible() {
            return allOpts.filter(function (o) { return o.style.getPropertyValue('display') !== 'none'; });
        }

        function moveFocus(dir) {
            var vis = getVisible();
            if (!vis.length) return;
            if (focused) {
                focused.classList.remove('opennit-combo-hover');
                setOptState(focused, focused.classList.contains('opennit-combo-selected') ? 'selected' : 'normal');
            }
            var idx = vis.indexOf(focused);
            if (dir === 1) idx = idx < vis.length - 1 ? idx + 1 : 0;
            else           idx = idx > 0 ? idx - 1 : vis.length - 1;
            focused = vis[idx];
            focused.classList.add('opennit-combo-hover');
            setOptState(focused, 'hover');
            focused.scrollIntoView({ block: 'nearest' });
            search.setAttribute('aria-activedescendant', focused.id);
        }

        search.setAttribute('data-placeholder', search.placeholder);

        search.addEventListener('focus', function () { filter(''); show(); });

        search.addEventListener('input', function () {
            hidden.value = '';
            selected = null;
            allOpts.forEach(function (o) { o.classList.remove('opennit-combo-selected'); setOptState(o, 'normal'); });
            filter(search.value);
            show();
        });

        search.addEventListener('keydown', function (e) {
            if      (e.key === 'ArrowDown') { e.preventDefault(); moveFocus(1);  show(); }
            else if (e.key === 'ArrowUp')   { e.preventDefault(); moveFocus(-1); show(); }
            else if (e.key === 'Enter') {
                e.preventDefault();
                var target = focused || getVisible()[0];
                if (target) selectOpt(target);
            } else if (e.key === 'Escape') {
                search.value = (selected && !selected.classList.contains('opennit-combo-option-none'))
                    ? selected.textContent.trim() : '';
                hide();
            }
        });

        dropdown.addEventListener('mousedown', function (e) { e.preventDefault(); });

        allOpts.forEach(function (opt) {
            opt.addEventListener('click', function () { selectOpt(opt); });
            opt.addEventListener('mouseenter', function () {
                if (focused) {
                    focused.classList.remove('opennit-combo-hover');
                    setOptState(focused, focused.classList.contains('opennit-combo-selected') ? 'selected' : 'normal');
                }
                focused = opt;
                opt.classList.add('opennit-combo-hover');
                setOptState(opt, 'hover');
            });
        });

        search.addEventListener('blur', function () {
            setTimeout(function () {
                if (!hidden.value && selected && selected.classList.contains('opennit-combo-option-none')) {
                    search.value = '';
                } else if (!hidden.value && !selected) {
                    search.value = '';
                } else if (selected && !selected.classList.contains('opennit-combo-option-none')) {
                    search.value = selected.textContent.trim();
                }
                hide();
            }, 200);
        });

        wrapper._comboSelectByValue = function (val) {
            var opt = allOpts.filter(function (o) { return o.getAttribute('data-value') === String(val); })[0];
            if (opt) selectOpt(opt);
        };
    }

    /* ----------------------------------------------------------------
       Initialisierung
       ---------------------------------------------------------------- */
    function initTicketForm() {
        var wrap = document.getElementById('opennit-ticket-wrap');
        if (!wrap || wrap._opennitInit) return;
        wrap._opennitInit = true;

        /* Combos initialisieren */
        wrap.querySelectorAll('.opennit-combo').forEach(opennitInitCombo);

        /* Asset per URL-Parameter vorauswählen */
        var preselectId = wrap.getAttribute('data-preselect-asset') || '';
        if (preselectId) {
            var assetCombo = document.getElementById('opennit-asset-combo');
            if (assetCombo && assetCombo._comboSelectByValue) {
                assetCombo._comboSelectByValue(preselectId);
            }
        }

        /* ----------------------------------------------------------------
           Formular-Logik
           ---------------------------------------------------------------- */
        var form         = document.getElementById('opennit-ticket-form');
        var successPanel = document.getElementById('opennit-ticket-success-panel');
        var successMsg   = document.getElementById('opennit-ticket-success-msg');
        var newBtn       = document.getElementById('opennit-ticket-new-btn');
        var errorEl      = document.getElementById('opennit-ticket-error');
        var submit       = document.getElementById('opennit-ticket-submit');
        var fileInput    = document.getElementById('opennit-attachment');
        var preview      = document.getElementById('opennit-attachment-preview');

        if (!form) return;

        var ajaxUrl     = wrap.getAttribute('data-ajax-url')     || '';
        var successText = wrap.getAttribute('data-success-text') || 'Ticket erfolgreich übermittelt.';
        var submitLabel = wrap.getAttribute('data-submit-label') || 'Ticket absenden';

        if (newBtn) {
            newBtn.addEventListener('click', function () {
                successPanel.style.display = 'none';
                form.style.display = '';
                errorEl.style.display = 'none';
            });
        }

        if (fileInput) {
            var dropzone = wrap.querySelector('.opennit-dropzone');
            var dzText   = dropzone ? dropzone.querySelector('.opennit-dropzone-text') : null;
            var dzDefault = dzText ? dzText.innerHTML : '';
            fileInput.addEventListener('change', function () {
                if (!fileInput.files.length) {
                    preview.style.display = 'none'; preview.textContent = '';
                    if (dropzone) dropzone.classList.remove('is-filled');
                    if (dzText) dzText.innerHTML = dzDefault;
                    return;
                }
                var f = fileInput.files[0];
                if (f.size > 10 * 1024 * 1024) {
                    errorEl.textContent = 'Die Datei ist zu groß (max. 10 MB).';
                    errorEl.style.display = 'block';
                    fileInput.value = '';
                    preview.style.display = 'none';
                    if (dropzone) dropzone.classList.remove('is-filled');
                    if (dzText) dzText.innerHTML = dzDefault;
                    return;
                }
                preview.textContent = '📎 ' + f.name + ' (' + (f.size / 1024).toFixed(1) + ' KB)';
                preview.style.display = 'block';
                if (dropzone) dropzone.classList.add('is-filled');
                if (dzText) dzText.innerHTML = '<strong>Datei ausgewählt</strong> – zum Ändern klicken';
            });
        }

        form.addEventListener('submit', function (e) {
            e.preventDefault();
            successPanel.style.display = 'none';
            errorEl.style.display      = 'none';

            var categoryId  = form.querySelector('[name="category_id"]').value;
            var locationId  = form.querySelector('[name="location_id"]').value;
            var subject     = form.querySelector('[name="subject"]').value.trim();
            var description = form.querySelector('[name="description"]').value.trim();

            if (!categoryId || !locationId || !subject || !description) {
                errorEl.textContent   = 'Bitte alle Pflichtfelder ausfüllen.';
                errorEl.style.display = 'block';
                return;
            }

            var file = fileInput && fileInput.files.length ? fileInput.files[0] : null;

            function doSubmit(attachmentB64, attachmentName, attachmentMime) {
                var data = new FormData();
                data.append('action',      'opennit_submit_ticket');
                data.append('nonce',       form.querySelector('[name="nonce"]').value);
                data.append('from_email',  form.querySelector('[name="from_email"]').value);
                data.append('from_name',   form.querySelector('[name="from_name"]').value);
                data.append('category_id', categoryId);
                data.append('location_id', locationId);
                data.append('subject',     subject);
                data.append('description', description);
                var assetEl = form.querySelector('[name="asset_id"]');
                if (assetEl && assetEl.value) data.append('asset_id', assetEl.value);
                if (attachmentB64) {
                    data.append('attachment_b64',  attachmentB64);
                    data.append('attachment_name', attachmentName);
                    data.append('attachment_mime', attachmentMime);
                }

                fetch(ajaxUrl, { method: 'POST', body: data })
                    .then(function (r) { return r.json(); })
                    .then(function (res) {
                        if (res.success) {
                            form.style.display = 'none';
                            errorEl.style.display = 'none';
                            successMsg.textContent = (res.data && res.data.message) ? res.data.message : successText;
                            successPanel.style.display = '';
                            form.reset();
                            if (preview) { preview.style.display = 'none'; preview.textContent = ''; }
                            var dz = wrap.querySelector('.opennit-dropzone');
                            if (dz) {
                                dz.classList.remove('is-filled');
                                var dzt = dz.querySelector('.opennit-dropzone-text');
                                if (dzt) dzt.innerHTML = '<strong>Datei auswählen</strong>';
                            }
                            wrap.querySelectorAll('.opennit-combo').forEach(function (combo) {
                                var s = combo.querySelector('.opennit-combo-search');
                                var h = combo.querySelector('.opennit-combo-hidden');
                                if (s) s.value = '';
                                if (h) h.value = '';
                                combo.querySelectorAll('.opennit-combo-option').forEach(function (o) {
                                    o.classList.remove('opennit-combo-selected', 'opennit-combo-hover');
                                });
                            });
                        } else {
                            var msg = (res.data && res.data.message) ? res.data.message : (res.data || 'Unbekannter Fehler.');
                            errorEl.textContent   = msg;
                            errorEl.style.display = 'block';
                        }
                    })
                    .catch(function () {
                        errorEl.textContent   = 'Verbindungsfehler. Bitte versuchen Sie es erneut.';
                        errorEl.style.display = 'block';
                    })
                    .finally(function () {
                        submit.disabled    = false;
                        submit.textContent = submitLabel;
                    });
            }

            submit.disabled    = true;
            submit.textContent = 'Wird gesendet…';

            if (file) {
                var reader = new FileReader();
                reader.onload  = function (ev) { doSubmit(ev.target.result, file.name, file.type); };
                reader.onerror = function () {
                    errorEl.textContent   = 'Fehler beim Lesen der Datei.';
                    errorEl.style.display = 'block';
                    submit.disabled    = false;
                    submit.textContent = submitLabel;
                };
                reader.readAsDataURL(file);
            } else {
                doSubmit('', '', '');
            }
        });
    }

    /* Initialisierung: sofort, bei DOMContentLoaded und nach kurzer Verzögerung
       (fängt Elementor-Widgets ab, die nach dem initialen Render geladen werden) */
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initTicketForm);
    } else {
        initTicketForm();
    }
    window.addEventListener('load', initTicketForm);
    setTimeout(initTicketForm, 800);

})();