style-mod.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. const C = "\u037c"
  2. const COUNT = typeof Symbol == "undefined" ? "__" + C : Symbol.for(C)
  3. const SET = typeof Symbol == "undefined" ? "__styleSet" + Math.floor(Math.random() * 1e8) : Symbol("styleSet")
  4. const top = typeof globalThis != "undefined" ? globalThis : typeof window != "undefined" ? window : {}
  5. // :: - Style modules encapsulate a set of CSS rules defined from
  6. // JavaScript. Their definitions are only available in a given DOM
  7. // root after it has been _mounted_ there with `StyleModule.mount`.
  8. //
  9. // Style modules should be created once and stored somewhere, as
  10. // opposed to re-creating them every time you need them. The amount of
  11. // CSS rules generated for a given DOM root is bounded by the amount
  12. // of style modules that were used. So to avoid leaking rules, don't
  13. // create these dynamically, but treat them as one-time allocations.
  14. export class StyleModule {
  15. // :: (Object<Style>, ?{finish: ?(string) → string})
  16. // Create a style module from the given spec.
  17. //
  18. // When `finish` is given, it is called on regular (non-`@`)
  19. // selectors (after `&` expansion) to compute the final selector.
  20. constructor(spec, options) {
  21. this.rules = []
  22. let {finish} = options || {}
  23. function splitSelector(selector) {
  24. return /^@/.test(selector) ? [selector] : selector.split(/,\s*/)
  25. }
  26. function render(selectors, spec, target, isKeyframes) {
  27. let local = [], isAt = /^@(\w+)\b/.exec(selectors[0]), keyframes = isAt && isAt[1] == "keyframes"
  28. if (isAt && spec == null) return target.push(selectors[0] + ";")
  29. for (let prop in spec) {
  30. let value = spec[prop]
  31. if (/&/.test(prop)) {
  32. render(prop.split(/,\s*/).map(part => selectors.map(sel => part.replace(/&/, sel))).reduce((a, b) => a.concat(b)),
  33. value, target)
  34. } else if (value && typeof value == "object") {
  35. if (!isAt) throw new RangeError("The value of a property (" + prop + ") should be a primitive value.")
  36. render(splitSelector(prop), value, local, keyframes)
  37. } else if (value != null) {
  38. local.push(prop.replace(/_.*/, "").replace(/[A-Z]/g, l => "-" + l.toLowerCase()) + ": " + value + ";")
  39. }
  40. }
  41. if (local.length || keyframes) {
  42. target.push((finish && !isAt && !isKeyframes ? selectors.map(finish) : selectors).join(", ") +
  43. " {" + local.join(" ") + "}")
  44. }
  45. }
  46. for (let prop in spec) render(splitSelector(prop), spec[prop], this.rules)
  47. }
  48. // :: () → string
  49. // Returns a string containing the module's CSS rules.
  50. getRules() { return this.rules.join("\n") }
  51. // :: () → string
  52. // Generate a new unique CSS class name.
  53. static newName() {
  54. let id = top[COUNT] || 1
  55. top[COUNT] = id + 1
  56. return C + id.toString(36)
  57. }
  58. // :: (union<Document, ShadowRoot>, union<[StyleModule], StyleModule>)
  59. //
  60. // Mount the given set of modules in the given DOM root, which ensures
  61. // that the CSS rules defined by the module are available in that
  62. // context.
  63. //
  64. // Rules are only added to the document once per root.
  65. //
  66. // Rule order will follow the order of the modules, so that rules from
  67. // modules later in the array take precedence of those from earlier
  68. // modules. If you call this function multiple times for the same root
  69. // in a way that changes the order of already mounted modules, the old
  70. // order will be changed.
  71. static mount(root, modules) {
  72. (root[SET] || new StyleSet(root)).mount(Array.isArray(modules) ? modules : [modules])
  73. }
  74. }
  75. let adoptedSet = null
  76. class StyleSet {
  77. constructor(root) {
  78. if (!root.head && root.adoptedStyleSheets && typeof CSSStyleSheet != "undefined") {
  79. if (adoptedSet) {
  80. root.adoptedStyleSheets = [adoptedSet.sheet].concat(root.adoptedStyleSheets)
  81. return root[SET] = adoptedSet
  82. }
  83. this.sheet = new CSSStyleSheet
  84. root.adoptedStyleSheets = [this.sheet].concat(root.adoptedStyleSheets)
  85. adoptedSet = this
  86. } else {
  87. this.styleTag = (root.ownerDocument || root).createElement("style")
  88. let target = root.head || root
  89. target.insertBefore(this.styleTag, target.firstChild)
  90. }
  91. this.modules = []
  92. root[SET] = this
  93. }
  94. mount(modules) {
  95. let sheet = this.sheet
  96. let pos = 0 /* Current rule offset */, j = 0 /* Index into this.modules */
  97. for (let i = 0; i < modules.length; i++) {
  98. let mod = modules[i], index = this.modules.indexOf(mod)
  99. if (index < j && index > -1) { // Ordering conflict
  100. this.modules.splice(index, 1)
  101. j--
  102. index = -1
  103. }
  104. if (index == -1) {
  105. this.modules.splice(j++, 0, mod)
  106. if (sheet) for (let k = 0; k < mod.rules.length; k++)
  107. sheet.insertRule(mod.rules[k], pos++)
  108. } else {
  109. while (j < index) pos += this.modules[j++].rules.length
  110. pos += mod.rules.length
  111. j++
  112. }
  113. }
  114. if (!sheet) {
  115. let text = ""
  116. for (let i = 0; i < this.modules.length; i++)
  117. text += this.modules[i].getRules() + "\n"
  118. this.styleTag.textContent = text
  119. }
  120. }
  121. }
  122. // Style::Object<union<Style,string>>
  123. //
  124. // A style is an object that, in the simple case, maps CSS property
  125. // names to strings holding their values, as in `{color: "red",
  126. // fontWeight: "bold"}`. The property names can be given in
  127. // camel-case—the library will insert a dash before capital letters
  128. // when converting them to CSS.
  129. //
  130. // If you include an underscore in a property name, it and everything
  131. // after it will be removed from the output, which can be useful when
  132. // providing a property multiple times, for browser compatibility
  133. // reasons.
  134. //
  135. // A property in a style object can also be a sub-selector, which
  136. // extends the current context to add a pseudo-selector or a child
  137. // selector. Such a property should contain a `&` character, which
  138. // will be replaced by the current selector. For example `{"&:before":
  139. // {content: '"hi"'}}`. Sub-selectors and regular properties can
  140. // freely be mixed in a given object. Any property containing a `&` is
  141. // assumed to be a sub-selector.
  142. //
  143. // Finally, a property can specify an @-block to be wrapped around the
  144. // styles defined inside the object that's the property's value. For
  145. // example to create a media query you can do `{"@media screen and
  146. // (min-width: 400px)": {...}}`.