All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jsMain.styled.sheets.CSSOMSheet.kt Maven / Gradle / Ivy

There is a newer version: 1.2.4-pre.855
Show newest version
package styled.sheets

import js.core.asList
import js.core.globalThis
import web.html.HTMLStyleElement
import web.timers.requestIdleCallback
import web.timers.setTimeout

private typealias Rules = Iterable

internal enum class RemoveMode { OnBrowserIdle, Instantly }

/**
 * A stylesheet that is injected using the CSSOM API.
 * Removes unused styled after [cleanTimeout].
 * Useful in production mode when application have a lot of pages and creates a lot of different css for these pages.
 */
internal class CSSOMSheet(
    val type: RuleType,
    val removeMode: RemoveMode = RemoveMode.OnBrowserIdle,
    private var cleanTimeout: Int = 30000,
    maxRulesPerSheet: Int? = DEFAULT_MAX_RULES_PER_SHEET
) : AbstractSheet(type, maxRulesPerSheet) {
    private val groups = LinkedHashMap()
    internal val scheduledGroups = LinkedHashMap()

    private var groupId: Int = 0
        get() = field.also { field++ }

    private var isCleanRequested: Boolean = false

    override fun scheduleToInject(rules: Rules): Int = groupId.also { scheduledGroups[it] = rules }

    fun requestClean(clean: () -> Unit) {
        if (isCleanRequested) return
        isCleanRequested = true
        if (removeMode == RemoveMode.Instantly) {
            isCleanRequested = false
            clean()
            return
        }
        setTimeout({
            if (!!globalThis.requestIdleCallback && removeMode == RemoveMode.OnBrowserIdle) {
                requestIdleCallback {
                    isCleanRequested = false
                    clean()
                }
            } else {
                isCleanRequested = false
                clean()
            }
        }, cleanTimeout)
    }

    fun removeGroups(groupIds: List) {
        for (groupId in groupIds) {
            if (groupId in scheduledGroups) {
                scheduledGroups.remove(groupId)
                continue
            }

            val removedGroup = groups.remove(groupId) ?: throw IllegalArgumentException("Group $groupId does not exist")

            if (!removedGroup.rulesRange.isEmpty()) {
                val sheet = removedGroup.element.cssSheet
                removedGroup.rulesRange.reversed().forEach {
                    sheet.deleteRule(it)
                }

                if (sheet.cssRules.length == 0) {
                    removedGroup.element.removeAndCleanUp()
                }
            }

            val rulesShift = removedGroup.rulesRange.last - removedGroup.rulesRange.first + 1

            for (entry in groups) {
                val (otherGroupId, otherGroup) = entry
                if (otherGroup.element == removedGroup.element && otherGroupId > groupId) {
                    val otherRange = otherGroup.rulesRange
                    val shiftedRange = (otherRange.first - rulesShift)..(otherRange.last - rulesShift)
                    entry.setValue(otherGroup.copy(rulesRange = shiftedRange))
                }
            }
        }

        compressSheets()
    }

    override fun injectScheduled() {
        if (scheduledGroups.isNotEmpty()) {
            for ((groupId, rules) in scheduledGroups) {
                val element = getCurrentStyleElement(rules.count())
                val ruleStart = element.cssSheet.cssRules.length
                var ruleId = ruleStart
                for (rule in rules) {
                    try {
                        element.cssSheet.insertRule(rule, ruleId)
                        ruleId++
                    } catch (e: Throwable) {
                        /* Browser does not support the rule */
                    }
                }
                groups[groupId] = RulesGroup(element, ruleStart until ruleId)
            }
            scheduledGroups.clear()
        }
    }

    /**
     * Combines successive style sheets with less than [maxRulesPerSheet] rules in total into one style sheet.
     * It preserves rules order and updates group mapping.
     */
    private fun compressSheets() {
        val maxRulesPerSheet = maxRulesPerSheet ?: return

        // Find successive groups of style elements that can be combined
        val elementGroups = mutableListOf>()
        var currentMergeGroup = mutableListOf()
        var totalRulesCount = 0
        for (element in usedStyleElements) {
            val rulesLength = element.cssSheet.cssRules.length
            if (rulesLength + totalRulesCount > maxRulesPerSheet) {
                if (currentMergeGroup.size > 1) {
                    elementGroups += currentMergeGroup
                }
                totalRulesCount = 0
                currentMergeGroup = mutableListOf()
            }
            totalRulesCount += rulesLength
            currentMergeGroup.add(element)
        }
        if (currentMergeGroup.size > 1) {
            elementGroups += currentMergeGroup
        }

        class RulesGroupUpdate(val element: HTMLStyleElement, val shift: Int)

        // Move all rules in a group to the first style element and delete other elements.
        val groupUpdates = mutableMapOf()
        elementGroups.forEach { elements ->
            val reused = elements.first()
            elements.drop(1).forEach { mergedSheet ->
                groupUpdates[mergedSheet] = RulesGroupUpdate(reused, reused.cssSheet.cssRules.length)
                mergedSheet.cssSheet.cssRules.asList().forEach {
                    reused.cssSheet.insertRule(it.cssText, reused.cssSheet.cssRules.length)
                }
                mergedSheet.removeAndCleanUp()
            }
        }

        // Update ranges of moved rules
        for (entry in groups) {
            val otherGroup = entry.value
            groupUpdates[otherGroup.element]?.let {
                val otherRange = otherGroup.rulesRange
                val shiftedRange = (otherRange.first + it.shift)..(otherRange.last + it.shift)
                entry.setValue(RulesGroup(element = it.element, rulesRange = shiftedRange))
            }
        }
    }

    override fun clear() {
        super.clear()
        groups.clear()
        scheduledGroups.clear()
    }

    private data class RulesGroup(val element: HTMLStyleElement, val rulesRange: IntRange)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy