Skip to content

Commit

Permalink
fix: solve the methods application issue
Browse files Browse the repository at this point in the history
To apply method calls of the main CSSStyleSheet to all adopters we need them to have style elements with CSSStyleSheet object attached. It means that we cannot work with disconnected elements, and the idea to work from inside the setter of the adoptedStyleSheets setter does not fit.

That's why we need to track the already connected elements via the MutationObserver and NodeIterator (to catch nested nodes).
  • Loading branch information
Lodin committed Aug 25, 2019
1 parent 688d963 commit 2d9f601
Showing 1 changed file with 76 additions and 34 deletions.
110 changes: 76 additions & 34 deletions adoptedStyleSheets.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,7 @@
}
}

OldCSSStyleSheet.prototype.replace = ConstructStyleSheet.prototype.replace;
OldCSSStyleSheet.prototype.replaceSync =
ConstructStyleSheet.prototype.replaceSync;

const adoptStyleSheets = (location, sheets, observer) => {
const adoptStyleSheets = (location, sheets = [], observer) => {
const newStyles = document.createDocumentFragment();
const justCreated = new Map();

Expand All @@ -147,7 +143,7 @@
if (adoptedStyleElement) {
// This operation removes the style element from the location, so we
// need to pause watching when it happens to avoid calling
// restoreStylesOnMutationCallback.
// adoptAndRestoreStylesOnMutationCallback.
observer.disconnect();
newStyles.append(adoptedStyleElement);
observer.observe();
Expand Down Expand Up @@ -175,30 +171,81 @@
}
};

// When any style is removed, we need to re-adopt all the styles because
// otherwise we can break the order of appended styles which will affect the
// rules overriding.
const restoreStylesOnMutationCallback = mutations => {
for (const {removedNodes} of mutations) {
const adoptAndRestoreStylesOnMutationCallback = mutations => {
for (const {addedNodes, removedNodes} of mutations) {
// When any style is removed, we need to re-adopt all the styles because
// otherwise we can break the order of appended styles which will affect the
// rules overriding.
for (const removedNode of removedNodes) {
const location = removedNode[$location];

if (location) {
adoptStyleSheets(
location,
location.adoptedStyleSheets,
location[$adoptedStyleSheets],
location[$observer],
);
break;
}
}

// When the new custom element is added in the observing location, we need
// to adopt its style sheets. However, Mutation Observer can track only
// the top level of children while we need to catch each custom element
// no matter how it is nested. To go through the nodes we use the
// NodeIterator.
for (const addedNode of addedNodes) {
const iter = document.createNodeIterator(
addedNode,
NodeFilter.SHOW_ELEMENT,
({shadowRoot}) =>
shadowRoot && shadowRoot.adoptedStyleSheets.length > 0
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT,
);

let node;

while ((node = iter.nextNode())) {
const {shadowRoot} = node;

adoptStyleSheets(
shadowRoot,
shadowRoot[$adoptedStyleSheets],
shadowRoot[$observer],
);
}
}
}
};

const createObserver = location => {
const observer = new MutationObserver(
adoptAndRestoreStylesOnMutationCallback,
);

location[$observer] = {
observe: () =>
observer.observe(location, {childList: true, subtree: true}),
disconnect: () => observer.disconnect(),
};

location[$observer].observe();
};

// Document body will be observed from the very start to catch all added
// custom elements
createObserver(document.body);

const adoptedStyleSheetAccessors = {
configurable: true,
get() {
return this[$adoptedStyleSheets] || [];
// Technically, the real adoptedStyleSheets array is placed on the body
// element to unify the logic with ShadowRoot. However, it is hidden under
// the symbol, and the public interface follows the specification.
const location = this.body ? this.body : this;

return location[$adoptedStyleSheets] || [];
},
set(sheets) {
if (!Array.isArray(sheets)) {
Expand All @@ -216,18 +263,9 @@
const location = this.body ? this.body : this;
const uniqueSheets = [...new Set(sheets)];

if (!this[$adoptedStyleSheets]) {
const observer = new MutationObserver(restoreStylesOnMutationCallback);

this[$observer] = {
observe: () => observer.observe(this, {childList: true}),
disconnect: () => observer.disconnect(),
};

this[$observer].observe();
} else {
if (location[$adoptedStyleSheets]) {
// Remove all the sheets the received array does not include.
for (const sheet of this[$adoptedStyleSheets]) {
for (const sheet of location[$adoptedStyleSheets]) {
if (uniqueSheets.includes(sheet)) {
continue;
}
Expand All @@ -236,21 +274,25 @@
location,
);

this[$observer].disconnect();
location[$observer].disconnect();
styleElement.remove();
this[$observer].observe();
location[$observer].observe();
}
} else if (location instanceof ShadowRoot) {
// Observer for document.body is already launched
createObserver(location);
}

this[$adoptedStyleSheets] = uniqueSheets;
location[$adoptedStyleSheets] = uniqueSheets;

// With this style elements will be appended even before the element is
// connected to the DOM and become unremovable due to
// restoreStylesOnMutationCallback.
//
// It should not harm the developer experience, but will help to catch
// each custom element, no matter how nested it is.
adoptStyleSheets(location, this[$adoptedStyleSheets], this[$observer]);
// Element can adopt style sheets only when it is connected
if (location.isConnected) {
adoptStyleSheets(
location,
location[$adoptedStyleSheets],
location[$observer],
);
}
},
};

Expand Down

0 comments on commit 2d9f601

Please sign in to comment.