-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
195 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
import { canUseDom } from './canUseDom' | ||
import { domContains } from './domContains' | ||
|
||
const APPEND_ORDER = 'data-rc-order' | ||
const APPEND_PRIORITY = 'data-rc-priority' | ||
const MARK_KEY = 'rc-util-key' | ||
|
||
const containerCache = new Map<ContainerType, Node & ParentNode>() | ||
|
||
export type ContainerType = Element | ShadowRoot | ||
export type Prepend = boolean | 'queue' | ||
export type AppendType = 'prependQueue' | 'append' | 'prepend' | ||
|
||
interface Options { | ||
attachTo?: ContainerType | ||
csp?: { nonce?: string } | ||
prepend?: Prepend | ||
/** | ||
* Config the `priority` of `prependQueue`. Default is `0`. | ||
* It's useful if you need to insert style before other style. | ||
*/ | ||
priority?: number | ||
mark?: string | ||
} | ||
|
||
function getMark({ mark }: Options = {}) { | ||
if (mark) | ||
return mark.startsWith('data-') ? mark : `data-${mark}` | ||
|
||
return MARK_KEY | ||
} | ||
|
||
function getContainer(option: Options) { | ||
if (option.attachTo) | ||
return option.attachTo | ||
|
||
const head = document.querySelector('head') | ||
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: ContainerType) { | ||
return Array.from( | ||
(containerCache.get(container) || container).children, | ||
).filter(node => node.tagName === 'STYLE') as HTMLStyleElement[] | ||
} | ||
|
||
export function injectCSS(css: string, option: Options = {}) { | ||
if (!canUseDom()) | ||
return null | ||
|
||
const { csp, prepend, priority = 0 } = option | ||
const mergedOrder = getOrder(prepend) | ||
const isPrependQueue = mergedOrder === 'prependQueue' | ||
|
||
const styleNode = document.createElement('style') | ||
styleNode.setAttribute(APPEND_ORDER, mergedOrder) | ||
|
||
if (isPrependQueue && priority) | ||
styleNode.setAttribute(APPEND_PRIORITY, `${priority}`) | ||
|
||
if (csp?.nonce) | ||
styleNode.nonce = csp?.nonce | ||
|
||
styleNode.innerHTML = css | ||
|
||
const container = getContainer(option) | ||
const { firstChild } = container | ||
|
||
if (prepend) { | ||
// If is queue `prepend`, it will prepend first style and then append rest style | ||
if (isPrependQueue) { | ||
const existStyle = findStyles(container).filter((node) => { | ||
// Ignore style which not injected by rc-util with prepend | ||
if ( | ||
!['prepend', 'prependQueue'].includes(node.getAttribute(APPEND_ORDER)!) | ||
) | ||
return false | ||
|
||
// Ignore style which priority less then new style | ||
const nodePriority = Number(node.getAttribute(APPEND_PRIORITY) || 0) | ||
return priority >= nodePriority | ||
}) | ||
|
||
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) | ||
} | ||
|
||
return styleNode | ||
} | ||
|
||
function findExistNode(key: string, option: Options = {}) { | ||
const container = getContainer(option) | ||
|
||
return findStyles(container).find( | ||
node => node.getAttribute(getMark(option)) === key, | ||
) | ||
} | ||
|
||
export function removeCSS(key: string, option: Options = {}) { | ||
const existNode = findExistNode(key, option) | ||
if (existNode) { | ||
const container = getContainer(option) | ||
container.removeChild(existNode) | ||
} | ||
} | ||
|
||
/** | ||
* qiankun will inject `appendChild` to insert into other | ||
*/ | ||
function syncRealContainer(container: ContainerType, option: Options) { | ||
const cachedRealContainer = containerCache.get(container) | ||
|
||
// Find real container when not cached or cached container removed | ||
if (!cachedRealContainer || !domContains(document, cachedRealContainer)) { | ||
const placeholderStyle = injectCSS('', option) | ||
if (!placeholderStyle) | ||
return | ||
const { parentNode } = placeholderStyle | ||
if (!parentNode) | ||
return | ||
containerCache.set(container, parentNode) | ||
container.removeChild(placeholderStyle) | ||
} | ||
} | ||
|
||
/** | ||
* manually clear container cache to avoid global cache in unit testes | ||
*/ | ||
export function clearContainerCache() { | ||
containerCache.clear() | ||
} | ||
|
||
/** | ||
* Update CSS text in style element | ||
* @param css CSS text | ||
* @param key style key | ||
* @param option Options | ||
* @returns style element | ||
* @fork https://github.com/react-component/util/blob/master/src/Dom/dynamicCSS.ts | ||
* @linkcode https://github.com/GreatAuk/utopia-utils/blob/main/packages/dom/src/dynamicCSS.ts | ||
* @example | ||
* ```js | ||
* updateCSS('body { color: red }', 'my-style') | ||
* ``` | ||
* */ | ||
export function updateCSS(css: string, key: string, option: Options = {}) { | ||
const container = getContainer(option) | ||
|
||
// Sync real parent | ||
syncRealContainer(container, option) | ||
|
||
const existNode = findExistNode(key, option) | ||
|
||
if (existNode) { | ||
if (option.csp?.nonce && existNode.nonce !== option.csp?.nonce) | ||
existNode.nonce = option.csp?.nonce | ||
|
||
if (existNode.innerHTML !== css) | ||
existNode.innerHTML = css | ||
|
||
return existNode | ||
} | ||
|
||
const newNode = injectCSS(css, option) | ||
if (!newNode) | ||
return | ||
newNode.setAttribute(getMark(option), key) | ||
return newNode | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters