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 = ` +