check / check (push) Waiting to run
The pages' Content-Security-Policy forbids eval, which the standard Alpine.js build needs, so no directive ran: add forms showed open and events never collapsed. 3p/ now holds the @alpinejs/csp 3.14.9 tarball instead, and every directive in templates/ names a property or method of a component registered in static/js/app.js, as that build requires. The policy is unchanged. A headless Chromium test in internal/server loads the webhook page and the event log under the real headers. The Dockerfile's test stage installs chromium; where it is missing the test skips. Model: opus-5-5
119 lines
3.6 KiB
JavaScript
119 lines
3.6 KiB
JavaScript
// 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 add target form. Only the chosen type's fields show, and the
|
|
// others are disabled so that the form does not submit them.
|
|
window.Alpine.data("targetForm", function () {
|
|
return {
|
|
targetType: "http",
|
|
chooseType(event) {
|
|
this.targetType = event.target.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;
|
|
},
|
|
};
|
|
});
|
|
});
|