import React, { useContext, useMemo } from 'react'; import * as sc from 'styled-components'; import { ComponentType, ShadowPortal, StylesInjector } from './types.js'; import { useShadowDom } from './hooks.js'; interface StyledComponentsInjectorOptions { disableCSSOMInjection?: boolean; disableVendorPrefixes?: boolean; stylisPlugins?: sc.StyleSheetManagerProps['stylisPlugins']; } // A lot of this code copypasted from styled-components source and modified to work with multiple targets const SPLITTER = '/*!sc*/\n'; const makeGroupedTag = (tag: any) => { return new DefaultGroupedTag(tag); }; const BASE_SIZE = 1 << 9; const DefaultGroupedTag = class DefaultGroupedTag { groupSizes: Uint32Array; length: number; tag: any; constructor(tag: any) { this.groupSizes = new Uint32Array(BASE_SIZE); this.length = BASE_SIZE; this.tag = tag; } indexOfGroup(group: number) { let index = 0; for (let i = 0; i < group; i++) { index += this.groupSizes[i]!; } return index; } insertRules(group: number, rules: string[]) { if (group >= this.groupSizes.length) { const oldBuffer = this.groupSizes; const oldSize = oldBuffer.length; let newSize = oldSize; while (group >= newSize) { newSize <<= 1; if (newSize < 0) { throw new Error(`Styled components error with code: 16, arg: ${group}`); } } this.groupSizes = new Uint32Array(newSize); this.groupSizes.set(oldBuffer); this.length = newSize; for (let i = oldSize; i < newSize; i++) { this.groupSizes[i] = 0; } } let ruleIndex = this.indexOfGroup(group + 1); for (let i = 0, l = rules.length; i < l; i++) { if (this.tag.insertRule(ruleIndex, rules[i])) { this.groupSizes[group]++; ruleIndex++; } } } clearGroup(group: number) { if (group < this.length) { const length = this.groupSizes[group]!; const startIndex = this.indexOfGroup(group); const endIndex = startIndex + length; this.groupSizes[group] = 0; for (let i = startIndex; i < endIndex; i++) { this.tag.deleteRule(startIndex); } } } getGroup(group: number) { let css = ''; if (group >= this.length || this.groupSizes[group] === 0) { return css; } const length = this.groupSizes[group]!; const startIndex = this.indexOfGroup(group); const endIndex = startIndex + length; for (let i = startIndex; i < endIndex; i++) { css += `${this.tag.getRule(i)}${SPLITTER}`; } return css; } }; function getNonce() { // @ts-ignore return typeof __webpack_nonce__ !== 'undefined' ? __webpack_nonce__ : null; } const SC_ATTR: string = // @ts-ignore (typeof process !== 'undefined' && // @ts-ignore typeof process.env !== 'undefined' && // @ts-ignore (process.env.REACT_APP_SC_ATTR || process.env.SC_ATTR)) || 'data-styled'; const SC_ATTR_ACTIVE = 'active'; const findLastStyleTag = (target: HTMLElement): void | HTMLStyleElement => { const arr = Array.from(target.querySelectorAll(`style[${SC_ATTR}]`)); return arr[arr.length - 1]; } const makeStyleTag = (target?: HTMLElement | undefined): HTMLStyleElement => { const head = document.head; const parent = target || head; const style = document.createElement('style'); const prevStyle = findLastStyleTag(parent); const nextSibling = prevStyle !== undefined ? prevStyle.nextSibling : null; style.setAttribute(SC_ATTR, SC_ATTR_ACTIVE); const nonce = getNonce(); if (nonce) style.setAttribute('nonce', nonce); parent.insertBefore(style, nextSibling); return style; }; const getSheet = (tag: HTMLStyleElement): CSSStyleSheet => { if (tag.sheet) { return tag.sheet as any as CSSStyleSheet; } // Avoid Firefox quirk where the style element might not have a sheet property const { styleSheets } = document; for (let i = 0, l = styleSheets.length; i < l; i++) { const sheet = styleSheets[i]!; if (sheet.ownerNode === tag) { return sheet as any as CSSStyleSheet; } } throw new Error('Styled components error with code 17'); } const makeTag = ({ isServer, useCSSOMInjection, target }: { isServer: boolean, useCSSOMInjection: boolean, target: HTMLElement[] }) => { if (isServer) { return new VirtualTag(target[0]); } else if (useCSSOMInjection) { return new CSSOMTag(target); } else { return new TextTag(target); } }; const CSSOMTag = class CSSOMTag { elements: HTMLStyleElement[]; sheets: CSSStyleSheet[]; length: number; constructor(targets: (HTMLElement | undefined)[]) { this.elements = (targets || [undefined]).map(target => makeStyleTag(target)); // Avoid Edge bug where empty style elements don't create sheets this.elements.forEach(element => element.appendChild(document.createTextNode(''))); this.sheets = this.elements.map(element => getSheet(element)); this.length = 0; } insertRule(index: number, rule: string): boolean { try { this.sheets.forEach(sheet => sheet.insertRule(rule, index)); this.length++; return true; } catch (_error) { return false; } } deleteRule(index: number): void { this.sheets.forEach(sheet => sheet.deleteRule(index)); this.length--; } getRule(index: number): string { const rule = this.sheets[0]!.cssRules[index]; // Avoid IE11 quirk where cssText is inaccessible on some invalid rules if (rule && rule.cssText) { return rule.cssText; } else { return ''; } } }; /** A Tag that emulates the CSSStyleSheet API but uses text nodes */ const TextTag = class TextTag { elements: HTMLStyleElement[]; nodesMatrix: Array>; length: number; constructor(targets: (HTMLElement | undefined)[]) { this.elements = (targets || [undefined]).map(target => makeStyleTag(target)); this.nodesMatrix = this.elements.map(el => el.childNodes); this.length = 0; } insertRule(index: number, rule: string) { if (index <= this.length && index >= 0) { this.nodesMatrix.forEach((ln, li) => { const node = document.createTextNode(rule); const refNode = ln[index]; this.elements[li]!.insertBefore(node, refNode || null); }) this.length++; return true; } else { return false; } } deleteRule(index: number) { this.elements.forEach((el, i) => { el.removeChild(this.nodesMatrix[i]![index]!); }); this.length--; } getRule(index: number) { if (index < this.length) { return this.nodesMatrix[0]![index]!.textContent as string; } else { return ''; } } }; /** A completely virtual (server-side) Tag that doesn't manipulate the DOM */ const VirtualTag = class VirtualTag { rules: string[]; length: number; constructor(_target?: HTMLElement | undefined) { this.rules = []; this.length = 0; } insertRule(index: number, rule: string) { if (index <= this.length) { this.rules.splice(index, 0, rule); this.length++; return true; } else { return false; } } deleteRule(index: number) { this.rules.splice(index, 1); this.length--; } getRule(index: number) { if (index < this.length) { return this.rules[index]; } else { return ''; } } }; // @ts-ignore class CustomStylesheet extends sc.__PRIVATE__.StyleSheet { getTag() { // @ts-ignore return this.tag || (this.tag = makeGroupedTag(makeTag(this.options))); } reconstructWithOptions(options: any, withNames = true) { // @ts-ignore return new CustomStylesheet({ ...this.options, ...options }, this.gs, (withNames && this.names) || undefined); } } export default (options: StyledComponentsInjectorOptions = {}): StylesInjector => { return

( Component: ComponentType

, shadowHost: HTMLElement, shadowRoot: ShadowRoot, mountingInto: HTMLDivElement, stylesWrapper: HTMLDivElement ) => { const { disableCSSOMInjection = false, disableVendorPrefixes = false, stylisPlugins = [] } = options; return (props: P) => { const { insideShadowDom, connectedPortals } = useShadowDom(); // @ts-ignore const sheet = useMemo(() => new CustomStylesheet({target: [stylesWrapper, ...connectedPortals.map((p: ShadowPortal) => p.stylesWrapper)]}), [connectedPortals]); return ( // @ts-ignore ); }; }; };