Skip to content

Commit

Permalink
feat: Shadow DOM
Browse files Browse the repository at this point in the history
  • Loading branch information
Marshal27 committed Oct 18, 2022
1 parent 314c5da commit b3fb604
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 2 deletions.
142 changes: 140 additions & 2 deletions src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ type LayoutState = {

const INSTANCE_SYMBOL: unique symbol = Symbol('CQ_INSTANCE');
const STYLESHEET_SYMBOL: unique symbol = Symbol('CQ_STYLESHEET');
const SHADOW_SYMBOL: unique symbol = Symbol('CQ_SHADOW');
const SUPPORTS_SMALL_VIEWPORT_UNITS = CSS.supports('width: 1svh');
const VERTICAL_WRITING_MODES = new Set([
'vertical-lr',
Expand Down Expand Up @@ -179,7 +180,7 @@ export function initializePolyfill() {

const dummyElement = document.createElement(`cq-polyfill-${PER_RUN_UID}`);
const globalStyleElement = document.createElement('style');
const mutationObserver = new MutationObserver(mutations => {
const createMutationObserver = (): MutationObserver => new MutationObserver(mutations => {
for (const entry of mutations) {
for (const node of entry.removedNodes) {
const instance = getInstance(node);
Expand Down Expand Up @@ -210,13 +211,50 @@ export function initializePolyfill() {
scheduleUpdate();
}
});
const mutationObserver = createMutationObserver();
mutationObserver.observe(documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeOldValue: true,
});

const originalAttachShadow = Element.prototype.attachShadow;
Element.prototype.attachShadow = function (options) {
const shadow = originalAttachShadow.apply(this, [options]);
getOrCreateInstance(shadow);
return shadow;
}

const originalReplaceSync = CSSStyleSheet.prototype.replaceSync;
if (originalReplaceSync) {
CSSStyleSheet.prototype.replaceSync = function (options) {
const result = transpileStyleSheet(
options,
undefined
);
setDescriptorsForStyleSheet(this, result.descriptors);
const replaceSync = originalReplaceSync.apply(this, [result.source]);
return replaceSync;
}
}

const originalReplace = CSSStyleSheet.prototype.replace;
if (originalReplace) {
CSSStyleSheet.prototype.replace = function (options) {
const result = transpileStyleSheet(
options,
undefined
);
setDescriptorsForStyleSheet(this, result.descriptors);
const replace = originalReplace.apply(this, [result.source]);
return replace;
}
}

// TODO: implement monkeypatch for CSSStyleSheet.prototype.insertRule and CSSStyleSheet.prototype.deleteRule
// as they currently are not handled.

const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
const instance = getOrCreateInstance(entry.target);
Expand Down Expand Up @@ -360,7 +398,7 @@ export function initializePolyfill() {
function getOrCreateInstance(node: Node): Instance {
let instance = getInstance(node);
if (!instance) {
let innerController: NodeController<Node>;
let innerController: NodeController<Node|ShadowRoot>;
let stateProvider: LayoutStateProvider | null = null;
let alwaysObserveSize = false;

Expand All @@ -381,6 +419,8 @@ export function initializePolyfill() {
...options,
}),
});
} else if (node instanceof ShadowRoot) {
innerController = new ShadowRootController(node, createMutationObserver());
} else if (node instanceof HTMLStyleElement) {
innerController = new StyleElementController(node, {
registerStyleSheet: options =>
Expand Down Expand Up @@ -416,6 +456,26 @@ export function initializePolyfill() {
];
};

const updateShadowHostState: (
node: ShadowRoot,
state: LayoutState
) => void = (node, state) => {
const rootQueryDescriptors: ContainerConditionEntry[] = [];
for (const adoptedStyleSheet of node.adoptedStyleSheets) {
if (adoptedStyleSheet) {
for (const query of getDescriptorsForStyleSheet(adoptedStyleSheet)) {
rootQueryDescriptors.push([
new Reference(query),
QueryContainerFlags.None,
]);
}
}
}
if (rootQueryDescriptors.length) {
state.conditions = rootQueryDescriptors;
}
};

const inlineStyles =
node instanceof HTMLElement || node instanceof SVGElement
? node.style
Expand Down Expand Up @@ -517,6 +577,12 @@ export function initializePolyfill() {
for (const child of node.childNodes) {
getOrCreateInstance(child).update(currentState);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const shadow = (node as any)[SHADOW_SYMBOL];
if (shadow) {
updateShadowHostState(shadow, currentState);
getOrCreateInstance(shadow).update(currentState);
}
},

resize() {
Expand All @@ -525,9 +591,15 @@ export function initializePolyfill() {

mutate() {
cacheKey = Symbol();
innerController.mutated();
for (const child of node.childNodes) {
getOrCreateInstance(child).mutate();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const shadow = (node as any)[SHADOW_SYMBOL];
if (shadow) {
getOrCreateInstance(shadow).mutate();
}
},
};

Expand Down Expand Up @@ -560,6 +632,10 @@ class NodeController<T extends Node> {
updated() {
// Handler implemented by subclasses
}

mutated() {
// Handler implemented by subclasses
}
}

class LinkElementController extends NodeController<HTMLLinkElement> {
Expand Down Expand Up @@ -644,6 +720,68 @@ class StyleElementController extends NodeController<HTMLStyleElement> {
}
}

class ShadowRootController extends NodeController<ShadowRoot> {
private controller: AbortController | null = null;
private mo: MutationObserver;
private host: HTMLElement;

constructor(node: ShadowRoot, mo: MutationObserver) {
super(node);
this.mo = mo;
this.host = node.host as HTMLElement;
}

connected(): void {
this.mo?.observe(this.node, {
childList: true,
subtree: true,
attributes: true,
attributeOldValue: true,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.host as any)[SHADOW_SYMBOL] = this.node;
}

updated() {
this.processCssRules();
}

mutated() {
this.processCssRules();
}

disconnected(): void {
this.controller?.abort();
this.controller = null;
this.mo?.disconnect();
}

private processCssRules(): void {
let setProp = false;
for (const adoptedStyleSheet of this.node.adoptedStyleSheets) {
for (const rule of adoptedStyleSheet.cssRules) {
if (rule instanceof CSSStyleRule) {
const value = rule.style.getPropertyValue(CUSTOM_PROPERTY_TYPE).trim();
if (value) {
const selector: string = rule.selectorText.replace(':host', this.host.localName);
// handle parentheses on :host([attribute])
const mutatedSelector = !selector.includes(':') ? selector.replace('(', '').replace(')', '') : selector;
// if match apply rule
if (this.host.matches(mutatedSelector)) {
this.host.style.setProperty(CUSTOM_PROPERTY_TYPE, value);
setProp = true;
break;
}
}
}
}
}
if (!setProp && this.host.style.getPropertyValue(CUSTOM_PROPERTY_TYPE)) {
this.host.style.removeProperty(CUSTOM_PROPERTY_TYPE);
}
}
}

class GlobalStyleElementController extends NodeController<HTMLStyleElement> {
connected(): void {
const style = `* { ${CUSTOM_PROPERTY_TYPE}: cq-normal; ${CUSTOM_PROPERTY_NAME}: cq-none; }`;
Expand Down
7 changes: 7 additions & 0 deletions src/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,10 @@

declare const IS_WPT_BUILD: boolean;
declare const PACKAGE_VERSION: string;
interface ShadowRoot {
adoptedStyleSheets: CSSStyleSheet[];
}
interface StyleSheet {
replace(text: string): Promise<CSSStyleSheet>;
replaceSync(text: string): void;
}

0 comments on commit b3fb604

Please sign in to comment.