Files
musenalm/views/transform/main.js
2026-01-09 18:34:59 +01:00

434 lines
13 KiB
JavaScript

// INFO: We import this so vite processes the stylesheet
import "./site.css";
import { FilterPill } from "./filter-pill.js";
import { FilterList } from "./filter-list.js";
import { ScrollButton } from "./scroll-button.js";
import { ToolTip } from "./tool-tip.js";
import { PopupImage } from "./popup-image.js";
import { TabList } from "./tab-list.js";
import { AbbreviationTooltips } from "./abbrev-tooltips.js";
import { IntLink } from "./int-link.js";
import { ImageReel } from "./image-reel.js";
import { MultiSelectRole } from "./multi-select-role.js";
import { MultiSelectSimple } from "./multi-select-simple.js";
import { ResetButton } from "./reset-button.js";
import { DivManager } from "./div-menu.js";
import { ItemsEditor } from "./items-editor.js";
import { SingleSelectRemote } from "./single-select-remote.js";
import { AlmanachEditPage } from "./almanach-edit.js";
import { RelationsEditor } from "./relations-editor.js";
import { EditPage } from "./edit-page.js";
import { FabMenu } from "./fab-menu.js";
const FILTER_LIST_ELEMENT = "filter-list";
const FAB_MENU_ELEMENT = "fab-menu";
const SCROLL_BUTTON_ELEMENT = "scroll-button";
const TOOLTIP_ELEMENT = "tool-tip";
const ABBREV_TOOLTIPS_ELEMENT = "abbrev-tooltips";
const INT_LINK_ELEMENT = "int-link";
const POPUP_IMAGE_ELEMENT = "popup-image";
const TABLIST_ELEMENT = "tab-list";
const FILTER_PILL_ELEMENT = "filter-pill";
const IMAGE_REEL_ELEMENT = "image-reel";
const MULTI_SELECT_ROLE_ELEMENT = "multi-select-places";
const MULTI_SELECT_SIMPLE_ELEMENT = "multi-select-simple";
const SINGLE_SELECT_REMOTE_ELEMENT = "single-select-remote";
const RESET_BUTTON_ELEMENT = "reset-button";
const DIV_MANAGER_ELEMENT = "div-manager";
const ITEMS_EDITOR_ELEMENT = "items-editor";
const ALMANACH_EDIT_PAGE_ELEMENT = "almanach-edit-page";
const RELATIONS_EDITOR_ELEMENT = "relations-editor";
const EDIT_PAGE_ELEMENT = "edit-page";
customElements.define(INT_LINK_ELEMENT, IntLink);
customElements.define(ABBREV_TOOLTIPS_ELEMENT, AbbreviationTooltips);
customElements.define(FILTER_LIST_ELEMENT, FilterList);
customElements.define(SCROLL_BUTTON_ELEMENT, ScrollButton);
customElements.define(TOOLTIP_ELEMENT, ToolTip);
customElements.define(POPUP_IMAGE_ELEMENT, PopupImage);
customElements.define(TABLIST_ELEMENT, TabList);
customElements.define(FILTER_PILL_ELEMENT, FilterPill);
customElements.define(IMAGE_REEL_ELEMENT, ImageReel);
customElements.define(MULTI_SELECT_ROLE_ELEMENT, MultiSelectRole);
customElements.define(MULTI_SELECT_SIMPLE_ELEMENT, MultiSelectSimple);
customElements.define(SINGLE_SELECT_REMOTE_ELEMENT, SingleSelectRemote);
customElements.define(RESET_BUTTON_ELEMENT, ResetButton);
customElements.define(DIV_MANAGER_ELEMENT, DivManager);
customElements.define(ITEMS_EDITOR_ELEMENT, ItemsEditor);
customElements.define(ALMANACH_EDIT_PAGE_ELEMENT, AlmanachEditPage);
customElements.define(RELATIONS_EDITOR_ELEMENT, RelationsEditor);
customElements.define(EDIT_PAGE_ELEMENT, EditPage);
customElements.define(FAB_MENU_ELEMENT, FabMenu);
function PathPlusQuery() {
const path = window.location.pathname;
const query = window.location.search;
const fullPath = path + query;
return encodeURIComponent(fullPath);
}
/**
* @param {number} timeout - Maximum time to wait in milliseconds.
* @param {number} interval - How often to check in milliseconds.
* @returns {Promise<Function>} Resolves with the QRCode constructor when available.
*/
function getQRCodeWhenAvailable(timeout = 5000, interval = 100) {
return new Promise((resolve, reject) => {
let elapsedTime = 0;
const checkInterval = setInterval(() => {
if (typeof window.QRCode === "function") {
clearInterval(checkInterval);
resolve(window.QRCode); // Resolve with the QRCode object/function
} else {
elapsedTime += interval;
if (elapsedTime >= timeout) {
clearInterval(checkInterval);
console.error("Timed out waiting for QRCode to become available.");
reject(new Error("QRCode not available after " + timeout + "ms. Check if qrcode.min.js is loaded correctly and sets window.QRCode."));
}
}
}, interval);
});
}
// INFO: We have to wait for the QRCode object to be available. It's messy.
async function GenQRCode(value) {
const QRCode = await getQRCodeWhenAvailable();
const qrElement = document.getElementById("qr");
if (qrElement) {
// INFO: Clear previous QR code if any
// Also hide it initially to prevent flickering
qrElement.innerHTML = "";
qrElement.classList.add("hidden");
new QRCode(qrElement, {
text: value,
width: 1280,
height: 1280,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H,
});
setTimeout(() => {
qrElement.classList.remove("hidden");
}, 20);
}
// Add event listeners to the token input field to select its content on focus or click
}
function SelectableInput(tokenElement) {
if (tokenElement) {
tokenElement.addEventListener("focus", (ev) => {
ev.preventDefault();
tokenElement.select();
});
tokenElement.addEventListener("mousedown", (ev) => {
ev.preventDefault();
tokenElement.select();
});
tokenElement.addEventListener("mouseup", (ev) => {
ev.preventDefault();
tokenElement.select();
});
}
if (tokenElement) {
tokenElement.addEventListener("focus", () => {
tokenElement.select();
});
tokenElement.addEventListener("click", () => {
tokenElement.select();
});
}
}
// TODO: Doesn't work properly.
// Intended to make sure errors from boosted links are shown.
function ShowBoostedErrors() {
document.body.addEventListener("htmx:responseError", function (event) {
const config = event.detail.requestConfig;
if (config.boosted) {
document.body.innerHTML = event.detail.xhr.responseText;
const newUrl = event.detail.xhr.responseURL || config.url;
window.history.pushState(null, "", newUrl);
}
});
}
// INFO: Hooks up to all the reset button children of the target element.
// If an element has a changed state, it will trigger the action with `true`.
// If no elements are changed, it will trigger the action with `false`.
// @param {HTMLElement} target - The parent element containing reset buttons.
// @param {Function} action - The function to call with the change state.
function HookupRBChange(target, action) {
if (!(target instanceof HTMLElement)) {
console.warn("Target must be an HTMLElement.");
return;
}
if (typeof action !== "function") {
console.warn("Action must be a function.");
return;
}
const btns = target.querySelectorAll(RESET_BUTTON_ELEMENT);
target.addEventListener("rbichange", (event) => {
for (const btn of btns) {
if (btn.isCurrentlyModified()) {
action(event.details, true);
return;
}
}
action(event.details, false);
});
}
// Check if browser supports field-sizing: content (Chrome 123+, Edge 123+)
// Firefox, Safari don't support it yet as of 2024
let browserSupportsFieldSizing = null;
function supportsFieldSizing() {
if (browserSupportsFieldSizing !== null) {
return browserSupportsFieldSizing;
}
// Check if CSS.supports is available and test for field-sizing
if (typeof CSS !== "undefined" && typeof CSS.supports === "function") {
browserSupportsFieldSizing = CSS.supports("field-sizing", "content");
} else {
browserSupportsFieldSizing = false;
}
console.log("Browser supports field-sizing:", browserSupportsFieldSizing);
return browserSupportsFieldSizing;
}
// Simple textarea auto-resize function
function TextareaAutoResize(textarea) {
console.log("TextareaAutoResize called for:", textarea.name || textarea.id);
if (!(textarea instanceof HTMLTextAreaElement)) {
console.log("Not a textarea element");
return;
}
// Skip if not visible
if (textarea.offsetParent === null) {
console.log("Textarea not visible");
return;
}
// Remove rows attribute
textarea.removeAttribute("rows");
// Set overflow auto to allow scrolling if needed
textarea.style.overflow = "auto";
// Special case: annotation textarea has 2 rows minimum
const isAnnotation = textarea.name === "annotation";
const minHeight = isAnnotation ? 76 : 38; // 2 rows vs 1 row
// For empty textareas, set minimum height
if (textarea.value.trim() === "") {
textarea.style.height = minHeight + "px";
console.log("Empty textarea, setting height to:", minHeight + "px");
return;
}
// Reset to 1px to get accurate scrollHeight
textarea.style.height = "1px";
// Set to content height (scrollHeight is the actual content height)
const contentHeight = textarea.scrollHeight;
const newHeight = Math.max(contentHeight, minHeight) + "px";
console.log("Setting height to:", newHeight);
textarea.style.height = newHeight;
}
function NoEnters(event) {
if (event.key === "Enter") {
event.preventDefault();
}
}
function HookupTextareaAutoResize(textarea) {
if (!(textarea instanceof HTMLTextAreaElement)) {
console.warn("HookupTextareaAutoResize: Provided element is not a textarea.");
return;
}
// If browser supports field-sizing, CSS handles it
if (supportsFieldSizing()) {
return;
}
// Fallback: attach event listener for manual resizing
textarea.addEventListener("input", () => {
TextareaAutoResize(textarea);
});
}
function DisconnectTextareaAutoResize(textarea) {
if (!(textarea instanceof HTMLTextAreaElement)) {
console.warn("DisconnectTextareaAutoResize: Provided element is not a textarea.");
return;
}
// Remove the input event listener
textarea.removeEventListener("input", () => {
TextareaAutoResize(textarea);
});
}
// INFO: Prevents Enter key from being used in textareas.
// @param {HTMLTextAreaElement} textarea - The textarea element to hook up the no-enter
// functionality.
function HookupNoEnters(textarea) {
if (!(textarea instanceof HTMLTextAreaElement) && textarea.classList.contains("no-enter")) {
return;
}
textarea.addEventListener("keydown", NoEnters);
}
// @param {HTMLTextAreaElement} textarea - The textarea element to disconnect the no-enter
function DisconnectNoEnters(textarea) {
if (!(textarea instanceof HTMLTextAreaElement) && textarea.classList.contains("no-enter")) {
return;
}
textarea.removeEventListener("keydown", NoEnters);
}
function MutateObserve(mutations, observer) {
const needsJSResize = !supportsFieldSizing();
for (const mutation of mutations) {
if (mutation.type === "childList") {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE && node.matches("textarea")) {
if (needsJSResize) {
HookupTextareaAutoResize(node);
TextareaAutoResize(node);
}
}
}
for (const node of mutation.removedNodes) {
if (node.nodeType === Node.ELEMENT_NODE && node.matches("textarea")) {
DisconnectNoEnters(node);
if (needsJSResize) {
DisconnectTextareaAutoResize(node);
}
}
}
}
}
}
// INFO: Various options and plugs for laoding and parsing forms.
function FormLoad(form) {
console.log("=== FormLoad CALLED ===");
if (!(form instanceof HTMLFormElement)) {
console.warn("FormLoad: Provided element is not a form.");
return;
}
const textareas = document.querySelectorAll("textarea");
console.log("Found", textareas.length, "textareas");
// Attach resize handler to all textareas
for (const textarea of textareas) {
console.log("Attaching input listener to:", textarea.name || textarea.id);
textarea.addEventListener('input', function() {
console.log("Input event on textarea:", this.name || this.id);
TextareaAutoResize(this);
});
}
// Initial resize after short delay (increased to ensure content is loaded)
setTimeout(() => {
console.log("Running initial textarea resize on", textareas.length, "textareas");
for (const textarea of textareas) {
TextareaAutoResize(textarea);
}
}, 200);
const noEnterTextareas = document.querySelectorAll("textarea.no-enter");
for (const textarea of noEnterTextareas) {
HookupNoEnters(textarea);
}
const observer = new MutationObserver(MutateObserve);
observer.observe(form, {
childList: true,
subtree: true,
});
// Watch for class changes (hidden/visible) and resize textareas when they become visible
const visibilityObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === "attributes" && mutation.attributeName === "class") {
const target = mutation.target;
// Check if this element or its children contain textareas
if (target instanceof HTMLElement) {
const textareasInTarget = target.matches("textarea")
? [target]
: Array.from(target.querySelectorAll("textarea"));
for (const textarea of textareasInTarget) {
// Only resize if now visible
if (textarea.offsetParent !== null) {
TextareaAutoResize(textarea);
}
}
}
}
}
});
visibilityObserver.observe(form, {
attributes: true,
attributeFilter: ["class"],
subtree: true,
});
}
document.addEventListener("keydown", (event) => {
if (event.key !== "Enter") {
return;
}
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
if (target.matches("textarea.no-enter")) {
event.preventDefault();
}
});
window.ShowBoostedErrors = ShowBoostedErrors;
window.GenQRCode = GenQRCode;
window.SelectableInput = SelectableInput;
window.PathPlusQuery = PathPlusQuery;
window.HookupRBChange = HookupRBChange;
window.FormLoad = FormLoad;
window.TextareaAutoResize = TextareaAutoResize;
export {
FilterList,
ScrollButton,
AbbreviationTooltips,
MultiSelectSimple,
MultiSelectRole,
ToolTip,
PopupImage,
TabList,
FilterPill,
ImageReel,
IntLink,
ItemsEditor,
SingleSelectRemote,
AlmanachEditPage,
RelationsEditor,
EditPage,
FabMenu,
};