// 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 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 targets section's add target form, in three steps: closed, // choosing a type, then filling in that type's fields. targetType // is empty until Next takes it from the type select. // // The reason and the fields' values come from the properties below // rather than from the markup, because each type's fields are made // afresh from the markup whenever that type is chosen. A refused // submission comes back with its type, reason and values in the // section's data attributes, and starts on that type's fields with // them. Cancel empties these properties and resets the form, which // holds whatever was typed, so the next Add starts with an empty // form and no reason. window.Alpine.data("targetForm", function () { return { choosing: false, targetType: "", reason: "", name: "", url: "", headers: "", timeout: "", maxRetries: "", expiry: "", init() { const refused = this.$root.dataset; this.targetType = refused.type; this.reason = refused.reason; this.name = refused.name; this.url = refused.destination; this.headers = refused.headers; this.timeout = refused.timeout; this.maxRetries = refused.maxRetries; this.expiry = refused.expiry; }, add() { this.choosing = true; }, next() { this.targetType = this.$refs.type.value; this.choosing = false; }, cancel() { this.choosing = false; this.targetType = ""; this.reason = ""; this.name = ""; this.url = ""; this.headers = ""; this.timeout = ""; this.maxRetries = ""; this.expiry = ""; this.$refs.form.reset(); }, get filling() { return this.targetType !== ""; }, get closed() { return !this.choosing && !this.filling; }, get isHttp() { return this.targetType === "http"; }, get isSlack() { return this.targetType === "slack"; }, get isDatabase() { return this.targetType === "database"; }, get isLog() { return this.targetType === "log"; }, }; }); });