Skip to content

Commit

Permalink
feat: new Function - updateCSS
Browse files Browse the repository at this point in the history
  • Loading branch information
GreatAuk committed Aug 16, 2023
1 parent 04a4ebd commit e6d7747
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 0 deletions.
192 changes: 192 additions & 0 deletions packages/dom/src/dynamicCSS.ts
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
}
3 changes: 3 additions & 0 deletions packages/dom/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export * from './panzoom'
export * from './canUseDom'
export * from './domContains'
export * from './dynamicCSS'
export * from './isAndroid'
export * from './isIOS'
export * from './isMobile'
Expand Down

0 comments on commit e6d7747

Please sign in to comment.