diff --git a/src/core/dom.js b/src/core/dom.js index bca83d0bc..2f3983938 100644 --- a/src/core/dom.js +++ b/src/core/dom.js @@ -37,17 +37,32 @@ const document_ready = (fn) => { /** * Return an array of DOM nodes. * - * @param {Node|NodeList|jQuery} nodes - The DOM node to start the search from. + * @param {Node|NodeList|jQuery} nodes - The object which should be returned as array. * * @returns {Array} - An array of DOM nodes. */ -const toNodeArray = (nodes) => { - if (nodes.jquery || nodes instanceof NodeList) { - // jQuery or document.querySelectorAll +const to_node_array = (nodes) => { + if (nodes?.jquery || nodes instanceof NodeList) { nodes = [...nodes]; } else if (nodes instanceof Array === false) { nodes = [nodes]; } + // Filter for DOM nodes only. + nodes = nodes.filter((node) => node instanceof Node); + return nodes; +}; + +/** + * Return an array of DOM elements. + * + * @param {Node|NodeList|jQuery} nodes - The object which should be returned as array. + * + * @returns {Array} - An array of DOM elements. + */ +const to_element_array = (nodes) => { + nodes = to_node_array(nodes); + // Filter for DOM elements only. + nodes = nodes.filter((node) => node instanceof Element); return nodes; }; @@ -55,18 +70,28 @@ const toNodeArray = (nodes) => { * Like querySelectorAll but including the element where it starts from. * Returns an Array, not a NodeList * - * @param {Node} el - The DOM node to start the search from. + * @param {Element|NodeList|Array} el - The DOM element, NodeList or array of elements to start the search from. + * @param {string} selector - The CSS selector to search for. * - * @returns {Array} - The DOM nodes found. + * @returns {Array} - The DOM elements found. */ const querySelectorAllAndMe = (el, selector) => { - if (!el || !el.querySelectorAll) { - return []; - } - - const all = [...el.querySelectorAll(selector)]; - if (el.matches(selector)) { - all.unshift(el); // start element should be first. + // Ensure we have a list of DOM elements. + const roots = to_element_array(el); + const seen = new WeakSet(); + const all = []; + + for (const root of roots) { + if (root.matches(selector) && !seen.has(root)) { + all.push(root); + seen.add(root); + } + for (const match of root.querySelectorAll(selector)) { + if (!seen.has(match)) { + all.push(match); + seen.add(match); + } + } } return all; }; @@ -157,8 +182,6 @@ const is_button = (el) => { `); }; - - /** * Return all direct parents of ``el`` matching ``selector``. * This matches against all parents but not the element itself. @@ -383,15 +406,15 @@ const get_relative_position = (el, reference_el = document.body) => { // https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect const left = Math.abs( el.getBoundingClientRect().left + - reference_el.scrollLeft - - reference_el.getBoundingClientRect().left - - dom.get_css_value(reference_el, "border-left-width", true) + reference_el.scrollLeft - + reference_el.getBoundingClientRect().left - + dom.get_css_value(reference_el, "border-left-width", true) ); const top = Math.abs( el.getBoundingClientRect().top + - reference_el.scrollTop - - reference_el.getBoundingClientRect().top - - dom.get_css_value(reference_el, "border-top-width", true) + reference_el.scrollTop - + reference_el.getBoundingClientRect().top - + dom.get_css_value(reference_el, "border-top-width", true) ); return { top, left }; @@ -535,9 +558,9 @@ const get_visible_ratio = (el, container) => { container !== window ? container.getBoundingClientRect() : { - top: 0, - bottom: window.innerHeight, - }; + top: 0, + bottom: window.innerHeight, + }; let visible_ratio = 0; if (rect.top < container_rect.bottom && rect.bottom > container_rect.top) { @@ -619,7 +642,9 @@ const find_inputs = (el) => { const dom = { document_ready: document_ready, - toNodeArray: toNodeArray, + to_element_array: to_element_array, + to_node_array: to_node_array, + toNodeArray: to_node_array, // BBB. querySelectorAllAndMe: querySelectorAllAndMe, wrap: wrap, hide: hide, diff --git a/src/core/dom.test.js b/src/core/dom.test.js index 16f1c8cac..a9a3d26bb 100644 --- a/src/core/dom.test.js +++ b/src/core/dom.test.js @@ -88,7 +88,7 @@ describe("core.dom tests", () => { }); }); - describe("toNodeArray tests", () => { + describe("to_node_array tests", () => { it("returns an array of nodes, if a jQuery object was passed.", (done) => { const html = document.createElement("div"); html.innerHTML = ` @@ -100,11 +100,18 @@ describe("core.dom tests", () => { const testee = $("span", html); expect(testee.length).toBe(2); - const ret = dom.toNodeArray(testee); + const ret = dom.to_node_array(testee); + expect(ret.jquery).toBeFalsy(); expect(ret.length).toBe(2); + expect(ret[0]).toBe(el1); + expect(ret[0].jquery).toBeFalsy(); + expect(ret[0] instanceof Node).toBe(true); + expect(ret[1]).toBe(el2); + expect(ret[1].jquery).toBeFalsy(); + expect(ret[1] instanceof Node).toBe(true); done(); }); @@ -120,7 +127,7 @@ describe("core.dom tests", () => { const testee = html.querySelectorAll("span"); expect(testee.length).toBe(2); - const ret = dom.toNodeArray(testee); + const ret = dom.to_node_array(testee); expect(ret instanceof NodeList).toBeFalsy(); expect(ret.length).toBe(2); expect(ret[0]).toBe(el1); @@ -132,13 +139,121 @@ describe("core.dom tests", () => { it("returns an array with a single node, if a single node was passed.", (done) => { const html = document.createElement("div"); - const ret = dom.toNodeArray(html); + const ret = dom.to_node_array(html); + expect(ret instanceof Array).toBeTruthy(); + expect(ret.length).toBe(1); + expect(ret[0]).toBe(html); + + done(); + }); + + it("returns an empty array, if nothing was passed", (done) => { + const ret = dom.to_node_array(); + expect(ret.length).toBe(0); + expect(ret instanceof Array).toBe(true); + + done(); + }); + + it("returns only DOM Nodes", (done) => { + const el = document.body; + const txt = document.createTextNode("okay"); + const ret = dom.toNodeArray([1, false, txt, "okay", el]); + expect(ret.length).toBe(2); + expect(ret[0]).toBe(txt); + expect(ret[1]).toBe(el); + + done(); + }); + + it("returns only DOM Nodes, using the deprecated name", (done) => { + const el = document.body; + const txt = document.createTextNode("okay"); + const ret = dom.toNodeArray([1, false, txt, "okay", el]); + expect(ret.length).toBe(2); + expect(ret[0]).toBe(txt); + expect(ret[1]).toBe(el); + + done(); + }); + }); + + describe("to_element_array tests", () => { + it("returns an array of DOM elements, if a jQuery object was passed.", (done) => { + const html = document.createElement("div"); + html.innerHTML = ` + + + `; + const el1 = html.querySelector("#id1"); + const el2 = html.querySelector("#id2"); + const testee = $("span", html); + expect(testee.length).toBe(2); + + const ret = dom.to_element_array(testee); + + expect(ret.jquery).toBeFalsy(); + expect(ret.length).toBe(2); + + expect(ret[0]).toBe(el1); + expect(ret[0].jquery).toBeFalsy(); + expect(ret[0] instanceof Element).toBe(true); + + expect(ret[1]).toBe(el2); + expect(ret[1].jquery).toBeFalsy(); + expect(ret[1] instanceof Element).toBe(true); + + done(); + }); + + it("returns an array of elements, if a NodeList was passed.", (done) => { + const html = document.createElement("div"); + html.innerHTML = ` + + + `; + const el1 = html.querySelector("#id1"); + const el2 = html.querySelector("#id2"); + const testee = html.querySelectorAll("span"); + expect(testee.length).toBe(2); + + const ret = dom.to_element_array(testee); + expect(ret instanceof NodeList).toBeFalsy(); + expect(ret.length).toBe(2); + expect(ret[0]).toBe(el1); + expect(ret[1]).toBe(el2); + + done(); + }); + + it("returns an array with a single element, if a single element was passed.", (done) => { + const html = document.createElement("div"); + + const ret = dom.to_element_array(html); expect(ret instanceof Array).toBeTruthy(); expect(ret.length).toBe(1); expect(ret[0]).toBe(html); done(); }); + + it("returns an empty array, if nothing was passed", (done) => { + const ret = dom.to_element_array(); + expect(ret.length).toBe(0); + expect(ret instanceof Array).toBe(true); + + done(); + }); + + it("returns only DOM Elements, no Nodes or others", (done) => { + const el = document.body; + const txt = document.createTextNode("okay"); + const ret = dom.to_element_array([1, false, txt, "okay", el]); + expect(ret.length).toBe(1); + expect(ret[0]).toBe(el); + + done(); + }); }); describe("querySelectorAllAndMe tests", () => { @@ -167,6 +282,91 @@ describe("core.dom tests", () => { done(); }); + it("Support multiple root nodes", (done) => { + const el1 = document.createElement("div"); + el1.className = "el1"; + const el2 = document.createElement("span"); + el2.className = "el2"; + const el3 = document.createElement("div"); + el3.className = "el3"; + + const ret = dom.querySelectorAllAndMe([el1, el2, el3], "div"); + const ids = ret + .map((el) => el.className) + .sort() + .join(" "); + + expect(ret.length).toBe(2); + expect(ids).toBe("el1 el3"); + + done(); + }); + + it("Support nesting", (done) => { + const el1 = document.createElement("div"); + el1.className = "el1"; + el1.innerHTML = '
'; + const el2 = document.createElement("span"); + el2.className = "el2"; + const el3 = document.createElement("div"); + el3.className = "el3"; + + const ret = dom.querySelectorAllAndMe([el1, el2, el3], "div"); + const ids = ret + .map((el) => el.className) + .sort() + .join(" "); + + expect(ret.length).toBe(3); + expect(ids).toBe("el1 el11 el3"); + + done(); + }); + + it("Return root nodes first", (done) => { + document.body.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ `; + + const roots = document.querySelectorAll(".root"); + const results = dom.querySelectorAllAndMe(roots, "span, div"); + const ids = results.map((element) => element.id).join(" "); + expect(ids).toBe("id1 id11 id12 id2 id21 id22 id31 id4 id41 id42"); + + done(); + }); + + it("Does not return the same element twice", (done) => { + const el1 = document.createElement("div"); + el1.className = "el1"; + + const ret = dom.querySelectorAllAndMe([el1, el1, el1], "div"); + const ids = ret + .map((el) => el.className) + .sort() + .join(" "); + + expect(ret.length).toBe(1); + expect(ids).toBe("el1"); + + done(); + }); + it("return empty list, if no element is passed.", (done) => { const res = dom.querySelectorAllAndMe(); expect(Array.isArray(res)).toBe(true); @@ -182,7 +382,6 @@ describe("core.dom tests", () => { done(); }); - }); describe("wrap tests", () => { @@ -549,7 +748,6 @@ describe("core.dom tests", () => { describe("is_button", () => { it("checks, if an element is a button-like element or not.", (done) => { - const button = document.createElement("button"); const button_button = document.createElement("button"); button_button.setAttribute("type", "button"); @@ -587,7 +785,6 @@ describe("core.dom tests", () => { }); }); - describe("create_from_string", () => { it("Creates a DOM element from a string", (done) => { const res = dom.create_from_string(` @@ -904,7 +1101,7 @@ describe("core.dom tests", () => { }); it("can be used to store and retrieve arbitrary data on DOM nodes.", function () { const el = document.createElement("div"); - const data = { okay() { } }; + const data = { okay() {} }; dom.set_data(el, "test_data", data); expect(dom.get_data(el, "test_data")).toBe(data); }); diff --git a/src/core/utils.js b/src/core/utils.js index 062438823..953d9d72f 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -242,8 +242,8 @@ function hasValue(el) { return false; } -const hideOrShow = (nodes, visible, options, pattern_name) => { - nodes = dom.toNodeArray(nodes); +const hideOrShow = (elements, visible, options, pattern_name) => { + elements = dom.to_element_array(elements); const transitions = { none: { hide: "hide", show: "show" }, @@ -267,7 +267,7 @@ const hideOrShow = (nodes, visible, options, pattern_name) => { }); }; - for (const el of nodes) { + for (const el of elements) { el.classList.remove("visible"); el.classList.remove("hidden"); el.classList.remove("in-progress"); @@ -415,7 +415,7 @@ function isElementInViewport(el, partial = false, offset = 0) { rec.top >= 0 && rec.left >= 0 && rec.bottom <= - (window.innerHeight || document.documentElement.clientHeight) && + (window.innerHeight || document.documentElement.clientHeight) && rec.right <= (window.innerWidth || document.documentElement.clientWidth) ); } diff --git a/src/pat/inject/inject.js b/src/pat/inject/inject.js index f94035e97..8590255c8 100644 --- a/src/pat/inject/inject.js +++ b/src/pat/inject/inject.js @@ -557,7 +557,7 @@ const inject = { // 2) getting the element to scroll to (if not "top") const scroll_target = ["top", "target"].includes(cfg.scroll) ? cfg.$target[0] - : dom.querySelectorAllAndMe($injected[0], cfg.scroll); + : dom.querySelectorAllAndMe($injected, cfg.scroll)[0]; const scroll_container = dom.find_scroll_container( scroll_target, diff --git a/src/pat/inject/inject.test.js b/src/pat/inject/inject.test.js index 3946ae6c7..83382a343 100644 --- a/src/pat/inject/inject.test.js +++ b/src/pat/inject/inject.test.js @@ -947,6 +947,89 @@ describe("pat-inject", function () { expect($div.contents().last().text()).toBe("repl"); }); + + it("9.1.13 - injects and scrolls to scroll target, if set.", async function () { + const scroll_spy = jest + .spyOn(window, "scrollTo") + .mockImplementation(() => null); + + answer(` + + +
+
+ + + `); + + document.body.innerHTML = ` + + + inject this + + + `; + + const trigger = document.querySelector(".pat-inject"); + + pattern.init($(trigger)); + await utils.timeout(1); // wait a tick for async to settle. + + trigger.click(); + await utils.timeout(1); // wait a tick for async to settle. + + expect(scroll_spy).toHaveBeenCalledTimes(1); + }); + + it("9.1.14 - injects and scrolls to scroll target with injection results as a list of jQuery nodes.", async function () { + const scroll_spy = jest + .spyOn(window, "scrollTo") + .mockImplementation(() => null); + + answer(` + + +
+
+
+
+
+ + + `); + + document.body.innerHTML = ` + + + inject this + + + `; + + const trigger = document.querySelector(".pat-inject"); + + pattern.init($(trigger)); + await utils.timeout(1); // wait a tick for async to settle. + + trigger.click(); + await utils.timeout(1); // wait a tick for async to settle. + + expect(scroll_spy).toHaveBeenCalledTimes(1); + }); }); describe("9.2 - inject on forms", function () {