Molodetz blogrol volgens DPP-template
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
Reference in New Issue
Block a user