// Webhooker client-side JavaScript // Copy-to-clipboard, as progressive enhancement. // // Markup renders each copy button with the `hidden` attribute and a // `data-copy-target` pointing at the id of the element holding the // text. This script reveals a button only once it has both a resolvable // target and a usable Clipboard API, so a browser without either shows // no button at all and the text stays selectable. (function () { "use strict"; const revertDelayMs = 2000; function flash(button, message) { const original = button.getAttribute("data-copy-label"); button.textContent = message; window.setTimeout(function () { button.textContent = original; }, revertDelayMs); } function wire(button) { const target = document.getElementById( button.getAttribute("data-copy-target") ); if (!target) { return; } button.setAttribute("data-copy-label", button.textContent); button.addEventListener("click", function () { navigator.clipboard.writeText(target.textContent.trim()).then( function () { flash(button, "Copied"); }, function () { flash(button, "Copy failed"); } ); }); button.removeAttribute("hidden"); } function init() { if (!navigator.clipboard || !navigator.clipboard.writeText) { return; } const buttons = document.querySelectorAll("[data-copy-target]"); buttons.forEach(wire); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })(); // Alpine.js components. // // The pages' Content-Security-Policy forbids eval, so the UI loads // Alpine's CSP build, which cannot run expressions written in the // markup: a directive in templates/ may only name a property or method, // and each x-data names a component registered here. This script runs // before Alpine, whose script tag is deferred, so this listener is in // place when Alpine starts. document.addEventListener("alpine:init", function () { "use strict"; // Something a click shows and hides: the mobile menu, an add form, // an entrypoint's edit form, an event in the event log, a delivery's // attempts. window.Alpine.data("collapsible", function () { return { open: false, toggle() { this.open = !this.open; }, get closed() { return !this.open; }, // Turns a downward caret up while open. get caretClass() { return { "rotate-180": this.open }; }, }; }); // The add target form. Only the chosen type's fields show, and the // others are disabled so that the form does not submit them. // // The type is read from the type select when Alpine starts, when the // select changes, and on pageshow. Going back to the page, the // browser restores the type chosen before without a change event, // in some browsers only after Alpine has started, but always before // pageshow. window.Alpine.data("targetForm", function () { return { targetType: "", init() { this.readType(); }, readType() { this.targetType = this.$root.querySelector( 'select[name="type"]' ).value; }, get isHttp() { return this.targetType === "http"; }, get isSlack() { return this.targetType === "slack"; }, get isDatabase() { return this.targetType === "database"; }, get notHttp() { return !this.isHttp; }, get notSlack() { return !this.isSlack; }, get notDatabase() { return !this.isDatabase; }, }; }); });