diff --git a/src/core/dom.js b/src/core/dom.js
index 2dbcfa451..ee5c3480b 100644
--- a/src/core/dom.js
+++ b/src/core/dom.js
@@ -143,6 +143,63 @@ const create_from_string = (string) => {
return document.createRange().createContextualFragment(string.trim());
};
+/**
+ * Return a CSS property value for a given DOM node.
+ * For length-values, relative values are converted to pixels.
+ * Optionally parse as pixels, if applicable.
+ *
+ * Note: The element must be attached to the body to make CSS caluclations work.
+ *
+ * @param {Node} el - DOM node.
+ * @param {String} property - CSS property to query on DOM node.
+ * @param {Boolean} [as_pixels=false] - Convert value to pixels, if applicable.
+ * @param {Boolean} [as_float=false] - Convert value to float, if applicable.
+ *
+ * @returns {(String|Number)} - The CSS value to return.
+ */
+function get_css_value(el, property, as_pixels = false, as_float = false) {
+ let value = window.getComputedStyle(el).getPropertyValue(property);
+ if (as_pixels || as_float) {
+ value = parseFloat(value) || 0.0;
+ }
+ if (as_pixels && !as_float) {
+ value = parseInt(Math.round(value), 10);
+ }
+ return value;
+}
+
+/**
+ * Find a scrollable element up in the DOM tree.
+ *
+ * Note: Setting the ``overflow`` shorthand property also sets the individual overflow-y and overflow-y properties.
+ *
+ * @param {Node} el - The DOM element to start the search on.
+ * @param {String} [direction=] - Not given: Search for any scrollable element up in the DOM tree.
+ * ``x``: Search for a horizontally scrollable element.
+ * ``y``: Search for a vertically scrollable element.
+ *
+ * @returns {Node} - Return the first scrollable element.
+ * If no other element could be found, document.body would be returned.
+ */
+const find_scroll_container = (el, direction) => {
+ while (el && el !== document.body) {
+ if (!direction || direction === "y") {
+ let overflow_y = get_css_value(el, "overflow-y");
+ if (["auto", "scroll"].includes(overflow_y)) {
+ return el;
+ }
+ }
+ if (!direction || direction === "x") {
+ let overflow_x = get_css_value(el, "overflow-x");
+ if (["auto", "scroll"].includes(overflow_x)) {
+ return el;
+ }
+ }
+ el = el.parentElement;
+ }
+ return el;
+};
+
const dom = {
toNodeArray: toNodeArray,
querySelectorAllAndMe: querySelectorAllAndMe,
@@ -155,6 +212,8 @@ const dom = {
acquire_attribute: acquire_attribute,
is_visible: is_visible,
create_from_string: create_from_string,
+ get_css_value: get_css_value,
+ find_scroll_container: find_scroll_container,
add_event_listener: events.add_event_listener, // BBB export. TODO: Remove in an upcoming version.
remove_event_listener: events.remove_event_listener, // BBB export. TODO: Remove in an upcoming version.
};
diff --git a/src/core/dom.test.js b/src/core/dom.test.js
index a657eed5e..f8f9ca8b4 100644
--- a/src/core/dom.test.js
+++ b/src/core/dom.test.js
@@ -484,4 +484,203 @@ describe("core.dom tests", () => {
done();
});
});
+
+ describe("get_css_value", function () {
+ beforeEach(function () {
+ document.body.innerHTML = `
+
+
+
+
+ `;
+ });
+
+ afterEach(function () {
+ document.body.innerHTML = "";
+ });
+
+ it("Return values for CSS properties of a HTML node", function () {
+ const el1 = document.querySelector("#el1");
+ expect(dom.get_css_value(el1, "font-size")).toBe("12px");
+ expect(dom.get_css_value(el1, "font-size", true)).toBe(12);
+ expect(dom.get_css_value(el1, "position")).toBe("relative");
+ });
+
+ it("Return string, int or float, as requested.", function () {
+ const el1 = document.querySelector("#el1");
+ expect(dom.get_css_value(el1, "font-size")).toBe("12px");
+ expect(dom.get_css_value(el1, "font-size", true)).toBe(12);
+ expect(dom.get_css_value(el1, "font-size", true, true)).toBe(12.0);
+ expect(dom.get_css_value(el1, "font-size", null, true)).toBe(12.0);
+ });
+
+ it("Returns 0 for when requesting a numerical value which doesn't exist.", function () {
+ const el = document.createElement("div");
+ expect(dom.get_css_value(el, "hallo", true)).toBe(0);
+ expect(dom.get_css_value(el, "hallo", true, true)).toBe(0.0);
+ expect(dom.get_css_value(el, "hallo", null, true)).toBe(0.0);
+ });
+
+ it.skip("Return inherited values for CSS properties", function () {
+ // Missing JSDOM support for style inheritance yet. See:
+ // https://github.com/jsdom/jsdom/issues/2160
+ // https://github.com/jsdom/jsdom/pull/2668
+ // https://github.com/jsdom/jsdom/blob/master/Changelog.md
+
+ const el2 = document.querySelector("#el2");
+ expect(dom.get_css_value(el2, "font-size")).toBe("12px");
+ });
+
+ it.skip("Shorthand properties are split up", function () {
+ // Missing JSDOM support for property split yet.
+
+ const el1 = document.querySelector("#el1");
+ // ``em`` are parsed to pixel values.
+ // shorthand property sets like ``border`` are split up into their
+ // individual properties, like ``border-top-width``.
+ expect(dom.get_css_value(el1, "border-top-width")).toBe("12px");
+ expect(dom.get_css_value(el1, "border-top-style")).toBe("solid");
+ expect(dom.get_css_value(el1, "border-top-color")).toBe("rgb(0, 0, 0)");
+ });
+
+ it.skip("Values with relative units are converted to pixels", function () {
+ // Missing JSDOM support for unit conversion yet.
+
+ const el1 = document.querySelector("#el1");
+ const el2 = document.querySelector("#el2");
+ // Relative length-type values are converted to absolute pixels.
+ expect(dom.get_css_value(el1, "margin-top")).toBe("12px");
+ expect(dom.get_css_value(el1, "margin-top", true)).toBe(12);
+ expect(dom.get_css_value(el2, "margin-top", true)).toBe(0);
+ expect(dom.get_css_value(el2, "margin-bottom")).toBe("24px");
+ expect(dom.get_css_value(el2, "margin-bottom", true)).toBe(24);
+ });
+ });
+
+ describe("find_scroll_container", function () {
+ it("finds itself", function (done) {
+ document.body.innerHTML = `
+
+ `;
+ const div1 = document.querySelector("#div1");
+ const div2 = document.querySelector("#div2");
+ expect(dom.find_scroll_container(div2)).toBe(div1);
+ done();
+ });
+ it.skip("finds any scrolling direction with generic overflow property", function (done) {
+ // Skipped due to jsDOM not setting overflow-x or overflow-y when only overflow is set.
+ document.body.innerHTML = `
+