forked from retoor/devplacepy
feat: add news sanitize CLI command and upload static file serving with content-disposition
This commit is contained in:
@@ -1,538 +1,25 @@
|
||||
import { ModalManager } from "./ModalManager.js";
|
||||
import { FormManager } from "./FormManager.js";
|
||||
import { VoteManager } from "./VoteManager.js";
|
||||
import { MessageSearch } from "./MessageSearch.js";
|
||||
import { ProfileEditor } from "./ProfileEditor.js";
|
||||
import { MobileNav } from "./MobileNav.js";
|
||||
import { CommentManager } from "./CommentManager.js";
|
||||
import { ContentEnhancer } from "./ContentEnhancer.js";
|
||||
import { DomUtils } from "./DomUtils.js";
|
||||
|
||||
class Application {
|
||||
constructor() {
|
||||
this.initPasswordToggles();
|
||||
this.initModals();
|
||||
this.initPostForm();
|
||||
this.initCommentForms();
|
||||
this.initVoteButtons();
|
||||
this.initMessageSearch();
|
||||
this.initNotificationDismiss();
|
||||
this.initProfileEdit();
|
||||
this.initMobileNav();
|
||||
this.initMessagesResponsive();
|
||||
|
||||
this.initFormDisable();
|
||||
this.initContentRenderer();
|
||||
this.initCommentReply();
|
||||
this.initEmojiPickers();
|
||||
this.initMentionInputs();
|
||||
this.initConfirmations();
|
||||
this.initImageFallbacks();
|
||||
this.initClipboardCopy();
|
||||
this.initAutoSubmitSelects();
|
||||
this.initTogglers();
|
||||
this.initStopPropagation();
|
||||
this.initMessageThread();
|
||||
this.initPlatformTags();
|
||||
this.initAttachmentManagers();
|
||||
this.modals = new ModalManager();
|
||||
this.forms = new FormManager();
|
||||
this.votes = new VoteManager();
|
||||
this.messageSearch = new MessageSearch();
|
||||
this.profile = new ProfileEditor();
|
||||
this.mobileNav = new MobileNav();
|
||||
this.comments = new CommentManager();
|
||||
this.content = new ContentEnhancer();
|
||||
this.dom = new DomUtils();
|
||||
}
|
||||
|
||||
initMessagesResponsive() {
|
||||
const list = document.querySelector(".messages-list");
|
||||
const main = document.querySelector(".messages-main");
|
||||
const backBtn = document.getElementById("messages-back-btn");
|
||||
if (!list || !main) return;
|
||||
|
||||
const isMobile = () => window.innerWidth <= 768;
|
||||
|
||||
if (isMobile()) {
|
||||
if (window.location.search.includes("with_uid=")) {
|
||||
list.classList.add("hide");
|
||||
main.classList.remove("hide");
|
||||
} else {
|
||||
list.classList.remove("hide");
|
||||
main.classList.add("hide");
|
||||
}
|
||||
}
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener("click", () => {
|
||||
if (!isMobile()) return;
|
||||
list.classList.remove("hide");
|
||||
main.classList.add("hide");
|
||||
window.history.replaceState(null, "", "/messages");
|
||||
});
|
||||
}
|
||||
|
||||
list.querySelectorAll(".conversation-item").forEach((item) => {
|
||||
item.addEventListener("click", (e) => {
|
||||
if (!isMobile()) return;
|
||||
list.classList.add("hide");
|
||||
main.classList.remove("hide");
|
||||
});
|
||||
});
|
||||
|
||||
const mq = window.matchMedia("(max-width: 768px)");
|
||||
mq.addEventListener("change", () => {
|
||||
if (!isMobile()) {
|
||||
list.classList.remove("hide");
|
||||
main.classList.remove("hide");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initMobileNav() {
|
||||
const btn = document.getElementById("hamburger-btn");
|
||||
const panel = document.getElementById("mobile-panel");
|
||||
const overlay = document.getElementById("mobile-overlay");
|
||||
if (!btn || !panel || !overlay) return;
|
||||
|
||||
const close = () => {
|
||||
panel.classList.remove("open");
|
||||
overlay.style.display = "none";
|
||||
btn.innerHTML = "\u2630";
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
panel.classList.add("open");
|
||||
overlay.style.display = "block";
|
||||
btn.innerHTML = "\u2715";
|
||||
};
|
||||
|
||||
btn.addEventListener("click", () => {
|
||||
if (panel.classList.contains("open")) {
|
||||
close();
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
});
|
||||
|
||||
overlay.addEventListener("click", close);
|
||||
|
||||
panel.querySelectorAll("a").forEach((link) => {
|
||||
link.addEventListener("click", close);
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && panel.classList.contains("open")) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadCSS(href) {
|
||||
if (document.querySelector(`link[href="${href}"]`)) {
|
||||
return;
|
||||
}
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
initPasswordToggles() {
|
||||
document.querySelectorAll(".auth-toggle-pw").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const input = btn.parentElement.querySelector("input");
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
const type = input.type === "password" ? "text" : "password";
|
||||
input.type = type;
|
||||
btn.textContent = type === "password" ? "\u{1F441}" : "\u{1F441}";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initModals() {
|
||||
document.querySelectorAll("[data-modal]").forEach((trigger) => {
|
||||
trigger.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const modalId = trigger.dataset.modal;
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.add("visible");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll(".modal-overlay").forEach((modal) => {
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.classList.remove("visible");
|
||||
}
|
||||
});
|
||||
modal.querySelectorAll(".modal-close").forEach((closeBtn) => {
|
||||
closeBtn.addEventListener("click", () => {
|
||||
modal.classList.remove("visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initPostForm() {
|
||||
const form = document.getElementById("create-post-form");
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
const content = form.querySelector("#post-content");
|
||||
const title = form.querySelector("#post-title");
|
||||
const contentCount = form.querySelector("#post-content-count");
|
||||
const titleCount = form.querySelector("#post-title-count");
|
||||
|
||||
if (content && contentCount) {
|
||||
content.addEventListener("input", () => {
|
||||
contentCount.textContent = `${content.value.length}/2000`;
|
||||
});
|
||||
}
|
||||
if (title && titleCount) {
|
||||
title.addEventListener("input", () => {
|
||||
titleCount.textContent = `${title.value.length}/500`;
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener("submit", () => {
|
||||
const btn = form.querySelector("button[type='submit']");
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = "Posting...";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initFormDisable() {
|
||||
document.querySelectorAll("form").forEach((form) => {
|
||||
form.addEventListener("submit", () => {
|
||||
const btn = form.querySelector("button[type='submit']");
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initCommentForms() {
|
||||
document.querySelectorAll(".comment-form").forEach((form) => {
|
||||
const textarea = form.querySelector("textarea");
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
textarea.addEventListener("input", () => {
|
||||
textarea.style.height = "auto";
|
||||
textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initVoteButtons() {
|
||||
document.querySelectorAll(".post-action-btn[data-vote]").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const targetUid = btn.dataset.target;
|
||||
const targetType = btn.dataset.type || "post";
|
||||
const value = btn.dataset.vote;
|
||||
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = `/votes/${targetType}/${targetUid}`;
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = "value";
|
||||
input.value = value;
|
||||
form.appendChild(input);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initMessageSearch() {
|
||||
const searchInput = document.getElementById("message-search");
|
||||
if (!searchInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wrap = searchInput.parentElement;
|
||||
const dropdown = document.createElement("div");
|
||||
dropdown.className = "search-dropdown";
|
||||
wrap.appendChild(dropdown);
|
||||
|
||||
let debounceTimer = null;
|
||||
|
||||
searchInput.addEventListener("input", () => {
|
||||
clearTimeout(debounceTimer);
|
||||
const q = searchInput.value.trim();
|
||||
if (q.length < 1) {
|
||||
dropdown.innerHTML = "";
|
||||
dropdown.style.display = "none";
|
||||
return;
|
||||
}
|
||||
debounceTimer = setTimeout(async () => {
|
||||
try {
|
||||
const resp = await fetch(`/messages/search?q=${encodeURIComponent(q)}`);
|
||||
const data = await resp.json();
|
||||
const results = data.results || [];
|
||||
if (results.length === 0) {
|
||||
dropdown.style.display = "none";
|
||||
return;
|
||||
}
|
||||
dropdown.innerHTML = "";
|
||||
for (const r of results) {
|
||||
const item = document.createElement("a");
|
||||
item.className = "search-dropdown-item";
|
||||
item.href = `/messages?with_uid=${r.uid}`;
|
||||
item.innerHTML = `<img src="/avatar/multiavatar/${encodeURIComponent(r.username)}?size=24" class="avatar-img" style="width:24px;height:24px;border-radius:50%" alt="" loading="lazy"><span>${r.username}</span>`;
|
||||
dropdown.appendChild(item);
|
||||
}
|
||||
dropdown.style.display = "block";
|
||||
} catch (e) {
|
||||
// silently fail — no suggestions
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
|
||||
searchInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
const first = dropdown.querySelector(".search-dropdown-item");
|
||||
if (first) {
|
||||
window.location.href = first.href;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!wrap.contains(e.target)) {
|
||||
dropdown.style.display = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initNotificationDismiss() {
|
||||
document.querySelectorAll(".notification-dismiss").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const uid = btn.dataset.uid;
|
||||
if (!uid) {
|
||||
return;
|
||||
}
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = `/notifications/mark-read/${uid}`;
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initProfileEdit() {
|
||||
document.querySelectorAll("[data-edit-field]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const field = btn.dataset.editField;
|
||||
const display = document.getElementById(`display-${field}`);
|
||||
const input = document.getElementById(`input-${field}`);
|
||||
if (!display || !input) {
|
||||
return;
|
||||
}
|
||||
display.classList.toggle("hidden");
|
||||
input.classList.toggle("hidden");
|
||||
if (!input.classList.contains("hidden")) {
|
||||
input.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initContentRenderer() {
|
||||
document.querySelectorAll("[data-render]").forEach((el) => {
|
||||
window.contentRenderer.applyTo(el);
|
||||
});
|
||||
window.contentRenderer.highlightAll();
|
||||
}
|
||||
|
||||
initCommentReply() {
|
||||
document.querySelectorAll("[data-action='reply']").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const comment = btn.closest(".comment");
|
||||
const commentForm = document.querySelector(".comment-form");
|
||||
if (!comment || !commentForm) return;
|
||||
const textarea = commentForm.querySelector("textarea");
|
||||
if (!textarea) return;
|
||||
let parentInput = commentForm.querySelector('input[name="parent_uid"]');
|
||||
if (!parentInput) {
|
||||
parentInput = document.createElement("input");
|
||||
parentInput.type = "hidden";
|
||||
parentInput.name = "parent_uid";
|
||||
commentForm.appendChild(parentInput);
|
||||
}
|
||||
const commentBody = comment.querySelector(".comment-body");
|
||||
if (commentBody && commentBody.dataset.commentUid) {
|
||||
parentInput.value = commentBody.dataset.commentUid;
|
||||
}
|
||||
textarea.focus();
|
||||
textarea.scrollIntoView({ behavior: "smooth" });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initMentionInputs() {
|
||||
if (!window.MentionInput) return;
|
||||
document.querySelectorAll("[data-mention]").forEach((el) => {
|
||||
if (el.dataset.mentionInitialized) return;
|
||||
el.dataset.mentionInitialized = "true";
|
||||
new window.MentionInput(el);
|
||||
});
|
||||
}
|
||||
|
||||
initConfirmations() {
|
||||
document.querySelectorAll("[data-confirm]").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
if (!confirm(btn.dataset.confirm)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initImageFallbacks() {
|
||||
document.querySelectorAll(".image-fallback").forEach((img) => {
|
||||
img.addEventListener("error", () => {
|
||||
const container = img.closest(".news-card-image, .news-detail-image") || img.parentElement;
|
||||
if (container) container.style.display = "none";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initClipboardCopy() {
|
||||
document.querySelectorAll("[data-copy]").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const source = document.getElementById(btn.dataset.copy);
|
||||
if (!source) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(source.textContent);
|
||||
const original = btn.textContent;
|
||||
btn.textContent = "Copied!";
|
||||
setTimeout(() => { btn.textContent = original; }, 2000);
|
||||
} catch {
|
||||
// silently fail
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initAutoSubmitSelects() {
|
||||
document.querySelectorAll("[data-auto-submit]").forEach((select) => {
|
||||
select.addEventListener("change", () => {
|
||||
const form = select.closest("form");
|
||||
if (form) form.submit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initTogglers() {
|
||||
document.querySelectorAll("[data-toggle]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const target = document.getElementById(btn.dataset.toggle);
|
||||
if (target) target.classList.toggle("hidden");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initStopPropagation() {
|
||||
document.querySelectorAll("[data-stop-propagation]").forEach((el) => {
|
||||
el.addEventListener("click", (e) => e.stopPropagation());
|
||||
});
|
||||
}
|
||||
|
||||
initMessageThread() {
|
||||
const thread = document.querySelector(".messages-thread");
|
||||
if (thread) thread.scrollTop = thread.scrollHeight;
|
||||
}
|
||||
|
||||
initPlatformTags() {
|
||||
const platformsInput = document.getElementById("platforms-input");
|
||||
const hiddenInput = document.getElementById("platforms");
|
||||
const tagsContainer = document.getElementById("platforms-tags");
|
||||
if (!platformsInput || !hiddenInput || !tagsContainer) return;
|
||||
|
||||
const addPlatform = (val) => {
|
||||
val = val.trim();
|
||||
if (!val) return;
|
||||
const tag = document.createElement("span");
|
||||
tag.className = "platform-tag";
|
||||
tag.textContent = val;
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.textContent = "x";
|
||||
remove.style.marginLeft = "4px";
|
||||
remove.style.fontSize = "0.75rem";
|
||||
remove.style.padding = "0";
|
||||
remove.style.background = "none";
|
||||
remove.style.border = "none";
|
||||
remove.style.color = "inherit";
|
||||
remove.style.cursor = "pointer";
|
||||
remove.addEventListener("click", () => {
|
||||
tag.remove();
|
||||
updatePlatforms();
|
||||
});
|
||||
tag.appendChild(remove);
|
||||
tagsContainer.appendChild(tag);
|
||||
platformsInput.value = "";
|
||||
updatePlatforms();
|
||||
};
|
||||
|
||||
const updatePlatforms = () => {
|
||||
const values = [];
|
||||
tagsContainer.querySelectorAll(".platform-tag").forEach((t) => {
|
||||
values.push(t.textContent.replace("x", "").trim());
|
||||
});
|
||||
hiddenInput.value = values.join(",");
|
||||
};
|
||||
|
||||
platformsInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
addPlatform(platformsInput.value);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll(".platform-preset").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
addPlatform(btn.dataset.platform);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initAttachmentManagers() {
|
||||
if (!window.AttachmentUploader) return;
|
||||
document.querySelectorAll(".attachment-upload-container").forEach((container) => {
|
||||
if (container.dataset.attachmentInitialized) return;
|
||||
container.dataset.attachmentInitialized = "true";
|
||||
const form = container.closest("form");
|
||||
if (form) {
|
||||
form.dataset.maxSize = container.dataset.maxSize;
|
||||
form.dataset.maxFiles = container.dataset.maxFiles;
|
||||
form.dataset.allowedTypes = container.dataset.allowedTypes || "";
|
||||
new window.AttachmentUploader(form);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initEmojiPickers() {
|
||||
document.querySelectorAll(".comment-form textarea, .emoji-picker-target").forEach((textarea) => {
|
||||
if (textarea.dataset.emojiInitialized) return;
|
||||
textarea.dataset.emojiInitialized = "true";
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "emoji-toggle-btn";
|
||||
btn.title = "Add emoji";
|
||||
btn.innerHTML = "\u{1F600}";
|
||||
|
||||
const picker = new window.EmojiPicker(textarea);
|
||||
btn.addEventListener("click", () => picker.toggle());
|
||||
|
||||
const parent = textarea.parentElement;
|
||||
if (parent) {
|
||||
const actions = parent.querySelector(".comment-form-actions");
|
||||
if (actions) {
|
||||
actions.style.position = "relative";
|
||||
actions.appendChild(picker.wrapper);
|
||||
actions.insertBefore(btn, actions.firstChild);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
const app = new Application();
|
||||
|
||||
Reference in New Issue
Block a user