From 1bbb837183895033360c5cb1524991ebd287443f Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Sun, 31 Dec 2023 15:59:46 +0100 Subject: [PATCH 1/3] feat(core dom): Add document_ready function which ensures callbacks to be run when or after the DOM has been loaded. --- src/core/dom.js | 26 ++++++++++++++ src/core/dom.test.js | 80 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/src/core/dom.js b/src/core/dom.js index ad3c42659..bee3d91c3 100644 --- a/src/core/dom.js +++ b/src/core/dom.js @@ -1,4 +1,5 @@ /* Utilities for DOM traversal or navigation */ +import events from "./events"; import logging from "./logging"; import create_uuid from "./uuid"; @@ -9,6 +10,30 @@ const DATA_STYLE_DISPLAY = "__patternslib__style__display"; const INPUT_SELECTOR = "input, select, textarea, button"; +/** + * Wait for the document to be ready. + * + * @param {Function} fn - The function to call when the document is ready. + */ +const document_ready = (fn) => { + const event_id = create_uuid(); + + const _ready = () => { + if (document.readyState !== "loading") { + // Remove the event listener for this callback. + events.remove_event_listener(document, event_id); + // call on next available tick + setTimeout(fn, 1); + } + }; + + // Listen for the document to be ready and call _ready() when it is. + events.add_event_listener(document, "readystatechange", event_id, _ready); + + // Also check the ready state immediately in case we missed the event. + _ready(); +}; + /** * Return an array of DOM nodes. * @@ -575,6 +600,7 @@ const find_inputs = (el) => { }; const dom = { + document_ready: document_ready, toNodeArray: toNodeArray, querySelectorAllAndMe: querySelectorAllAndMe, wrap: wrap, diff --git a/src/core/dom.test.js b/src/core/dom.test.js index caa17cbcb..f8159f38e 100644 --- a/src/core/dom.test.js +++ b/src/core/dom.test.js @@ -1,11 +1,91 @@ import $ from "jquery"; import dom from "./dom"; +import utils from "./utils"; describe("core.dom tests", () => { // Tests from the core.dom module afterEach(() => { document.body.innerHTML = ""; + jest.restoreAllMocks(); + }); + + describe("document_ready", () => { + it("calls the callback, once the document is ready.", async () => { + let cnt = 0; + const counter = () => { + cnt++; + }; + + // Call document ready immediately. It should already call the + // callback, if ready. Which it isn't. + jest.spyOn(document, "readyState", "get").mockReturnValue("loading"); + dom.document_ready(counter); + await utils.timeout(1); + expect(cnt).toBe(0); + + // While readyState "loading" the callback should not be called. + document.dispatchEvent(new Event("readystatechange")); + await utils.timeout(1); + expect(cnt).toBe(0); + + // While still loading the callback should still not be called. + document.dispatchEvent(new Event("readystatechange")); + await utils.timeout(1); + expect(cnt).toBe(0); + + // Now it's the time. + jest.spyOn(document, "readyState", "get").mockReturnValue("complete"); + document.dispatchEvent(new Event("readystatechange")); + await utils.timeout(1); + expect(cnt).toBe(1); + + // But the callback is only called once and the event handler removed from the document. + document.dispatchEvent(new Event("readystatechange")); + await utils.timeout(1); + expect(cnt).toBe(1); + }); + + it("it will also fire on readyState interactive, not only complete.", async () => { + let cnt = 0; + const counter = () => { + cnt++; + }; + + // Call document ready immediately. It should already call the + // callback, if ready. Which it isn't. + jest.spyOn(document, "readyState", "get").mockReturnValue("loading"); + dom.document_ready(counter); + await utils.timeout(1); + expect(cnt).toBe(0); + + // When readyState interactive, the callback should be called. + jest.spyOn(document, "readyState", "get").mockReturnValue("interactive"); + document.dispatchEvent(new Event("readystatechange")); + await utils.timeout(1); + expect(cnt).toBe(1); + }); + + it("the callback will be called immedeately if the ready state change has already happended.", async () => { + let cnt = 0; + const counter = () => { + cnt++; + }; + + // Call document ready immediately. It should already call the + // callback, if ready. Which it isn't. + jest.spyOn(document, "readyState", "get").mockReturnValue("complete"); + dom.document_ready(counter); + await utils.timeout(1); + expect(cnt).toBe(1); + + // But another state change would not call the callback, because + // the event listener is already de-registered. + jest.spyOn(document, "readyState", "get").mockReturnValue("interactive"); + document.dispatchEvent(new Event("readystatechange")); + await utils.timeout(1); + expect(cnt).toBe(1); + }); }); describe("toNodeArray tests", () => { From c73b0e1caf7848f8b208c00aa91516fcc387a9e2 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Mon, 1 Jan 2024 15:04:22 +0100 Subject: [PATCH 2/3] maint(core registry): Use dom.document_ready instead of jQuery aequivalent. --- src/core/registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/registry.js b/src/core/registry.js index 03fd0430c..4418ca93a 100644 --- a/src/core/registry.js +++ b/src/core/registry.js @@ -61,7 +61,7 @@ const registry = { // the DOM is scanned. After that registering a new pattern // results in rescanning the DOM only for this pattern. init() { - $(document).ready(function () { + dom.document_ready(() => { if (window.__patternslib_registry_initialized) { // Do not reinitialize a already initialized registry. return; From e8c8b4b530e0c99d1e484ca7c8aa94ec108487ff Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Mon, 1 Jan 2024 15:04:05 +0100 Subject: [PATCH 3/3] maint(pat-markdown): Use dom.document_ready instead of jQuery aequivalent. --- src/pat/markdown/markdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pat/markdown/markdown.js b/src/pat/markdown/markdown.js index 03ecabf4a..0fe1976f8 100644 --- a/src/pat/markdown/markdown.js +++ b/src/pat/markdown/markdown.js @@ -104,7 +104,7 @@ class Pattern extends BasePattern { } } -$(document).ready(function () { +dom.document_ready(() => { $(document.body).on( "patterns-inject-triggered.pat-markdown", "a.pat-inject",