Molodetz blogrol volgens DPP-template

This commit is contained in:
2026-10-05 09:36:20 +02:00
commit f290a4c766
277 changed files with 15758 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
// retoor <retoor@molodetz.nl>
import { Component } from "./Component.js";
export class Avatar extends Component {
static tag = "dp-avatar";
static observedAttributes = ["seed", "uid", "size", "online"];
setup() {
this.render();
}
attributeChangedCallback() {
if (this.dataset.ready) {
this.render();
}
}
render() {
const uid = this.getAttribute("uid") || "";
const size = Number(this.getAttribute("size") || 32);
this.replaceChildren();
const img = document.createElement("img");
img.className = "avatar";
img.alt = "";
img.width = size;
img.height = size;
const seed = this.getAttribute("seed") || uid;
img.src = `/avatar/svg/${encodeURIComponent(seed)}?size=${size}`;
const dot = document.createElement("span");
dot.className = "presence-dot";
dot.dataset.presenceUid = uid;
dot.classList.toggle("online", this.hasAttribute("online"));
this.append(img, dot);
}
}
+43
View File
@@ -0,0 +1,43 @@
// retoor <retoor@molodetz.nl>
import { Component } from "./Component.js";
export class Chat extends Component {
static tag = "dp-chat";
setup() {
this.log = document.createElement("div");
this.log.className = "chat-log";
this.log.setAttribute("aria-live", "polite");
const form = document.createElement("form");
form.className = "chat-form";
this.input = document.createElement("textarea");
this.input.rows = 2;
this.input.setAttribute("aria-label", "Bericht");
const send = document.createElement("button");
send.type = "submit";
send.className = "button small";
send.textContent = "Verstuur";
form.append(this.input, send);
form.addEventListener("submit", (event) => {
event.preventDefault();
const text = this.input.value.trim();
if (text) {
this.emit("dp-chat-send", { text });
this.input.value = "";
}
});
this.append(this.log, form);
}
addMessage(author, text) {
const item = document.createElement("div");
item.className = "chat-line";
const who = document.createElement("strong");
who.textContent = author;
const body = document.createElement("span");
body.textContent = ` ${text}`;
item.append(who, body);
this.log.append(item);
this.log.scrollTop = this.log.scrollHeight;
}
}
+37
View File
@@ -0,0 +1,37 @@
// retoor <retoor@molodetz.nl>
import hljs from "../../vendor/highlight.es.min.js";
import { Component } from "./Component.js";
import { Toast } from "../utils/Toast.js";
export class Code extends Component {
static tag = "dp-code";
setup() {
requestAnimationFrame(() => this.enhance());
}
enhance() {
const code = this.querySelector("pre > code");
if (!code) {
return;
}
try {
hljs.highlightElement(code);
} catch (error) {
console.debug("highlight failed", error);
}
const button = document.createElement("button");
button.type = "button";
button.className = "copy-button";
button.textContent = "Kopieer";
button.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(code.textContent);
Toast.flash(button, "Gekopieerd", 1500, "Kopieer");
} catch {
Toast.flash(button, "Mislukt", 1500, "Kopieer");
}
});
this.prepend(button);
}
}
@@ -0,0 +1,23 @@
// retoor <retoor@molodetz.nl>
export class Component extends HTMLElement {
static tag = "";
static define() {
if (this.tag && !customElements.get(this.tag)) {
customElements.define(this.tag, this);
}
}
emit(name, detail = {}) {
this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
}
connectedCallback() {
if (!this.dataset.ready) {
this.dataset.ready = "1";
this.setup();
}
}
setup() {}
}
+23
View File
@@ -0,0 +1,23 @@
// retoor <retoor@molodetz.nl>
import { Component } from "./Component.js";
import { ContentRenderer } from "../utils/ContentRenderer.js";
export class Content extends Component {
static tag = "dp-content";
setup() {
const source = this.querySelector('script[type="text/markdown"]');
if (source) {
this.markdown = source.textContent;
}
}
set markdown(text) {
this.innerHTML = ContentRenderer.render(text);
this.querySelectorAll("pre > code").forEach((code) => {
const host = document.createElement("dp-code");
code.parentElement.replaceWith(host);
host.appendChild(code.parentElement);
});
}
}
@@ -0,0 +1,41 @@
// retoor <retoor@molodetz.nl>
import { Component } from "./Component.js";
export class ContextMenu extends Component {
static tag = "dp-context-menu";
setup() {
this.hidden = true;
document.addEventListener("click", (event) => {
if (!this.contains(event.target)) {
this.close();
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
this.close();
}
});
}
open(x, y, items) {
this.replaceChildren();
items.forEach((item) => {
const button = document.createElement("button");
button.type = "button";
button.textContent = item.label;
button.addEventListener("click", () => {
this.close();
item.action();
});
this.append(button);
});
this.style.left = `${Math.min(x, window.innerWidth - 220)}px`;
this.style.top = `${y}px`;
this.hidden = false;
}
close() {
this.hidden = true;
}
}
+50
View File
@@ -0,0 +1,50 @@
// retoor <retoor@molodetz.nl>
import { Component } from "./Component.js";
export class Dialog extends Component {
static tag = "dp-dialog";
setup() {
this.className = "modal-overlay";
this.setAttribute("role", "alertdialog");
this.setAttribute("aria-modal", "true");
}
confirm(message, { okLabel = "Ja", cancelLabel = "Annuleren" } = {}) {
return new Promise((resolve) => {
this.replaceChildren();
const box = document.createElement("div");
box.className = "modal";
const text = document.createElement("p");
text.textContent = message;
const actions = document.createElement("div");
actions.className = "actions";
const ok = document.createElement("button");
ok.type = "button";
ok.className = "button";
ok.textContent = okLabel;
const cancel = document.createElement("button");
cancel.type = "button";
cancel.className = "button ghost";
cancel.textContent = cancelLabel;
actions.append(cancel, ok);
box.append(text, actions);
this.append(box);
this.classList.add("visible");
const finish = (value) => {
this.classList.remove("visible");
document.removeEventListener("keydown", onKey);
resolve(value);
};
const onKey = (event) => {
if (event.key === "Escape") {
finish(false);
}
};
document.addEventListener("keydown", onKey);
ok.addEventListener("click", () => finish(true));
cancel.addEventListener("click", () => finish(false));
ok.focus();
});
}
}
+72
View File
@@ -0,0 +1,72 @@
// retoor <retoor@molodetz.nl>
import { Component } from "./Component.js";
export class Lightbox extends Component {
static tag = "dp-lightbox";
setup() {
this.setAttribute("role", "dialog");
this.setAttribute("aria-modal", "true");
this.setAttribute("aria-label", "Afbeelding");
this.hidden = true;
this.image = document.createElement("img");
this.image.alt = "";
this.caption = document.createElement("p");
this.caption.className = "caption";
this.closeButton = document.createElement("button");
this.closeButton.type = "button";
this.closeButton.className = "modal-close lightbox-close";
this.closeButton.setAttribute("aria-label", "Sluiten");
this.closeButton.textContent = "\u00d7";
this.append(this.closeButton, this.image, this.caption);
this.items = [];
this.index = 0;
document.addEventListener("click", (event) => this.onDocumentClick(event));
this.addEventListener("click", (event) => {
if (event.target === this || event.target === this.closeButton) {
this.close();
}
});
document.addEventListener("keydown", (event) => this.onKey(event));
}
onDocumentClick(event) {
const link = event.target.closest("[data-lightbox]");
if (!link || event.metaKey || event.ctrlKey) {
return;
}
event.preventDefault();
this.items = [...document.querySelectorAll("[data-lightbox]")];
this.show(this.items.indexOf(link));
}
show(index) {
this.index = (index + this.items.length) % this.items.length;
const link = this.items[this.index];
this.image.src = link.getAttribute("href");
this.image.alt = link.dataset.caption || "";
this.caption.textContent = link.dataset.caption || "";
this.hidden = false;
this.classList.add("visible");
this.closeButton.focus();
}
close() {
this.hidden = true;
this.classList.remove("visible");
this.image.removeAttribute("src");
}
onKey(event) {
if (this.hidden) {
return;
}
if (event.key === "Escape") {
this.close();
} else if (event.key === "ArrowRight") {
this.show(this.index + 1);
} else if (event.key === "ArrowLeft") {
this.show(this.index - 1);
}
}
}
+12
View File
@@ -0,0 +1,12 @@
// retoor <retoor@molodetz.nl>
import { Component } from "./Component.js";
import { ContentRenderer } from "../utils/ContentRenderer.js";
export class Title extends Component {
static tag = "dp-title";
setup() {
const text = this.textContent;
this.innerHTML = ContentRenderer.render(text, { inline: true });
}
}
@@ -0,0 +1,19 @@
// retoor <retoor@molodetz.nl>
import { Component } from "./Component.js";
export class ToastHost extends Component {
static tag = "dp-toast";
setup() {
this.setAttribute("role", "status");
this.setAttribute("aria-live", "polite");
}
show(message, kind = "info", ms = 3500) {
const item = document.createElement("div");
item.className = `toast ${kind}`;
item.textContent = message;
this.append(item);
setTimeout(() => item.remove(), ms);
}
}
+31
View File
@@ -0,0 +1,31 @@
// retoor <retoor@molodetz.nl>
import { Component } from "./Component.js";
export class Upload extends Component {
static tag = "dp-upload";
setup() {
this.input = document.createElement("input");
this.input.type = "file";
this.input.multiple = this.hasAttribute("multiple");
this.input.accept = this.getAttribute("accept") || "image/*";
this.input.className = "sr-only";
const button = document.createElement("button");
button.type = "button";
button.className = "button ghost small";
button.textContent = this.getAttribute("label") || "Bestand kiezen";
button.addEventListener("click", () => this.input.click());
this.input.addEventListener("change", () => this.emit("dp-files", { files: [...this.input.files] }));
this.addEventListener("dragover", (event) => {
event.preventDefault();
this.classList.add("dragging");
});
this.addEventListener("dragleave", () => this.classList.remove("dragging"));
this.addEventListener("drop", (event) => {
event.preventDefault();
this.classList.remove("dragging");
this.emit("dp-files", { files: [...event.dataTransfer.files] });
});
this.append(this.input, button);
}
}
+13
View File
@@ -0,0 +1,13 @@
// retoor <retoor@molodetz.nl>
import { Avatar } from "./Avatar.js";
import { Chat } from "./Chat.js";
import { Code } from "./Code.js";
import { Content } from "./Content.js";
import { ContextMenu } from "./ContextMenu.js";
import { Dialog } from "./Dialog.js";
import { Lightbox } from "./Lightbox.js";
import { Title } from "./Title.js";
import { ToastHost } from "./ToastHost.js";
import { Upload } from "./Upload.js";
[Avatar, Chat, Code, Content, ContextMenu, Dialog, Lightbox, Title, ToastHost, Upload].forEach((component) => component.define());