/* @flow */
import warning from 'tiny-warning'
import RuleList from '../RuleList'
import type {
CSSKeyframesRule,
JssStyle,
RuleOptions,
ToCssOptions,
ContainerRule,
KeyframesMap,
Plugin
} from '../types'
import escape from '../utils/escape'
const defaultToStringOptions = {
indent: 1,
children: true
}
const nameRegExp = /@keyframes\s+([\w-]+)/
/**
* Rule for @keyframes
*/
export class KeyframesRule implements ContainerRule {
type = 'keyframes'
at: string = '@keyframes'
key: string
name: string
id: string
rules: RuleList
options: RuleOptions
isProcessed: boolean = false
renderable: ?CSSKeyframesRule
constructor(key: string, frames: Object, options: RuleOptions) {
const nameMatch = key.match(nameRegExp)
if (nameMatch && nameMatch[1]) {
this.name = nameMatch[1]
} else {
this.name = 'noname'
warning(false, `[JSS] Bad keyframes name ${key}`)
}
this.key = `${this.type}-${this.name}`
this.options = options
const {scoped, sheet, generateId} = options
this.id = scoped === false ? this.name : escape(generateId(this, sheet))
this.rules = new RuleList({...options, parent: this})
for (const name in frames) {
this.rules.add(name, frames[name], {
...options,
parent: this
})
}
this.rules.process()
}
/**
* Generates a CSS string.
*/
toString(options?: ToCssOptions = defaultToStringOptions): string {
if (options.indent == null) options.indent = defaultToStringOptions.indent
if (options.children == null) options.children = defaultToStringOptions.children
if (options.children === false) {
return `${this.at} ${this.id} {}`
}
let children = this.rules.toString(options)
if (children) children = `\n${children}\n`
return `${this.at} ${this.id} {${children}}`
}
}
const keyRegExp = /@keyframes\s+/
const refRegExp = /\$([\w-]+)/g
const findReferencedKeyframe = (val, keyframes) => {
if (typeof val === 'string') {
return val.replace(refRegExp, (match, name) => {
if (name in keyframes) {
return keyframes[name]
}
warning(false, `[JSS] Referenced keyframes rule "${name}" is not defined.`)
return match
})
}
return val
}
/**
* Replace the reference for a animation name.
*/
const replaceRef = (style: JssStyle, prop: string, keyframes: KeyframesMap) => {
const value = style[prop]
const refKeyframe = findReferencedKeyframe(value, keyframes)
if (refKeyframe !== value) {
style[prop] = refKeyframe
}
}
const plugin: Plugin = {
onCreateRule(key, frames, options) {
return typeof key === 'string' && keyRegExp.test(key)
? new KeyframesRule(key, frames, options)
: null
},
// Animation name ref replacer.
onProcessStyle: (style, rule, sheet) => {
if (rule.type !== 'style' || !sheet) return style
if ('animation-name' in style) replaceRef(style, 'animation-name', sheet.keyframes)
if ('animation' in style) replaceRef(style, 'animation', sheet.keyframes)
return style
},
onChangeValue(val, prop, rule) {
const {sheet} = rule.options
if (!sheet) {
return val
}
switch (prop) {
case 'animation':
return findReferencedKeyframe(val, sheet.keyframes)
case 'animation-name':
return findReferencedKeyframe(val, sheet.keyframes)
default:
return val
}
}
}
export default plugin