Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement document_ready as replacement for $ #1214

Merged
merged 3 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/core/dom.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* Utilities for DOM traversal or navigation */
import events from "./events";
import logging from "./logging";
import create_uuid from "./uuid";

Expand All @@ -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.
*
Expand Down Expand Up @@ -575,6 +600,7 @@ const find_inputs = (el) => {
};

const dom = {
document_ready: document_ready,
toNodeArray: toNodeArray,
querySelectorAllAndMe: querySelectorAllAndMe,
wrap: wrap,
Expand Down
80 changes: 80 additions & 0 deletions src/core/dom.test.js
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/core/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/pat/markdown/markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading