check / check (push) Waiting to run
An event's row in the event log was a button element, whose text a browser does not let be selected, and a drag over the event's ID toggled the event. The row is now an element with the button role: focusable, toggled by Enter and Space, and saying whether it is expanded. A click that ends a text selection leaves the event as it is. The browser test now also clicks the row's caret, selects the ID with the mouse, and uses the keyboard. Model: opus-5-5
142 lines
4.6 KiB
JavaScript
142 lines
4.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 entrypoint's edit form, an event in the event log or in the
|
|
// recent events, a delivery's attempts. It starts hidden, or shown
|
|
// when its element has the data-open attribute.
|
|
window.Alpine.data("collapsible", function () {
|
|
return {
|
|
open: false,
|
|
init() {
|
|
this.open = this.$root.hasAttribute("data-open");
|
|
},
|
|
toggle() {
|
|
this.open = !this.open;
|
|
},
|
|
// Toggles, except on a click that ends a text selection, such
|
|
// as selecting an event's ID to copy it.
|
|
toggleUnlessSelecting() {
|
|
if (window.getSelection().toString() === "") {
|
|
this.toggle();
|
|
}
|
|
},
|
|
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;
|
|
},
|
|
};
|
|
});
|
|
});
|