PageSourceSearch

https://www.hopehealthaction.org/js/component/slideoutForm.js?v=5

js hopehealthaction.org collected 2026-09-28 10:18:03 UTC 24,391 bytes, 515 lines download raw bytes

1/* ------------------------------------------------------------------
2 * slideoutForm — Alpine component for #slideout-panel-root (form preset).
3 * Shared decorate/validate/tooltip behaviour for editable side panels.
4 * ------------------------------------------------------------------ */
5(function () {
6    'use strict';
7
8    function callHost(methodName, args) {
9        var store = Alpine.store('slideout');
10        if (!store || !store.hostId || !methodName) return;
11        var el = document.getElementById(store.hostId);
12        if (!el || !Alpine.$data) return;
13        var host = Alpine.$data(el);
14        if (host && typeof host[methodName] === 'function') {
15            host[methodName].apply(host, args || []);
16        }
17    }
18
19    function isObjectMapListPanel(store) {
20        return !!(store && store.panelClass && store.panelClass.indexOf('-object-map-list') !== -1);
21    }
22
23    function slideoutForm() {
24        return {
25            _iframeBound: false,
26
27            init: function () {
28                var self = this;
29                this.bindIframeSave();
30                document.body.addEventListener('slideout-saved', function () {
31                    var store = Alpine.store('slideout');
32                    if (!store || !store.isOpen) return;
33                    store.dirty = false;
34                    self.finishSaveAttempt();
35                    callHost(store.onSaved);
36                });
37                document.body.addEventListener('htmx:afterSwap', function (ev) {
38                    var tid = ev.detail && ev.detail.target && ev.detail.target.id;
39                    if (tid === 'slideout-htmx-body' || tid === 'slideout-content') {
40                        self.onContentLoaded(ev.detail.target);
41                    }
42                });
43                document.addEventListener('keydown', function (ev) {
44                    if (ev.key !== 'Escape') return;
45                    var modals = Alpine.store('modals');
46                    if (modals && modals.isOpen && modals.isOpen()) return;
47                    var store = Alpine.store('slideout');
48                    if (!store || !store.isOpen) return;
49                    ev.preventDefault();
50                    if (isObjectMapListPanel(store)) return;
51                    if (store.type === 'form') store.requestClose();
52                    else {
53                        callHost(store.onClose);
54                        store.close();
55                    }
56                });
57            },
58
59            handleOverlayClick: function () {
60                var store = Alpine.store('slideout');
61                if (!store) return;
62                if (isObjectMapListPanel(store)) return;
63                if (store.type === 'form' && store.dirtyGuard) store.requestClose();
64                else {
65                    callHost(store.onClose);
66                    store.close();
67                }
68            },
69
70            handleCloseClick: function () {
71                var store = Alpine.store('slideout');
72                if (!store) return;
73                if (isObjectMapListPanel(store)) {
74                    callHost(store.onClose);
75                    store.close();
76                    return;
77                }
78                this.handleOverlayClick();
79            },
80
81            cancelExplicit: function () {
82                var store = Alpine.store('slideout');
83                if (!store) return;
84                if (store.type === 'form' && store.dirtyGuard) {
85                    store.requestClose();
86                    return;
87                }
88                callHost(store.onClose);
89                store.close();
90            },
91
92            bindIframeSave: function () {
93                if (this._iframeBound) return;
94                var frame = document.getElementById('slideout-save-frame');
95                if (!frame) return;
96                this._iframeBound = true;
97                var self = this;
98                frame.addEventListener('load', function () {
99                    var store = Alpine.store('slideout');
100                    if (!store || !store.isOpen || store.submitTarget !== '
100iframe') return;
101                    var path = '';
102                    try { path = frame.contentWindow.location.pathname || ''; } catch (e) { return; }
103                    if (!path || path.indexOf('about') === 0) return;
104                    if (path.indexOf('/admin/workflows/') === -1) return;
105                    store.dirty = false;
106                    self.finishSaveAttempt();
107                    callHost(store.onSaved);
108                });
109            },
110
111            onContentLoaded: function (body) {
112                var store = Alpine.store('slideout');
113                if (!store) return;
114                if (store.type === 'form') this.decorateForm(body);
115                callHost(store.onContentLoaded);
116            },
117
118            syncEditorsBeforeSubmit: function () {
119                try {
120                    if (window.tinymce) {
121                        window.tinymce.triggerSave();
122                        var ed = window.tinymce.get('body');
123                        if (ed) ed.save();
124                    }
125                } catch (e) { /* ignore */ }
126            },
127
128            syncSaveChrome: function (saving) {
129                var foot = document.getElementById('slideout-foot-actions');
130                if (!foot) return;
131                foot.querySelectorAll('.slideout-foot-save').forEach(function (btn) {
132                    if (saving) {
133                        if (!btn.dataset.slideoutSaveLabel) {
134                            btn.dataset.slideoutSaveLabel = btn.tagName === 'INPUT'
135                                ? btn.value
136                                : (btn.textContent || '').trim();
137                        }
138                        btn.disabled = true;
139                        btn.setAttribute('aria-busy', 'true');
140                        btn.classList.add('-saving');
141                        if (btn.tagName === 'INPUT') btn.value = 'Saving…';
142                        else btn.textContent = 'Saving…';
143                    } else {
144                        btn.disabled = false;
145                        btn.removeAttribute('aria-busy');
146                        btn.classList.remove('-saving');
147                        var label = btn.dataset.slideoutSaveLabel;
148                        if (label) {
149                            if (btn.tagName === 'INPUT') btn.value = label;
150                            else btn.textContent = label;
151                        }
152                    }
153                });
154            },
155
156            finishSaveAttempt: function () {
157                var store = Alpine.store('slideout');
158                if (!store) return;
159                store._saveInFlight = false;
160                store._pendingSave = false;
161                this.syncSaveChrome(false);
162            },
163
164            /** Fallback when htmx does not dispatch HX-Trigger (e.g. hx-swap=none edge cases). */
165            dispatchSaveTriggers: function (xhr) {
166                var firedSaved = false;
167                var triggerHdr = xhr.getResponseHeader('HX-Trigger');
168                if (triggerHdr) {
169                    try {
170                        var triggers = JSON.parse(triggerHdr);
171                        Object.keys(triggers).forEach(function (name) {
172                            document.body.dispatchEvent(new CustomEvent(name, {
173                                detail: triggers[name],
174                                bubbles: true,
175                            }));
176                            if (name === 'slideout-saved') firedSaved = true;
177                        });
178                    } catch (e) { /* ignore */ }
179                }
180                if (!firedSaved) {
181                    try {
182                        var data = JSON.parse(xhr.responseText || '{}');
183                        if (data.ok) {
184                            document.body.dispatchEvent(new CustomEvent('slideout-saved', { bubbles: true }));
185                            if (data.message && typeof notify === 'function') {
186                                notify('Saved', data.message);
187                            }
188                        }
189                    } catch (e) { /* ignore */ }
190                }
191            },
192
193            handleFormHtmxResponse: function (ev) {
194                var store = Alpine.store('slideout');
195                if (!store || !store.isOpen || store.submitTarget !== 'htmx') return;
196                if (!store._pendingSave) return;
197                var xhr = ev.detail.xhr;
198                var self = this;
199                if (xhr.status === 422) {
200                    self.finishSaveAttempt();
201                    try {
202                        var err = JSON.parse(xhr.responseText || '{}');
203                        var msg = (err.errors && err.errors.general) ? err.errors.general : 'Save failed';
204                        if (typeof notify === 'function') notify('Save failed', msg, 'error');
205                    } catch (e) { /* ignore */ }
206                    return;
207                }
208                if (!ev.detail.successful || xhr.status !== 200) {
209                    self.finishSaveAttempt();
210                    if (typeof notify === 'function') notify('Save failed', 'Could not save — please try again.', 'error');
211                    return;
212                }
213                var text = (xhr.responseText || '').trim();
214                if (text.charAt(0) === '<') {
215                    self.finishSaveAttempt();
216                    if (typeof notify === 'function') {
217                        notify('Save failed', 'Server returned the editor page instead of confirming save — please try again.', 'error');
218                    }
219                    return;
220                }
221                self.dispatchSaveTriggers(xhr);
222            },
223
224            markDirty: function (force) {
225                var store = Alpine.store('slideout');
226                if (!store) return;
227                if (force) store._dirtyArmed = true;
228                else if (!store._dirtyArmed) return;
229                store.dirty = true;
230            },
231
232            resetDirtyBaseline: function () {
233                var store = Alpine.store('slideout');
234                if (!store) return;
235                store._dirtyArmed = false;
236                store.dirty = false;
237                if (store._dirtyArmTimer) {
238                    clearTimeout(store._dirtyArmTimer);
239                    store._dirtyArmTimer = null;
240                }
241                try {
242                    if (window.tinymce) {
243                        window.tinymce.triggerSave();
244                        var ed = window.tinymce.get('body');
245                        if (ed && typeof ed.save === 'function') ed.save();
246                    }
247                } catch (e) { /* ignore */ }
248                store._dirtyArmTimer = setTimeout(function () {
249                    if (!store.isOpen) return;
250                    try {
251                        if (window.tinymce) {
252                            window.tinymce.triggerSave();
253                            var ed = window.tinymce.get('body');
254                            if (ed && typeof ed.save === 'function') ed.save();
255                        }
256                    } catch (e) { /* ignore */ }
257                    // Arm only. Do not clear dirty — a radio/slider click during
258                    // this window is a real edit and must still confirm on close.
259                    store._dirtyArmed = true;
260                    store._dirtyArmTimer = null;
261                }, 450);
262            },
263
264            bindDirtyTracking: function (body) {
265                var self = this;
266                if (!body) return;
267                if (!body._slideoutDirtyDelegated) {
268                    body._slideoutDirtyDelegated = true;
269                    body.addEventListener('input', function (ev) { self.markDirty(ev.isTrusted); }, true);
270                    body.addEventListener('change', function (ev) { self.markDirty(ev.isTrusted); }, true);
271                }
272                self.bindTinyMceDirty(body);
273            },
274
275            bindTinyMceDirty: function (body) {
276                var self = this;
277                var bindEd = function (ed) {
278                    if (!ed || ed._slideoutDirtyBound) return;
279                    ed._slideoutDirtyBound = true;
280                    ed.on('change keyup undo redo', function () { self.markDirty(true); });
281                };
282                if (!window.tinymce) return;
283                bindEd(window.tinymce.get('body'));
284                if (!body._slideoutTinyMceHooked) {
285                    body._slideoutTinyMceHooked = true;
286                    window.tinymce.on('AddEditor', function (e) {
287                        if (e.editor && e.editor.id === 'body') bindEd(e.editor);
288                    });
289                }
290                var tries = 0;
291                var poll = setInterval(function () {
292                    bindEd(window.tinymce.get('body'));
293                    if (++tries >= 24) clearInterval(poll);
294                }, 250);
295            },
296
297            decorateForm: function (body) {
298                var store = Alpine.store('slideout');
299                var foot = document.getElementById('slideout-foot-actions');
300                var frameName = store.submitTarget === 'iframe' ? 'slideout-save-frame' : null;
301                var useHtmx = store.submitTarget === 'htmx';
302                var self = this;
303
304                this.bindDirtyTracking(body);
305
306                if (foot) foot.querySelectorAll('[data-slideout-moved]').forEach(function (b) { b.remove(); });
307
308                body.querySelectorAll('form').forEach(function (f, i) {
309                    if (frameName) f.setAttribute('target', frameName);
310                    if (useHtmx) {
311                        f.removeAttribute('target');
312                        var action = f.getAttribute('action') || f.action;
313                        if (action) {
314                            f.setAttribute('hx-post', action);
315                            f.setAttribute('hx-swap', 'none');
316                            f.setAttribute('hx-headers', '{"HX-Request":"true"}');
317                        }
318                    }
319                    if (!f.id) f.id = 'slideout-form-' + i;
320
321                    if (foot) {
322                        f.querySelectorAll('input[type="submit"], button[type="submit"]').forEach(function (btn, bi) {
323                            btn.setAttribute('form', f.id);
324                            btn.setAttribute('data-slideout-moved', '1');
325                            // Opt into .btn so the admin bare-input reset (padding:4px)
326                            // does not leave footer Save buttons looking slimline.
327                            btn.classList.add('slideout-foot-save', 'btn');
328                            if (!btn._slideoutSaveClickBound) {
329                                btn._slideoutSaveClickBound = true;
330                                btn.addEventListener('click', function (ev) {
331                                    if (store && store._saveInFlight) {
332                                        ev.preventDefault();
333                                        ev.stopImmediatePropagation();
334                                    }
335                                }, true);
336                            }
337                            foot.appendChild(btn);
338                            if (bi === 0) f._slideoutSubmitBtn = btn;
339                        });
340                    }
341
342                    // Footer submit buttons sit outside the form DOM; htmx serialises only
343                    // in-form fields, so PHP never sees submit_btn and re-renders the editor.
344                    if (useHtmx && f._slideoutSubmitBtn && !f.querySelector('[name="submit_btn"]')) {
345                        var srcBtn = f._slideoutSubmitBtn;
346                        var hid = document.createElement('input');
347                        hid.type = 'hidden';
348                        hid.name = srcBtn.name || 'submit_btn';
349                        hid.value = srcBtn.value || srcBtn.getAttribute('value') || '1';
350                        f.appendChild(hid);
351                    }
352
353                    if (!f._slideoutEnterBound) {
354                        f._slideoutEnterBound = true;
355                        f.addEventListener('keydown', function (ev) {
356                            if (store && store._saveInFlight) return;
357                            if (ev.key !== 'Enter' || ev.shiftKey || ev.isComposing) return;
358                            var t = ev.target;
359                            var tag = (t.tagName || '').toLowerCase();
360                            if (tag === 'textarea' || tag === 'select' || tag === 'button') return;
361                            if (t.isContentEditable) return;
362                            if (t.closest && t.closest('.tox, .mce-content-body, .schema-builder, .grid')) return;
363                            ev.preventDefault();
364                            var btn = f._slideoutSubmitBtn;
365                            if (store) store._pendingSave = true;
366                            if (f.requestSubmit) f.requestSubmit(btn || undefined);
367                            else if (btn) btn.click();
368                            else f.submit();
369                        });
370                    }
371
372                    if (!f._slideoutSubmitBound) {
373                        f._slideoutSubmitBound = true;
374                        // Capture phase, registered before fieldValidator — sync TinyMCE
375                        // and mark pending save before validation runs.
376                        f.addEventListener('submit', function (ev) {
377                            if (store && store._saveInFlight) {
378                                ev.preventDefault();
379                                ev.stopPropagation();
380                                return;
381                            }
382                            self.syncEditorsBeforeSubmit();
383                            if (store) store._pendingSave = true;
384                        }, true);
385                    }
386
387                    if (window.fieldValidator && !f._fvBound) {
388                        window.fieldValidator.bind(f, { requiredFields: store.requiredFields || [] });
389                    }
390
391                    if (useHtmx && !f._slideoutHtmxBound) {
392                        f._slideoutHtmxBound = true;
393                        f.addEventListener('htmx:beforeRequest', function (ev) {
394                            if (!store || !store._pendingSave || store._saveInFlight) {
395                                if (store && store._saveInFlight) ev.preventDefault();
396                                return;
397                            }
398                            store._saveInFlight = true;
399                            self.syncSaveChrome(true);
400                        });
401                        f.addEventListener('htmx:afterRequest', function (ev) {
402                            self.handleFormHtmxResponse(ev);
403                        });
404                        if (window.htmx) {
405                            try { window.htmx.process(f); } catch (e) { /* ignore */ }
406                        }
407                    }
408                });
409
410                body.querySelectorAll('.button-block').forEach(function (bb) {
411                    if (!bb.querySelector('input, button')) bb.style.display = 'none';
412                });
413
414                self.bindMaillistGroups(body);
415                self.initFragmentTooltips(body);
416                body.querySelectorAll('a[href]:not(.popupwindow):not([target])').forEach(function (a) {
417                    a.target = '_blank';
418                    a.rel = 'noopener';
419                });
420
421                var bump = function () { try { window.dispatchEvent(new Event('resize')); } catch (e) { /* ignore */ } };
422                setTimeout(bump, 220);
423                setTimeout(bump, 520);
424                setTimeout(bump, 900);
425
426                if (window.htmx) {
427                    try { window.htmx.process(body); } catch (e) { /* ignore */ }
428                }
429
430                self.resetDirtyBaseline();
431            },
432
433            initFragmentTooltips: function (body) {
434                var tip = window.tippy;
435                if (typeof tip !== 'function') return;
436                var bind = function (el, content) {
437                    if (!content || el._slideoutTipped) return;
438                    el._slideoutTipped = true;
439                    if (el._tippy) { try { el._tippy.destroy(); } catch (e) { /* ignore */ } }
440                    try {
441                        tip(el, {
442                            content: content,
443                            allowHTML: true,
444                            maxWidth: 320,
445                            appendTo: function () { return document.body; },
446                        });
447                    } catch (e) { /* ignore */ }
448                };
449                body.querySelectorAll('.tippy.info-hint').forEach(function (el) {
450                    var html = el.getAttribute('data-tp_content') || el.innerHTML.trim();
451                    if (html) bind(el, '<div class="modern-form">' + html + '</div>');
452                });
453            },
454
455            bindMaillistGroups: function (body) {
456                var box = body.querySelector('#maillist-groups[data-group-action]');
457                var sel = body.querySelector('#id_maillist_id');
458                if (!box || !sel || box.dataset.maillistBound === '1') return;
459                box.dataset.maillistBound = '1';
460                var payload = {};
461                try { payload = JSON.parse(box.getAttribute('data-group-action') || '{}'); } catch (e) { /* ignore */ }
462                var load = function () {
463                    var id = sel.value;
464                    if (!id) { box.innerHTML = ''; return; }
465                    var fd = new FormData();
466                    var ga = payload.group_action;
467                    if (Array.isArray(ga)) {
468                        ga.forEach(function (v) { if (v) fd.append('group_action[]', v); });
469                    } else if (ga != null && ga !== '') {
470                        fd.append('group_action', ga);
471                    }
472                    fetch('/ecampaign-' + id + '/admin/ajax/display_group_add_form.htm', {
473                        method: 'POST',
474                        headers: { 'HX-Request': 'true' },
475                        body: fd,
476                    }).then(function (r) { return r.text(); }).then(function (html) {
477                        box.innerHTML = html;
478                        if (window.htmx) { try { window.htmx.process(box); } catch (e) { /* ignore */ } }
479                    }).catch(function () { /* ignore */ });
480                };
481                sel.addEventListener('change', load);
482                load();
483            },
484
485            /** POST-loaded fragment (e.g. manual trigger panel). */
486            loadPostFragment: function (url, bodyStr, onLoaded) {
487                var store = Alpine.store('slideout');
488                var target = (store && typeof store.htmxTarget === 'function')
489                    ? store.htmxTarget()
490                    : (document.getElementById('slideout-htmx-body') || document.getElementById('slideout-content'));
491                if (!target) return;
492                target.innerHTML = '<div class="slideout-loading">Loading…</div>';
493                fetch(url, {
494                    method: 'POST',
495                    headers: {
496                        'Content-Type': 'application/x-www-form-urlencoded',
497                        'HX-Request': 'true',
498                    },
499                    body: bodyStr,
500                }).then(function (r) { return r.text(); })
501                    .then(function (html) {
502                        target.innerHTML = html;
503                        if (window.htmx) { try { window.htmx.process(target); } catch (e) { /* ignore */ } }
504                        if (typeof onLoaded === 'function') onLoaded(target);
505                    })
506                    .catch(function () {
507                        target.innerHTML = '<div class="slideout-loading">Could not load — please try again.</div>';
508                    });
509            },
510        };
511    }
512
513    window.appComponents = window.appComponents || {};
514    window.appComponents.slideoutForm = slideoutForm;
515})();

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.