diff --git a/examples/dynaymicCSS.tsx b/examples/dynaymicCSS.tsx new file mode 100644 index 00000000..866cacce --- /dev/null +++ b/examples/dynaymicCSS.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { updateCSS, removeCSS } from '../src/Dom/dynamicCSS'; +import type { Prepend } from '../src/Dom/dynamicCSS'; + +function injectStyle(id: number, prepend?: Prepend) { + const randomColor = Math.floor(Math.random() * 16777215).toString(16); + + updateCSS(`body { background: #${randomColor} }`, `style-${id}`, { + prepend, + }); +} + +export default () => { + const [id, setId] = React.useState(0); + const idRef = React.useRef(id); + idRef.current = id; + + // Clean up + React.useEffect(() => { + return () => { + for (let i = 0; i <= idRef.current; i += 1) { + removeCSS(`style-${i}`); + } + }; + }, []); + + return ( + <> + + + + + ); +}; diff --git a/package.json b/package.json index 34eab9cf..d97911a3 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@types/jest": "^25.2.3", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", + "@types/responselike": "^1.0.0", "@types/shallowequal": "^1.1.1", "@types/warning": "^3.0.0", "@umijs/fabric": "^2.0.8", @@ -41,7 +42,7 @@ "create-react-class": "^15.6.3", "cross-env": "^7.0.2", "eslint": "^6.6.0", - "father": "^2.14.0", + "father": "^2.29.9", "np": "^6.2.3", "react": "^18.0.0", "react-dom": "^18.0.0", diff --git a/src/Dom/dynamicCSS.ts b/src/Dom/dynamicCSS.ts index 186733e4..e88726bc 100644 --- a/src/Dom/dynamicCSS.ts +++ b/src/Dom/dynamicCSS.ts @@ -1,11 +1,17 @@ import canUseDom from './canUseDom'; +const APPEND_ORDER = '_rc_util_order'; const MARK_KEY = `rc-util-key`; +const containerCache = new Map(); + +export type Prepend = boolean | 'queue'; +export type AppendType = 'prependQueue' | 'append' | 'prepend'; + interface Options { attachTo?: Element; csp?: { nonce?: string }; - prepend?: boolean; + prepend?: Prepend; mark?: string; } @@ -25,25 +31,60 @@ function getContainer(option: Options) { return head || document.body; } +function getOrder(prepend?: Prepend): AppendType { + if (prepend === 'queue') { + return 'prependQueue'; + } + + return prepend ? 'prepend' : 'append'; +} + +/** + * Find style which inject by rc-util + */ +function findStyles(container: Element) { + return Array.from( + (containerCache.get(container) || container).children, + ).filter( + node => node.tagName === 'STYLE' && node[APPEND_ORDER], + ) as HTMLStyleElement[]; +} + export function injectCSS(css: string, option: Options = {}) { if (!canUseDom()) { return null; } + const { csp, prepend } = option; + const styleNode = document.createElement('style'); - if (option.csp?.nonce) { - styleNode.nonce = option.csp?.nonce; + styleNode[APPEND_ORDER] = getOrder(prepend); + + if (csp?.nonce) { + styleNode.nonce = csp?.nonce; } styleNode.innerHTML = css; const container = getContainer(option); const { firstChild } = container; - if (option.prepend && container.prepend) { - // Use `prepend` first - container.prepend(styleNode); - } else if (option.prepend && firstChild) { - // Fallback to `insertBefore` like IE not support `prepend` + if (prepend) { + // If is queue `prepend`, it will prepend first style and then append rest style + if (prepend === 'queue') { + const existStyle = findStyles(container).filter(node => + ['prepend', 'prependQueue'].includes(node[APPEND_ORDER]), + ); + if (existStyle.length) { + container.insertBefore( + styleNode, + existStyle[existStyle.length - 1].nextSibling, + ); + + return styleNode; + } + } + + // Use `insertBefore` as `prepend` container.insertBefore(styleNode, firstChild); } else { container.appendChild(styleNode); @@ -52,15 +93,12 @@ export function injectCSS(css: string, option: Options = {}) { return styleNode; } -const containerCache = new Map(); - function findExistNode(key: string, option: Options = {}) { const container = getContainer(option); - return Array.from(containerCache.get(container).children).find( - node => - node.tagName === 'STYLE' && node.getAttribute(getMark(option)) === key, - ) as HTMLStyleElement; + return findStyles(container).find( + node => node.getAttribute(getMark(option)) === key, + ); } export function removeCSS(key: string, option: Options = {}) { diff --git a/tests/dynamicCSS.test.tsx b/tests/dynamicCSS.test.tsx index 738114c2..ff793440 100644 --- a/tests/dynamicCSS.test.tsx +++ b/tests/dynamicCSS.test.tsx @@ -54,6 +54,33 @@ describe('dynamicCSS', () => { head.prepend = originPrepend; }); + + it('prepend with queue', () => { + const head = document.querySelector('head'); + + const styles = [ + injectCSS(TEST_STYLE, { prepend: 'queue' }), + injectCSS(TEST_STYLE, { prepend: 'queue' }), + ]; + + const styleNodes = Array.from(head.querySelectorAll('style')); + expect(styleNodes).toHaveLength(2); + + for (let i = 0; i < styleNodes.length; i += 1) { + expect(styles[i]).toBe(styleNodes[i]); + } + + // Should not after append + const appendStyle = injectCSS(TEST_STYLE); + const prependStyle = injectCSS(TEST_STYLE, { prepend: 'queue' }); + const nextStyleNodes = Array.from(head.querySelectorAll('style')); + + expect(nextStyleNodes).toHaveLength(4); + expect(nextStyleNodes[0]).toBe(styles[0]); + expect(nextStyleNodes[1]).toBe(styles[1]); + expect(nextStyleNodes[2]).toBe(prependStyle); + expect(nextStyleNodes[3]).toBe(appendStyle); + }); }); });