From 6e956fe87ed29dc8da3c6e17301f143c1a56021c Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 14 Jan 2026 09:47:36 +0100 Subject: [PATCH 1/3] chore(linting): enable no-cycle --- packages/oxlint-config/rules/consider.jsonc | 1 - packages/oxlint-config/rules/enabled.jsonc | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/oxlint-config/rules/consider.jsonc b/packages/oxlint-config/rules/consider.jsonc index 4a8612fe9e71..e5012adadf2f 100644 --- a/packages/oxlint-config/rules/consider.jsonc +++ b/packages/oxlint-config/rules/consider.jsonc @@ -2,7 +2,6 @@ "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json", "rules": { "no-array-for-each": "off", - "no-cycle": "off", "no-nested-ternary": "off", "no-array-sort": "off", "preserve-caught-error": "off", diff --git a/packages/oxlint-config/rules/enabled.jsonc b/packages/oxlint-config/rules/enabled.jsonc index f3d1b6bdebb8..7e9561344ae1 100644 --- a/packages/oxlint-config/rules/enabled.jsonc +++ b/packages/oxlint-config/rules/enabled.jsonc @@ -80,5 +80,6 @@ "unicorn/prefer-structured-clone": "error", "curly": ["error", "multi-line", "consistent"], "no-sequences": "error", + "import/no-cycle": "error", }, } From 50dfe5f6f48096200d548d832ffa2c654767856b Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 14 Jan 2026 10:28:19 +0100 Subject: [PATCH 2/3] refactor: rewrite onDOMReady to be closer to jquery ready implementation (@fehmer) (#7356) --- frontend/__tests__/utils/dom.jsdom-spec.ts | 113 ++++++++++++++++++++- frontend/src/ts/utils/dom.ts | 94 ++++++++++++++--- 2 files changed, 192 insertions(+), 15 deletions(-) diff --git a/frontend/__tests__/utils/dom.jsdom-spec.ts b/frontend/__tests__/utils/dom.jsdom-spec.ts index 25c5f424c4ed..238623106bad 100644 --- a/frontend/__tests__/utils/dom.jsdom-spec.ts +++ b/frontend/__tests__/utils/dom.jsdom-spec.ts @@ -1,7 +1,13 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { screen } from "@testing-library/dom"; import { userEvent } from "@testing-library/user-event"; -import { ElementWithUtils, qsr } from "../../src/ts/utils/dom"; +import { + ElementWithUtils, + qsr, + onDOMReady, + __testing, +} from "../../src/ts/utils/dom"; +const resetReady = __testing.resetReady; describe("dom", () => { describe("ElementWithUtils", () => { @@ -168,4 +174,109 @@ describe("dom", () => { }); }); }); + describe("onDOMReady", () => { + beforeEach(() => { + document.body.innerHTML = ""; + resetReady(); + vi.useFakeTimers(); + }); + + function dispatchEvent(event: "DOMContextLoaded" | "load"): void { + if (event === "DOMContextLoaded") { + document.dispatchEvent(new Event("DOMContentLoaded")); + } else { + window.dispatchEvent(new Event("load")); + } + + vi.runAllTimers(); + } + + it("executes callbacks when DOMContentLoaded fires", () => { + const spy = vi.fn(); + onDOMReady(spy); + expect(spy).not.toHaveBeenCalled(); + + dispatchEvent("DOMContextLoaded"); + + expect(spy).toHaveBeenCalledOnce(); + }); + + it("executes callbacks added before ready in order", () => { + const calls: number[] = []; + onDOMReady(() => calls.push(1)); + onDOMReady(() => calls.push(2)); + + dispatchEvent("DOMContextLoaded"); + + expect(calls).toEqual([1, 2]); + }); + + it("executes callbacks asynchronously when DOM is already ready", () => { + const spy = vi.fn(); + + Object.defineProperty(document, "readyState", { + value: "complete", + configurable: true, + }); + + onDOMReady(spy); + + expect(spy).not.toHaveBeenCalled(); + + vi.runAllTimers(); + + expect(spy).toHaveBeenCalledOnce(); + }); + + it("executes callbacks added after ready asynchronously", () => { + const calls: string[] = []; + onDOMReady(() => calls.push("ready")); + + dispatchEvent("DOMContextLoaded"); + + onDOMReady(() => calls.push("late")); + + expect(calls).toEqual(["ready"]); + + vi.runAllTimers(); + + expect(calls).toEqual(["ready", "late"]); + }); + + it("executes callbacks added during ready execution", () => { + const calls: number[] = []; + + onDOMReady(() => { + calls.push(1); + onDOMReady(() => calls.push(3)); + }); + + onDOMReady(() => calls.push(2)); + + dispatchEvent("DOMContextLoaded"); + + expect(calls).toEqual([1, 2, 3]); + }); + + it("does not execute ready callbacks more than once", () => { + const spy = vi.fn(); + + onDOMReady(spy); + + dispatchEvent("DOMContextLoaded"); + dispatchEvent("load"); + + expect(spy).toHaveBeenCalledOnce(); + }); + + it("falls back to window load event if DOMContentLoaded does not fire", () => { + const spy = vi.fn(); + + onDOMReady(spy); + + dispatchEvent("load"); + + expect(spy).toHaveBeenCalledOnce(); + }); + }); }); diff --git a/frontend/src/ts/utils/dom.ts b/frontend/src/ts/utils/dom.ts index c6cdf4553802..a4ceb1e14278 100644 --- a/frontend/src/ts/utils/dom.ts +++ b/frontend/src/ts/utils/dom.ts @@ -4,7 +4,79 @@ import { JSAnimation, } from "animejs"; -// Implementation +/** + * list of deferred callbacks to be executed once we reached ready state + */ +let readyList: (() => void)[] | undefined; +let isReady = false; + +/** + * Execute a callback function when the DOM is fully loaded. + * Tries to mimic the ready function of jQuery https://github.com/jquery/jquery/blob/main/src/core/ready.js + * If the document is already loaded, the callback is executed in the next event loop + */ +export function onDOMReady(callback: () => void): void { + bindReady(); + if (isReady) { + setTimeout(callback); + } else { + readyList?.push(callback); + } +} + +/** + * initialize the readyList and bind the necessary events + */ +function bindReady(): void { + // do nothing if we are bound already + if (readyList !== undefined) return; + + readyList = []; + + if (document.readyState !== "loading") { + // DOM is already loaded handle ready in the next event loop + // Handle it asynchronously to allow scripts the opportunity to delay ready + setTimeout(handleReady); + } else { + // register a single event listener for both events. + document.addEventListener("DOMContentLoaded", handleReady); + //load event is used as a fallback "that will always work" according to jQuery source code + window.addEventListener("load", handleReady); + } +} + +/** + * call all deferred ready callbacks and cleanup the event listener + */ +function handleReady(): void { + //make sure we only run once + if (isReady) return; + + isReady = true; + + //cleanup event listeners that are no longer needed + document.removeEventListener("DOMContentLoaded", handleReady); + window.removeEventListener("load", handleReady); + + //call deferred callbacks and empty the list + //flush the list in a loop in case callbacks were added during the execution + while (readyList && readyList.length) { + const callbacks = readyList; + readyList = []; + callbacks.forEach((it) => { + //jQuery lets the callbacks fail independently + try { + it(); + } catch (e) { + setTimeout(() => { + throw e; + }); + } + }); + } + readyList = undefined; +} + /** * Query Selector * @@ -54,19 +126,6 @@ export function qsr( return new ElementWithUtils(el); } -/** - * Execute a callback function when the DOM is fully loaded. If you need to wait - * for all resources (images, stylesheets, scripts, etc.) to load, use `onWindowLoad` instead. - * If the document is already loaded, the callback is executed immediately. - */ -export function onDOMReady(callback: () => void): void { - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", callback); - } else { - callback(); - } -} - /** * Creates an ElementWithUtils wrapping a newly created element. * @param tagName The tag name of the element to create. @@ -958,3 +1017,10 @@ function checkUniqueSelector( bannerCenter?.appendChild(warning); } } + +export const __testing = { + resetReady: () => { + isReady = false; + readyList = undefined; + }, +}; From 3534f8fc3131389aacf3193a0ccfd477cc806367 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 14 Jan 2026 11:01:53 +0100 Subject: [PATCH 3/3] fix(theme): fix button roundness in dark_note theme (@fehmer) (#7358) --- frontend/static/themes/dark_note.css | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/static/themes/dark_note.css b/frontend/static/themes/dark_note.css index 24e6163948b1..5b156b33ebbf 100644 --- a/frontend/static/themes/dark_note.css +++ b/frontend/static/themes/dark_note.css @@ -20,6 +20,7 @@ --current-color: var(--main-color); } +button:not(.accountButtonAndMenu *), .button:not(.accountButtonAndMenu *), #testConfig .row { --roundness: 5em; /* overwriting default roundness to make it softer */