/* @flow */
import createRule from './utils/createRule'
import {StyleRule, KeyframesRule} from './plugins/index'
import type {
RuleListOptions,
ToCssOptions,
Rule,
RuleOptions,
JssStyle,
Classes,
KeyframesMap,
UpdateArguments
} from './types'
import escape from './utils/escape'
const defaultUpdateOptions = {
process: true
}
const forceUpdateOptions = {
force: true,
process: true
}
/**
* Contains rules objects and allows adding/removing etc.
* Is used for e.g. by `StyleSheet` or `ConditionalRule`.
*/
export default class RuleList {
// Rules registry for access by .get() method.
// It contains the same rule registered by name and by selector.
map: {[key: string]: Rule} = {}
// Original styles object.
raw: {[key: string]: JssStyle} = {}
// Used to ensure correct rules order.
index: Array<Rule> = []
options: RuleListOptions
classes: Classes
keyframes: KeyframesMap
constructor(options: RuleListOptions) {
this.options = options
this.classes = options.classes
this.keyframes = options.keyframes
}
/**
* Create and register rule.
*
* Will not render after Style Sheet was rendered the first time.
*/
add(key: string, decl: JssStyle, ruleOptions?: RuleOptions): Rule | null {
const {parent, sheet, jss, Renderer, generateId, scoped} = this.options
const options = {
classes: this.classes,
parent,
sheet,
jss,
Renderer,
generateId,
scoped,
...ruleOptions
}
// We need to save the original decl before creating the rule
// because cache plugin needs to use it as a key to return a cached rule.
this.raw[key] = decl
if (key in this.classes) {
// For e.g. rules inside of @media container
options.selector = `.${escape(this.classes[key])}`
}
const rule = createRule(key, decl, options)
if (!rule) return null
this.register(rule)
const index = options.index === undefined ? this.index.length : options.index
this.index.splice(index, 0, rule)
return rule
}
/**
* Get a rule.
*/
get(name: string): Rule {
return this.map[name]
}
/**
* Delete a rule.
*/
remove(rule: Rule): void {
this.unregister(rule)
delete this.raw[rule.key]
this.index.splice(this.indexOf(rule), 1)
}
/**
* Get index of a rule.
*/
indexOf(rule: Rule): number {
return this.index.indexOf(rule)
}
/**
* Run `onProcessRule()` plugins on every rule.
*/
process(): void {
const {plugins} = this.options.jss
// We need to clone array because if we modify the index somewhere else during a loop
// we end up with very hard-to-track-down side effects.
this.index.slice(0).forEach(plugins.onProcessRule, plugins)
}
/**
* Register a rule in `.map` and `.classes` maps.
*/
register(rule: Rule): void {
this.map[rule.key] = rule
if (rule instanceof StyleRule) {
this.map[rule.selector] = rule
if (rule.id) this.classes[rule.key] = rule.id
} else if (rule instanceof KeyframesRule && this.keyframes) {
this.keyframes[rule.name] = rule.id
}
}
/**
* Unregister a rule.
*/
unregister(rule: Rule): void {
delete this.map[rule.key]
if (rule instanceof StyleRule) {
delete this.map[rule.selector]
delete this.classes[rule.key]
} else if (rule instanceof KeyframesRule) {
delete this.keyframes[rule.name]
}
}
/**
* Update the function values with a new data.
*/
update(...args: UpdateArguments): void {
let name
let data
let options
if (typeof args[0] === 'string') {
name = args[0]
// $FlowFixMe
data = args[1]
// $FlowFixMe
options = args[2]
} else {
data = args[0]
// $FlowFixMe
options = args[1]
name = null
}
if (name) {
this.onUpdate(data, this.get(name), options)
} else {
for (let index = 0; index < this.index.length; index++) {
this.onUpdate(data, this.index[index], options)
}
}
}
/**
* Execute plugins, update rule props.
*/
onUpdate(data: Object, rule: Rule, options?: Object = defaultUpdateOptions) {
const {
jss: {plugins},
sheet
} = this.options
// It is a rules container like for e.g. ConditionalRule.
if (rule.rules instanceof RuleList) {
rule.rules.update(data, options)
return
}
const styleRule: StyleRule = (rule: any)
const {style} = styleRule
plugins.onUpdate(data, rule, sheet, options)
// We rely on a new `style` ref in case it was mutated during onUpdate hook.
if (options.process && style && style !== styleRule.style) {
// We need to run the plugins in case new `style` relies on syntax plugins.
plugins.onProcessStyle(styleRule.style, styleRule, sheet)
// Update and add props.
for (const prop in styleRule.style) {
const nextValue = styleRule.style[prop]
const prevValue = style[prop]
// We need to use `force: true` because `rule.style` has been updated during onUpdate hook, so `rule.prop()` will not update the CSSOM rule.
// We do this comparison to avoid unneeded `rule.prop()` calls, since we have the old `style` object here.
if (nextValue !== prevValue) {
styleRule.prop(prop, nextValue, forceUpdateOptions)
}
}
// Remove props.
for (const prop in style) {
const nextValue = styleRule.style[prop]
const prevValue = style[prop]
// We need to use `force: true` because `rule.style` has been updated during onUpdate hook, so `rule.prop()` will not update the CSSOM rule.
// We do this comparison to avoid unneeded `rule.prop()` calls, since we have the old `style` object here.
if (nextValue == null && nextValue !== prevValue) {
styleRule.prop(prop, null, forceUpdateOptions)
}
}
}
}
/**
* Convert rules to a CSS string.
*/
toString(options?: ToCssOptions): string {
let str = ''
const {sheet} = this.options
const link = sheet ? sheet.options.link : false
for (let index = 0; index < this.index.length; index++) {
const rule = this.index[index]
const css = rule.toString(options)
// No need to render an empty rule.
if (!css && !link) continue
if (str) str += '\n'
str += css
}
return str
}
}