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.