DevPlace CI / test (push) Failing after 2m7s
- Add `/block`, `/mute` endpoints with block/unblock and mute/unmute functionality in `routers/relations.py`, hiding blocked users' content everywhere except their own profile while muting only suppresses notifications - Introduce `devplace emoji-sync` CLI command to regenerate `static/js/emoji-shortcodes.js` from the emoji library, documented in `CLAUDE.md` and wired in `cli.py` - Create `get_blocked_uids()` database helper and apply it in `content.py` `load_detail()` to filter blocked users' posts from detail views - Implement `_uid_index()` and `_drop_index()` helpers in `database.py` for unique uid indexes across tables, with `user_relations` added to `SOFT_DELETE_TABLES` - Document new routes in `AGENTS.md` and `README.md`, including emoji shortcodes rendering behavior distinct from the emoji picker
173 lines
5.9 KiB
JavaScript
173 lines
5.9 KiB
JavaScript
// retoor <retoor@molodetz.nl>
|
|
|
|
import { Component } from "./Component.js";
|
|
|
|
export class AppContextMenu extends Component {
|
|
connectedCallback() {
|
|
if (this._built) {
|
|
return;
|
|
}
|
|
this._built = true;
|
|
this.build();
|
|
this.bindGlobal();
|
|
}
|
|
|
|
build() {
|
|
const menu = document.createElement("div");
|
|
menu.className = "context-menu";
|
|
menu.setAttribute("role", "menu");
|
|
this.appendChild(menu);
|
|
this.menu = menu;
|
|
menu.addEventListener("click", (e) => e.stopPropagation());
|
|
menu.addEventListener("keydown", (e) => this.onKeydown(e));
|
|
}
|
|
|
|
onKeydown(e) {
|
|
const items = [...this.menu.querySelectorAll(".context-menu-item:not([disabled])")];
|
|
if (!items.length) return;
|
|
const index = items.indexOf(document.activeElement);
|
|
let next = null;
|
|
if (e.key === "ArrowDown") next = index + 1;
|
|
else if (e.key === "ArrowUp") next = index - 1;
|
|
else if (e.key === "Home") next = 0;
|
|
else if (e.key === "End") next = items.length - 1;
|
|
if (next === null) return;
|
|
e.preventDefault();
|
|
items[(next + items.length) % items.length].focus();
|
|
}
|
|
|
|
bindGlobal() {
|
|
document.addEventListener("click", (e) => {
|
|
if (!this.menu.contains(e.target)) this.close();
|
|
});
|
|
document.addEventListener("scroll", () => this.close(), true);
|
|
this._lastWidth = window.innerWidth;
|
|
window.addEventListener("resize", () => {
|
|
if (window.innerWidth !== this._lastWidth) {
|
|
this._lastWidth = window.innerWidth;
|
|
this.close();
|
|
}
|
|
});
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape") this.close();
|
|
});
|
|
}
|
|
|
|
render(items) {
|
|
this.menu.textContent = "";
|
|
const list = document.createElement("ul");
|
|
list.className = "context-menu-list";
|
|
list.setAttribute("role", "none");
|
|
items.forEach((item) => {
|
|
const li = document.createElement("li");
|
|
if (item.separator) {
|
|
li.className = "context-menu-sep";
|
|
li.setAttribute("role", "separator");
|
|
list.appendChild(li);
|
|
return;
|
|
}
|
|
li.setAttribute("role", "none");
|
|
const btn = document.createElement("button");
|
|
btn.type = "button";
|
|
btn.className = "context-menu-item" + (item.danger ? " danger" : "");
|
|
btn.setAttribute("role", "menuitem");
|
|
btn.tabIndex = -1;
|
|
btn.disabled = !!item.disabled;
|
|
if (item.disabled) btn.setAttribute("aria-disabled", "true");
|
|
const icon = item.icon ? `<span class="context-menu-icon" aria-hidden="true">${item.icon}</span>` : "";
|
|
btn.innerHTML = icon + '<span class="context-menu-label"></span>';
|
|
btn.querySelector(".context-menu-label").textContent = item.label;
|
|
if (!item.disabled && item.onSelect) {
|
|
btn.addEventListener("click", () => {
|
|
this.close();
|
|
item.onSelect();
|
|
});
|
|
}
|
|
li.appendChild(btn);
|
|
list.appendChild(li);
|
|
});
|
|
this.menu.appendChild(list);
|
|
}
|
|
|
|
open(x, y, items) {
|
|
if (!items || !items.length) return;
|
|
this.render(items);
|
|
this.menu.classList.add("visible");
|
|
const rect = this.menu.getBoundingClientRect();
|
|
const vv = window.visualViewport;
|
|
const safeW = vv ? vv.width : window.innerWidth;
|
|
const safeH = vv ? vv.height : window.innerHeight;
|
|
const offX = vv ? vv.offsetLeft : 0;
|
|
const offY = vv ? vv.offsetTop : 0;
|
|
let left = x;
|
|
let top = y;
|
|
if (left + rect.width > offX + safeW) left = offX + safeW - rect.width - 8;
|
|
if (top + rect.height > offY + safeH) top = offY + safeH - rect.height - 8;
|
|
this.menu.style.left = Math.max(offX + 8, left) + "px";
|
|
this.menu.style.top = Math.max(offY + 8, top) + "px";
|
|
const first = this.menu.querySelector(".context-menu-item:not([disabled])");
|
|
if (first) setTimeout(() => first.focus(), 0);
|
|
}
|
|
|
|
close() {
|
|
this.menu.classList.remove("visible");
|
|
}
|
|
|
|
suppressNextClick() {
|
|
const handler = (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
document.addEventListener("click", handler, { capture: true, once: true });
|
|
}
|
|
|
|
attach(host, builder) {
|
|
host.addEventListener("contextmenu", (e) => {
|
|
const items = builder(e);
|
|
if (!items || !items.length) return;
|
|
e.preventDefault();
|
|
this.open(e.clientX, e.clientY, items);
|
|
});
|
|
|
|
let timer = null;
|
|
let startX = 0;
|
|
let startY = 0;
|
|
let fired = false;
|
|
const clear = () => {
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
timer = null;
|
|
}
|
|
};
|
|
host.addEventListener("touchstart", (e) => {
|
|
if (e.touches.length !== 1) {
|
|
clear();
|
|
return;
|
|
}
|
|
const touch = e.touches[0];
|
|
startX = touch.clientX;
|
|
startY = touch.clientY;
|
|
fired = false;
|
|
timer = setTimeout(() => {
|
|
fired = true;
|
|
this.open(startX, startY, builder(e) || []);
|
|
}, 500);
|
|
}, { passive: true });
|
|
host.addEventListener("touchmove", (e) => {
|
|
const touch = e.touches[0];
|
|
if (touch && (Math.abs(touch.clientX - startX) > 10 || Math.abs(touch.clientY - startY) > 10)) {
|
|
clear();
|
|
}
|
|
}, { passive: true });
|
|
host.addEventListener("touchend", () => {
|
|
clear();
|
|
if (fired) {
|
|
fired = false;
|
|
this.suppressNextClick();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
customElements.define("dp-context-menu", AppContextMenu);
|