PageSourceSearch

https://inno-talk.de/wp-content/plugins/inno-plugin/newsletter/newsletter.js?ver=1.0.0

js inno-talk.de collected 2026-10-04 13:30:36 UTC 27,554 bytes, 612 lines download raw bytes

1(function () {
2    'use strict';
3
4    // Wird per wp_localize_script von PHP gesetzt
5    var cfg = window.innoNewsletter || {};
6    var strings = cfg.strings || {};
7    var endpoint = cfg.endpoint || '/api/newsletter/';
8    var loggedIn = !!cfg.loggedIn;
9
10    var TRIGGER_SELECTOR = 'a[href^="#inno-newsletter:"]';
11    var modalEl = null;
12    var lastFocus = null;
13    var pendingKey = null;
14    var currentStep = 1; // 1 = nur E-Mail (Prüfung), 2 = Name (Registrierung)
15
16    function t(key, fallback) {
17        return strings[key] || fallback;
18    }
19
20    function parseKey(href) {
21        var m = /^#inno-newsletter:([a-z_]+)$/.exec(href || '');
22        return m ? m[1] : null;
23    }
24
25    function toast(message, isError) {
26        var el = document.createElement('div');
27        el.className = 'inno-newsletter-toast' + (isError ? ' is-error' : '');
28        el.textContent = message;
29        document.body.appendChild(el);
30        // force reflow to enable transition
31        // eslint-disable-next-line no-unused-expressions
32        el.offsetHeight;
33        el.classList.add('is-visible');
34        setTimeout(function () {
35            el.classList.remove('is-visible');
36            setTimeout(function () { el.remove(); }, 300);
37        }, 4200);
38    }
39
40    function postForm(method, data) {
41        var body = new URLSearchParams();
42        Object.keys(data).forEach(function (k) {
43            if (data[k] !== undefined && data[k] !== null) {
44                body.append(k, data[k]);
45            }
46        });
47        return fetch(endpoint + method, {
48            method: 'POST',
49            credentials: 'include',
50            headers: {
51                'Accept': 'application/json',
52                'Content-Type': 'application/x-www-form-urlencoded',
53                'Pragma': 'no-cache',
54                'Cache-Control': 'no-cache'
55            },
56            body: body.toString()
57        }).then(function (res) {
58            return res.json().then(function (json) { return { ok: res.ok, status: res.status, json: json }; });
59        });
60    }
61
62    function subscribeLoggedIn(key) {
63        var trigger = document.querySelector(TRIGGER_SELECTOR + '[data-current="1"]');
64        if (trigger) trigger.setAttribute('aria-busy', 'true');
65        postForm('subscribe', { key: key }).then(function (r) {
66            if (r.ok && r.json && r.json.success) {
67                openSuccessModal(
68                    t('success_title', 'Vielen Dank!'),
69                    t('subscribed_success', 'Du bist jetzt für den Newsletter angemeldet.')
70                );
71            } else {
72                toast(t('subscribed_error', 'Anmeldung fehlgeschlagen. Bitte später erneut versuchen.'), true);
73            }
74        }).catch(function () {
75            toast(t('network_error', 'Netzwerkfehler. Bitte später erneut versuchen.'), true);
76        }).finally(function () {
77            if (trigger) trigger.removeAttribute('aria-busy');
78        });
79    }
80
81    function openSuccessModal(title, text) {
82        var m = buildModal();
83        var form = m.querySelector('.inno-newsletter-form');
84        var success = m.querySelector('.inno-newsletter-success');
85        var lead = m.querySelector('.inno-newsletter-lead');
86        var titleEl = m.querySelector('#inno-newsletter-title');
87        form.hidden = true;
88        if (lead) lead.hidden = true;
89        if (titleEl) titleEl.hidden = true;
90        success.hidden = false;
91        success.querySelector('h4').textContent = title;
92        success.querySelector('p').textContent = text;
93        m.classList.add('is-open', 'is-success-only');
94        document.body.classList.add('inno-newsletter-locked');
95        lastFocus = document.activeElement;
96        var closeBtn = success.querySelector('.inno-newsletter-close-success');
97        if (closeBtn) closeBtn.focus();
98    }
99
100    function buildModal() {
101        if (modalEl) return modalEl;
102
103        var overlay = document.createElement('div');
104        overlay.className = 'inno-newsletter-overlay';
105        overlay.setAttribute('role', 'dialog');
106        overlay.setAttribute('aria-modal', 'true');
107        overlay.setAttribute('aria-labelledby', 'inno-newsletter-title');
108        overlay.innerHTML = ''
109            + '<div class="inno-newsletter-modal">'
110            + '  <button type="button" class="inno-newsletter-close" aria-label="' + t('close', 'Schließen') + '">&times;</button>'
111            + '  <h3 id="inno-newsletter-title">' + t('modal_title', 'Newsletter abonnieren') + '</h3>'
112            + '  <p class="inno-newsletter-lead">' + t('modal_lead', 'Trage deine E-Mail-Adresse ein, um den Newsletter zu erhalten.') + '</p>'
113            + '  <form class="inno-newsletter-form" novalidate>'
114            + '    <div class="inno-newsletter-step inno-newsletter-step1">'
115            + '      <div class="inno-newsletter-row">'
116            + '        <label>'
117            + '          <span>' + t('field_email', 'E-Mail') + ' *</span>'
118            + '          <input name="email" type="email" required autocomplete="email" />'
119            + '        </label>'
120            + '      </div>'
121            + '    </div>'
122            + '    <div class="inno-newsletter-step inno-newsletter-step2" hidden>'
123            + '      <p class="inno-newsletter-step2-lead"></p>'
124            + '      <div class="inno-newsletter-row">'
125            + '        <label>'
126            + '          <span>' + t('field_gender', 'Anrede') + '</span>'
127            + '          <select name="gender">'
128            + '            <option value="">—</option>'
129            + '            <option value="w">' + t('gender_w', 'Frau') + '</option>'
130            + '            <option value="m">' + t('gender_m', 'Herr') + '</option>'
131            + '            <option value="u">' + t('gender_u', 'Divers') + '</option>'
132            + '          </select>'
133            + '        </label>'
134            + '      </div>'
135            + '      <div class="inno-newsletter-row inno-newsletter-row--split">'
136            + '        <label>'
137            + '          <span>' + t('field_firstname', 'Vorname') + ' *</span>'
138            + '          <input name="firstname" type="text" autocomplete="given-name" />'
139            + '        </label>'
140            + '        <label>'
141            + '          <span>' + t('field_lastname', 'Nachname') + ' *</span>'
142            + '          <input name="lastname" type="text" autocomplete="family-name" />'
143            + '        </label>'
144            + '      </div>'
145            + '    </div>'
146            + '    <div class="inno-newsletter-error" aria-live="polite" hidden></div>'
147            + '    <button type="submit" class="inno-newsletter-submit">' + t('next', 'Weiter') + '</button>'
148            + '    <p class="inno-newsletter-hint">' + t('login_hint', 'Bereits Kunde? Logge dich oben rechts ein, dann genügt ein Klick.') + '</p>'
149            + '  </form>'
150            + '  <div class="inno-newsletter-success" hidden>'
151            + '    <h4>' + t('success_title', 'Vielen Dank!') + '</h4>'
152            + '    <p>' + t('success_text', 'Du hast den Newsletter erfolgreich abonniert.') + '</p>'
153            + '    <button type="button" class="inno-newsletter-submit inno-newsletter-close-success">' + t('success_close', 'Schließen') + '</button>'
154            + '  </div>'
155            + '</div>';
156
157        document.body.appendChild(overlay);
158        modalEl = overlay;
159
160        overlay.addEventListener('click', function (e) {
161            if (e.target === overlay) closeModal();
162        });
163        overlay.querySelector('.inno-newsletter-close').addEventListener('click', closeModal);
164        overlay.querySelector('.inno-newsletter-close-success').addEventListener('click', closeModal);
165        overlay.querySelector('.inno-newsletter-form').addEventListener('submit', onSubmit);
166
167        document.addEventListener('keydown', function (e) {
168            if (e.key === 'Escape' && modalEl && modalEl.classList.contains('is-open')) closeModal();
169        });
170
171        return overlay;
172    }
173
174    function openModal(key) {
175        pendingKey = key;
176        var m = buildModal();
177        m.classList.add('is-open');
178        document.body.classList.add('inno-newsletter-locked');
179        lastFocus = document.activeElement;
180        resetForm();
181        var firstInput = m.querySelector('input[name="email"]');
182        if (firstInput) firstInput.focus();
183    }
184
185    function closeModal() {
186        if (!modalEl) return;
187        modalEl.classList.remove('is-open', 'is-success-only');
188        document.body.classList.remove('inno-newsletter-locked');
189        pendingKey = null;
190        if (lastFocus && typeof lastFocus.focus === 'function') lastFocus.focus();
191    }
192
193    function resetForm() {
194        if (!modalEl) return;
195        var form = modalEl.querySelector('.inno-newsletter-form');
196        var success = modalEl.querySelector('.inno-newsletter-success');
197        var err = modalEl.querySelector('.inno-newsletter-error');
198        var lead = modalEl.querySelector('.inno-newsletter-lead');
199        var titleEl = modalEl.querySelector('#inno-newsletter-title');
200        form.hidden = false;
201        success.hidden = true;
202        err.hidden = true;
203        err.textContent = '';
204        if (lead) lead.hidden = false;
205        if (titleEl) titleEl.hidden = false;
206        form.reset();
207        currentStep = 1;
208        modalEl.querySelector('.inno-newsletter-step1').hidden = false;
209        modalEl.querySelector('.inno-newsletter-step2').hidden = true;
210        var submit = form.querySelector('.inno-newsletter-submit');
211        submit.disabled = false;
212        submit.textContent = t('next', 'Weiter');
213    }
214
215    function onSubmit(e) {
216        e.preventDefault();
217        var form = e.currentTarget;
218        var err = modalEl.querySelector('.inno-newsletter-error');
219        err.hidden = true;
220        if (currentStep === 1) {
221            stepCheck(form, err);
222        } else {
223            stepRegister(form, err);
224        }
225    }
226
227    // Schritt 1: nur E-Mail prüfen. Existiert ein Account → Login-Maske,
228    // sonst Schritt 2 (Name) einblenden.
229    function stepCheck(form, err) {
230        var email = (new FormData(form).get('email') || '').toString().trim();
231        if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
232            err.textContent = t('error_invalid', 'Bitte gib eine gültige E-Mail-Adresse ein.');
233            err.hidden = false;
234            return;
235        }
236        var submit = form.querySelector('.inno-newsletter-submit');
237        submit.disabled = true;
238        submit.textContent = t('checking', 'Wird geprüft…');
239
240        postForm('check', { email: email }).then(function (r) {
241            if (r.ok && r.json && r.json.success) {
242                if (r.json.exists) {
243                    // Bestehender Account → zur Login-Maske / den Einstellungen.
244                    err.textContent = t('error_email_exists', 'Diese E-Mail ist bereits bei uns registriert. Wir leiten dich zu deinen Newsletter-Einstellungen weiter…');
245                    err.hidden = false;
246                    if (r.json.manage_url) {
247                        setTimeout(function () { window.location.href = r.json.manage_url; }, 1800);
248                        return;
249                    }
250                } else {
251                    // Neuer Kontakt → Name abfragen.
252                    showStep2(form, email);
253                    return;
254                }
255            } else {
256                err.textContent = t('error_generic', 'Anmeldung fehlgeschlagen. Bitte später erneut versuchen.');
257                err.hidden = false;
258            }
259            submit.disabled = false;
260            submit.textContent = t('next', 'Weiter');
261        }).catch(function () {
262            err.textContent = t('network_error', 'Netzwerkfehler. Bitte später erneut versuchen.');
263            err.hidden = false;
264            submit.disabled = false;
265            submit.textContent = t('next', 'Weiter');
266        });
267    }
268
269    function showStep2(form, email) {
270        currentStep = 2;
271        modalEl.querySelector('.inno-newsletter-step1').hidden = true;
272        var step2 = modalEl.querySelector('.inno-newsletter-step2');
273        var lead = step2.querySelector('.inno-newsletter-step2-lead');
274        if (lead) lead.textContent = t('step2_lead_prefix', 'Schön, dass du dabei bist! Für ') + email + t('step2_lead_suffix', ' brauchen wir noch deinen Namen.');
275        step2.hidden = false;
276        var submit = form.querySelector('.inno-newsletter-submit');
277        submit.disabled = false;
278        submit.textContent = t('submit', 'Newsletter abonnieren');
279        var fn = form.querySelector('input[name="firstname"]');
280        if (fn) fn.focus();
281    }
282
283    // Schritt 2: Name + Registrierung (Double-Opt-In).
284    function stepRegister(form, err) {
285        var fd = new FormData(form);
286        var firstname = (fd.get('firstname') || '').toString().trim();
287        var lastname  = (fd.get('lastname')  || '').toString().trim();
288        var email     = (fd.get('email')     || '').toString().trim();
289        var gender    = (fd.get('gender')    || '').toString().trim();
290
291        if (firstname.length === 0 || lastname.length === 0) {
292            err.textContent = t('error_invalid_names', 'Bitte fülle Vorname und Nachname aus.');
293            err.hidden = false;
294            return;
295        }
296
297        var submit = form.querySelector('.inno-newsletter-submit');
298        submit.disabled = true;
299        submit.textContent = t('submitting', 'Wird gesendet…');
300
301        postForm('register', {
302            firstname: firstname,
303            lastname: lastname,
304            email: email,
305            gender: gender,
306            key: pendingKey || ''
307        }).then(function (r) {
308            if (r.ok && r.json && r.json.success) {
309                // Double-Opt-In: Kontakt angelegt, Bestätigungsmail ist unterwegs.
310                var success = modalEl.querySelector('.inno-newsletter-success');
311                var h4 = success.querySelector('h4');
312                var p = success.querySelector('p');
313                if (h4) h4.textContent = t('optin_title', 'Fast geschafft!');
314                if (p) p.textContent = t('optin_text', 'Wir haben dir eine E-Mail geschickt. Bitte bestätige darin deine Anmeldung, dann erhältst du den Newsletter.');
315                form.hidden = true;
316                success.hidden = false;
317                return;
318            }
319            // Race: zwischen Schritt 1 und 2 doch schon registriert.
320            if (r.json && r.json.error === 'email_exists') {
321                err.textContent = t('error_email_exists', 'Diese E-Mail ist bereits bei uns registriert. Wir leiten dich zu deinen Newsletter-Einstellungen weiter…');
322                err.hidden = false;
323                if (r.json.manage_url) {
324                    setTimeout(function () { window.location.href = r.json.manage_url; }, 1800);
325                    return;
326                }
327            }
328            var msg = t('error_generic', 'Anmeldung fehlgeschlagen. Bitte später erneut versuchen.');
329            if (r.json && r.json.error === 'invalid_input') msg = t('error_invalid_names', 'Bitte fülle Vorname und Nachname aus.');
330            err.textContent = msg;
331            err.hidden = false;
332            submit.disabled = false;
333            submit.textContent = t('submit', 'Newsletter abonnieren');
334        }).catch(function () {
335            err.textContent = t('network_error', 'Netzwerkfehler. Bitte später erneut versuchen.');
336            err.hidden = false;
337            submit.disabled = false;
338            submit.textContent = t('submit', 'Newsletter abonnieren');
339        });
340    }
341
342    function onTriggerClick(e) {
343        var a = e.currentTarget;
344        var key = parseKey(a.getAttribute('href'));
345        if (!key) return;
346        e.preventDefault();
347        if (loggedIn) {
348            a.setAttribute('data-current', '1');
349            subscribeLoggedIn(key);
350            setTimeout(function () { a.removeAttribute('data-current'); }, 50);
351        } else {
352            openModal(key);
353        }
354    }
355
356    function attach() {
357        var links = document.querySelectorAll(TRIGGER_SELECTOR);
358        links.forEach(function (a) {
359            if (a.dataset.innoNewsletterBound) return;
360            a.dataset.innoNewsletterBound = '1';
361            a.addEventListener('click', onTriggerClick);
362        });
363    }
364
365    // -- Settings-Page (#inno-newsletter-settings) -----------------------------
366    function initSettingsPage() {
367        var root = document.getElementById('inno-newsletter-settings');
368        if (!root) return;
369
370        var email = root.getAttribute('data-email');
371        var guid  = root.getAttribute('data-guid');
372        var type  = root.getAttribute('data-type'); // optional: aus Mail-Link
373        var newsletterId = root.getAttribute('data-newsletter-id'); // optional: aus Mail-Link
374        var newsletters = {};
375        try { newsletters = JSON.parse(root.getAttribute('data-newsletters') || '{}'); } catch (e) {}
376        var onLabel = root.getAttribute('data-on') || 'Subscribed';
377        var offLabel = root.getAttribute('data-off') || 'Not subscribed';
378        // labels = key -> Anzeigename (für Bestätigungs-/Abmelde-Texte).
379        var labels = {};
380        Object.keys(newsletters).forEach(function (k) { labels[k] = newsletters[k].name; });
381
382        var statusEl = root.querySelector('.inno-ns-status');
383        var loadingEl = root.querySelector('.inno-ns-loading');
384        var form = root.querySelector('.inno-ns-form');
385        var boxes = root.querySelector('.inno-ns-cards');
386        var errEl = root.querySelector('.inno-ns-error');
387        var savedEl = root.querySelector('.inno-ns-saved');
388        var confirmEl = root.querySelector('.inno-ns-confirm');
389        var confirmTextEl = root.querySelector('.inno-ns-confirm-text');
390        var confirmYesBtn = root.querySelector('.inno-ns-confirm-yes');
391        var confirmNoBtn = root.querySelector('.inno-ns-confirm-no');
392
393        function showStatus(message, isError) {
394            statusEl.textContent = message;
395            statusEl.classList.toggle('is-error', !!isError);
396            statusEl.hidden = false;
397        }
398
399        function renderCheckboxes(flags) {
400            boxes.innerHTML = '';
401            Object.keys(newsletters).forEach(function (key) {
402                var n = newsletters[key] || {};
403                var id = 'inno-ns-' + key;
404                var checked = !!(flags && flags[key]);
405
406                var card = document.createElement('label');
407                card.className = 'inno-ns-card' + (checked ? ' is-active' : '');
408                card.setAttribute('for', id);
409
410                // Logo bzw. Text-Badge als Fallback
411                var media = document.createElement('div');
412                media.className = 'inno-ns-card-media';
413                if (n.logo) {
414                    var img = document.createElement('img');
415                    img.src = n.logo;
416                    img.alt = n.name || key;
417                    img.loading = 'lazy';
418                    media.appendChild(img);
419                } else {
420                    var badge = document.createElement('span');
421                    badge.className = 'inno-ns-card-badge';
422                    badge.textContent = n.name || key;
423                    media.appendChild(badge);
424                }
425                card.appendChild(media);
426
427                // Textblock
428                var body = document.createElement('div');
429                body.className = 'inno-ns-card-body';
430                var nameEl = document.createElement('span');
431                nameEl.className = 'inno-ns-card-name';
432                nameEl.textContent = n.name || key;
433                body.appendChild(nameEl);
434                if (n.tagline) {
435                    var tag = document.createElement('strong');
436                    tag.className = 'inno-ns-card-tagline';
437                    tag.textContent = n.tagline;
438                    body.appendChild(tag);
439                }
440                if (n.description) {
441                    var desc = document.createElement('p');
442                    desc.className = 'inno-ns-card-desc';
443                    desc.textContent = n.description;
444                    body.appendChild(desc);
445                }
446                card.appendChild(body);
447
448                // Toggle (Checkbox + Switch + Status-Text)
449                var toggle = document.createElement('div');
450                toggle.className = 'inno-ns-card-toggle';
451                var input = document.createElement('input');
452                input.type = 'checkbox';
453                input.id = id;
454                input.name = key;
455                input.checked = checked;
456                var sw = document.createElement('span');
457                sw.className = 'inno-ns-switch';
458                sw.setAttribute('aria-hidden', 'true');
459                var state = document.createElement('span');
460                state.className = 'inno-ns-state';
461                state.textContent = checked ? onLabel : offLabel;
462                input.addEventListener('change', function () {
463                    card.classList.toggle('is-active', input.checked);
464                    state.textContent = input.checked ? onLabel : offLabel;
465                });
466                toggle.appendChild(input);
467                toggle.appendChild(sw);
468                toggle.appendChild(state);
469                card.appendChild(toggle);
470
471                boxes.appendChild(card);
472            });
473        }
474
475        function loadStatus() {
476            var url = endpoint + 'status?guid=' + encodeURIComponent(guid) + '&email=' + encodeURIComponent(email);
477            return fetch(url, {
478                credentials: 'include',
479                headers: { 'Accept': 'application/json', 'Pragma': 'no-cache', 'Cache-Control': 'no-cache' }
480            }).then(function (res) {
481                return res.json().then(function (json) { return { ok: res.ok, json: json }; });
482            });
483        }
484
485        function showForm(flags) {
486            loadingEl.hidden = true;
487            renderCheckboxes(flags || {});
488            form.hidden = false;
489        }
490
491        // Meldet eine erfolgte Abmeldung zusätzlich ans newsletter-Tool, damit sie in
492        // dessen "Abmeldungen"-Statistik auftaucht. Notwendig, weil Elastic Email für
493        // disable_unsubscribe-Accounts (u.a. congress) diesen Klick nie sieht - ohne
494        // diesen Beacon bliebe die Statistik dort strukturell bei 0 (siehe
495        // newsletter/api/webhook/crm_unsubscribe.php). Rein informativ für die
496        // Statistik: die eigentliche Abmeldung ist mit dem obigen updateMailing-Call
497        // bereits vollzogen, ein Fehlschlag hier wird bewusst ignoriert.
498        function notifyNewsletterToolUnsubscribed() {
499            if (!newsletterId) return;
500            var body = new URLSearchParams();
501            body.append('guid', guid);
502            body.append('email', email);
503            body.append('newsletter_id', newsletterId);
504            fetch('https://innoform-coaching.de/newsletter/api/webhook/crm_unsubscribe.php', {
505                method: 'POST',
506                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
507                body: body.toString()
508            }).catch(function () {});
509        }
510
511        function doUnsubscribe(typeLabel) {
512            confirmEl.hidden = true;
513            loadingEl.hidden = false;
514            postForm('unsubscribe', { guid: guid, email: email, newsletter_type: type }).then(function (r) {
515                if (r.ok && r.json && r.json.success) {
516                    notifyNewsletterToolUnsubscribed();
517                    showStatus(t('unsub_success_prefix', 'Sie haben sich erfolgreich vom ') + typeLabel + t('unsub_success_suffix', ' abgemeldet.'));
518                    if (r.json.flags) {
519                        showForm(r.json.flags);
520                    } else {
521                        loadStatusIntoForm();
522                    }
523                } else {
524                    showStatus(t('unsub_failed', 'Abmeldung fehlgeschlagen. Bitte prüfe deine Einstellungen unten.'), true);
525                    loadStatusIntoForm();
526                }
527            }).catch(function () {
528                showStatus(t('network_error', 'Netzwerkfehler. Bitte später erneut versuchen.'), true);
529                loadStatusIntoForm();
530            });
531        }
532
533        function start() {
534            var typeLabel = labels[type] || type;
535
536            // Mit newsletter_type aus dem Mail-Link: erst auf der Seite bestätigen
537            // lassen (kein Auto-Abmelden – Mail-Scanner folgen Links sonst ungewollt).
538            if (type) {
539                loadingEl.hidden = true;
540                confirmTextEl.textContent = t('confirm_prefix', 'Möchten Sie sich wirklich vom ') + typeLabel + t('confirm_suffix', ' abmelden?');
541                confirmEl.hidden = false;
542                confirmYesBtn.addEventListener('click', function () { doUnsubscribe(typeLabel); });
543                confirmNoBtn.addEventListener('click', function () {
544                    confirmEl.hidden = true;
545                    loadingEl.hidden = false;
546                    loadStatusIntoForm();
547                });
548                return;
549            }
550
551            loadStatusIntoForm();
552        }
553
554        function loadStatusIntoForm() {
555            loadStatus().then(function (r) {
556                if (r.ok && r.json && r.json.success) {
557                    showForm(r.json.flags);
558                } else {
559                    loadingEl.hidden = true;
560                    showStatus(t('status_load_failed', 'Aktuelle Einstellungen konnten nicht geladen werden.'), true);
561                }
562            }).catch(function () {
563                loadingEl.hidden = true;
564                showStatus(t('network_error', 'Netzwerkfehler. Bitte später erneut versuchen.'), true);
565            });
566        }
567
568        form.addEventListener('submit', function (e) {
569            e.preventDefault();
570            errEl.hidden = true;
571            savedEl.hidden = true;
572            var data = { guid: guid, email: email };
573            Object.keys(labels).forEach(function (key) {
574                var input = form.querySelector('input[name="' + key + '"]');
575                data[key] = input && input.checked ? 1 : 0;
576            });
577            var btn = form.querySelector('.inno-ns-save');
578            btn.disabled = true;
579            postForm('save', data).then(function (r) {
580                btn.disabled = false;
581                if (r.ok && r.json && r.json.success) {
582                    savedEl.hidden = false;
583                } else {
584                    errEl.textContent = t('save_failed', 'Speichern fehlgeschlagen. Bitte später erneut versuchen.');
585                    errEl.hidden = false;
586                }
587            }).catch(function () {
588                btn.disabled = false;
589                errEl.textContent = t('network_error', 'Netzwerkfehler. Bitte später erneut versuchen.');
590                errEl.hidden = false;
591            });
592        });
593
594        start();
595    }
596
597    function onReady() {
598        attach();
599        initSettingsPage();
600    }
601
602    if (document.readyState === 'loading') {
603        document.addEventListener('DOMContentLoaded', onReady);
604    } else {
605        onReady();
606    }
607
608    // Falls Buttons später dynamisch nachgeladen werden, kann der Aufrufer
609    // window.innoNewsletter.refresh() rufen, um sie zu binden.
610    window.innoNewsletter = window.innoNewsletter || {};
611    window.innoNewsletter.refresh = attach;
612})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.