Skip to content

Commit

Permalink
Cache element styles for getComputedStyle()
Browse files Browse the repository at this point in the history
  • Loading branch information
jsnajdr authored and domenic committed Jan 22, 2023
1 parent 980c6f6 commit 709f33a
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 46 deletions.
31 changes: 8 additions & 23 deletions lib/jsdom/browser/Window.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const reportException = require("../living/helpers/runtime-script-errors");
const { getCurrentEventHandlerValue } = require("../living/helpers/create-event-accessor.js");
const { fireAnEvent } = require("../living/helpers/events");
const SessionHistory = require("../living/window/SessionHistory");
const { forEachMatchingSheetRuleOfElement, getResolvedValue, propertiesWithResolvedValueImplemented,
const { getDeclarationForElement, getResolvedValue, propertiesWithResolvedValueImplemented,
SHADOW_DOM_PSEUDO_REGEXP } = require("../living/helpers/style-rules.js");
const CustomElementRegistry = require("../living/generated/CustomElementRegistry");
const jsGlobals = require("./js-globals.json");
Expand Down Expand Up @@ -835,28 +835,13 @@ function Window(options) {
const declaration = new CSSStyleDeclaration();
const { forEach } = Array.prototype;

function handleProperty(style, property) {
const value = style.getPropertyValue(property);
// https://drafts.csswg.org/css-cascade-4/#valdef-all-unset
if (value === "unset") {
declaration.removeProperty(property);
} else {
declaration.setProperty(
property,
value,
style.getPropertyPriority(property)
);
}
}

forEachMatchingSheetRuleOfElement(elt, rule => {
forEach.call(rule.style, property => {
handleProperty(rule.style, property);
});
});

forEach.call(elt.style, property => {
handleProperty(elt.style, property);
const elementDeclaration = getDeclarationForElement(elt);
forEach.call(elementDeclaration, property => {
declaration.setProperty(
property,
elementDeclaration.getPropertyValue(property),
elementDeclaration.getPropertyPriority(property)
);
});

// https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle
Expand Down
73 changes: 51 additions & 22 deletions lib/jsdom/living/helpers/style-rules.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";
const cssom = require("cssom");
const { CSSStyleDeclaration } = require("cssstyle");
const defaultStyleSheet = require("../../browser/default-stylesheet");
const { matchesDontThrow } = require("./selectors");

Expand All @@ -21,7 +22,7 @@ exports.propertiesWithResolvedValueImplemented = {
}
};

exports.forEachMatchingSheetRuleOfElement = (elementImpl, handleRule) => {
function forEachMatchingSheetRuleOfElement(elementImpl, handleRule) {
function handleSheet(sheet) {
forEach.call(sheet.cssRules, rule => {
if (rule.media) {
Expand All @@ -44,6 +45,54 @@ exports.forEachMatchingSheetRuleOfElement = (elementImpl, handleRule) => {

handleSheet(parsedDefaultStyleSheet);
forEach.call(elementImpl._ownerDocument.styleSheets._list, handleSheet);
}

exports.invalidateStyleCache = elementImpl => {
if (elementImpl._attached) {
elementImpl._ownerDocument._styleCache = null;
}
};

exports.getDeclarationForElement = elementImpl => {
let styleCache = elementImpl._ownerDocument._styleCache;
if (!styleCache) {
styleCache = elementImpl._ownerDocument._styleCache = new WeakMap();
}

const cachedDeclaration = styleCache.get(elementImpl);
if (cachedDeclaration) {
return cachedDeclaration;
}

const declaration = new CSSStyleDeclaration();

function handleProperty(style, property) {
const value = style.getPropertyValue(property);
// https://drafts.csswg.org/css-cascade-4/#valdef-all-unset
if (value === "unset") {
declaration.removeProperty(property);
} else {
declaration.setProperty(
property,
value,
style.getPropertyPriority(property)
);
}
}

forEachMatchingSheetRuleOfElement(elementImpl, rule => {
forEach.call(rule.style, property => {
handleProperty(rule.style, property);
});
});

forEach.call(elementImpl.style, property => {
handleProperty(elementImpl.style, property);
});

styleCache.set(elementImpl, declaration);

return declaration;
};

function matches(rule, element) {
Expand All @@ -57,27 +106,7 @@ function matches(rule, element) {
// rules appear. The last rule is the most specific while the first rule is
// the least specific.
function getCascadedPropertyValue(element, property) {
let value = "";

exports.forEachMatchingSheetRuleOfElement(element, rule => {
const propertyValue = rule.style.getPropertyValue(property);
// https://drafts.csswg.org/css-cascade-4/#valdef-all-unset
if (propertyValue === "unset") {
value = "";
} else if (propertyValue !== "") { // getPropertyValue returns "" if the property is not found
value = propertyValue;
}
});

const inlineValue = element.style.getPropertyValue(property);
// https://drafts.csswg.org/css-cascade-4/#valdef-all-unset
if (inlineValue === "unset") {
value = "";
} else if (inlineValue !== "" && inlineValue !== null) {
value = inlineValue;
}

return value;
return exports.getDeclarationForElement(element).getPropertyValue(property);
}

// https://drafts.csswg.org/css-cascade-4/#specified-value
Expand Down
5 changes: 5 additions & 0 deletions lib/jsdom/living/helpers/stylesheets.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const cssom = require("cssom");
const whatwgEncoding = require("whatwg-encoding");
const whatwgURL = require("whatwg-url");
const { invalidateStyleCache } = require("./style-rules");

// TODO: this should really implement https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet
// It (and the things it calls) is nowhere close right now.
Expand All @@ -18,6 +19,8 @@ exports.removeStylesheet = (sheet, elementImpl) => {
// Remove the association explicitly; in the spec it's implicit so this step doesn't exist.
elementImpl.sheet = null;

invalidateStyleCache(elementImpl);

// TODO: "Set the CSS style sheet’s parent CSS style sheet, owner node and owner CSS rule to null."
// Probably when we have a real CSSOM implementation.
};
Expand Down Expand Up @@ -52,6 +55,8 @@ function addStylesheet(sheet, elementImpl) {
// Set the association explicitly; in the spec it's implicit.
elementImpl.sheet = sheet;

invalidateStyleCache(elementImpl);

// TODO: title and disabled stuff
}

Expand Down
3 changes: 3 additions & 0 deletions lib/jsdom/living/nodes/Document-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ class DocumentImpl extends NodeImpl {

// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter
this._throwOnDynamicMarkupInsertionCounter = 0;

// Cache of computed element styles
this._styleCache = null;
}

_getTheParent(event) {
Expand Down
4 changes: 3 additions & 1 deletion lib/jsdom/living/nodes/Node-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const {
isShadowRoot, shadowIncludingRoot, assignSlot, assignSlotableForTree, assignSlotable, signalSlotChange, isSlot,
shadowIncludingInclusiveDescendantsIterator, shadowIncludingDescendantsIterator
} = require("../helpers/shadow-dom");
const { invalidateStyleCache } = require("../helpers/style-rules");

function nodeEquals(a, b) {
if (a.nodeType !== b.nodeType) {
Expand Down Expand Up @@ -225,10 +226,11 @@ class NodeImpl extends EventTargetImpl {
this._childNodesList._update();
}
this._clearMemoizedQueries();
invalidateStyleCache(this);
}

_childTextContentChangeSteps() {
// Default: do nothing
invalidateStyleCache(this);
}

_clearMemoizedQueries() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Updating the document must invalidate the style cache and change the results of getComputedStyle</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<style>
.my-link {
font-size: 1.0em;
}

.my-link[href] {
font-size: 1.1em;
}

.my-link[href="#a"] {
font-size: 1.2em;
}

.my-link[href="#b"] {
font-size: 1.3em;
}

.my-box ~ .my-link {
font-size: 1.4em;
}

.my-box.bigger ~ .my-link {
font-size: 1.5em;
}

.my-link:empty {
font-size: 1.6em;
}
</style>

<a class="my-link">Hello world</a>
<script>
"use strict";
const link = document.querySelector(".my-link");

test(() => {
assert_equals(getComputedStyle(link).fontSize, "1.0em");
}, "Initial style value");

test(() => {
link.href = "#x"; // appendAttribute

assert_equals(getComputedStyle(link).fontSize, "1.1em");
}, "Assigning to attribute updates the computed style");

test(() => {
link.setAttribute("href", "#a"); // changeAttribute

assert_equals(getComputedStyle(link).fontSize, "1.2em");
}, "Setting an attribute updates the computed style");

test(() => {
const attr = document.createAttribute("href");
attr.value = "#b";
link.attributes.setNamedItem(attr); // replaceAttribute

assert_equals(getComputedStyle(link).fontSize, "1.3em");
}, "Replacing an attribute updates the computed style");

test(() => {
link.removeAttribute("href"); // removeAttribute

assert_equals(getComputedStyle(link).fontSize, "1.0em");
}, "Removing an attribute updates the computed style");

test(() => {
const box = document.createElement("div");
box.className = "my-box";
link.before(box);

assert_equals(getComputedStyle(link).fontSize, "1.4em");
}, "Inserting an element updates the computed style");

test(() => {
const biggerBox = document.createElement("div");
biggerBox.className = "my-box bigger";
document.querySelector(".my-box").replaceWith(biggerBox);

assert_equals(getComputedStyle(link).fontSize, "1.5em");
}, "Replacing an element updates the computed style");

test(() => {
const box = document.querySelector(".my-box");
document.body.removeChild(box);

assert_equals(getComputedStyle(link).fontSize, "1.0em");
}, "Removing an element updates the computed style");

test(() => {
link.textContent = "";
// setting character data of a text node should also make the element :empty, but
// nwsapi doesn't implement this correctly yet. To update the computed styles,
// style cache needs to be invalidated also after `CharacterDataImpl.replaceData`.
// link.childNodes[0].data = "";

assert_equals(getComputedStyle(link).fontSize, "1.6em");
}, "Emptying an element updates the computed style");
</script>

0 comments on commit 709f33a

Please sign in to comment.