/* @flow */
import warning from 'tiny-warning'
import toCss from '../utils/toCss'
import toCssValue from '../utils/toCssValue'
import escape from '../utils/escape'
import type {
CSSStyleRule,
HTMLElementWithStyleMap,
ToCssOptions,
RuleOptions,
UpdateOptions,
Renderer as RendererInterface,
JssStyle,
JssValue,
BaseRule
} from '../types'
export class BaseStyleRule implements BaseRule {
type = 'style'
key: string
isProcessed: boolean = false
style: JssStyle
renderer: RendererInterface | null
renderable: ?Object
options: RuleOptions
constructor(key: string, style: JssStyle, options: RuleOptions) {
const {sheet, Renderer} = options
this.key = key
this.options = options
this.style = style
if (sheet) this.renderer = sheet.renderer
else if (Renderer) this.renderer = new Renderer()
}
/**
* Get or set a style property.
*/
prop(name: string, value?: JssValue, options?: UpdateOptions): this | string {
// It's a getter.
if (value === undefined) return this.style[name]
// Don't do anything if the value has not changed.
const force = options ? options.force : false
if (!force && this.style[name] === value) return this
let newValue = value
if (!options || options.process !== false) {
newValue = this.options.jss.plugins.onChangeValue(value, name, this)
}
const isEmpty = newValue == null || newValue === false
const isDefined = name in this.style
// Value is empty and wasn't defined before.
if (isEmpty && !isDefined && !force) return this
// We are going to remove this value.
const remove = isEmpty && isDefined
if (remove) delete this.style[name]
else this.style[name] = newValue
// Renderable is defined if StyleSheet option `link` is true.
if (this.renderable && this.renderer) {
if (remove) this.renderer.removeProperty(this.renderable, name)
else this.renderer.setProperty(this.renderable, name, newValue)
return this
}
const {sheet} = this.options
if (sheet && sheet.attached) {
warning(false, '[JSS] Rule is not linked. Missing sheet option "link: true".')
}
return this
}
}
export class StyleRule extends BaseStyleRule {
selectorText: string
id: ?string
renderable: ?CSSStyleRule
constructor(key: string, style: JssStyle, options: RuleOptions) {
super(key, style, options)
const {selector, scoped, sheet, generateId} = options
if (selector) {
this.selectorText = selector
} else if (scoped !== false) {
this.id = generateId(this, sheet)
this.selectorText = `.${escape(this.id)}`
}
}
/**
* Set selector string.
* Attention: use this with caution. Most browsers didn't implement
* selectorText setter, so this may result in rerendering of entire Style Sheet.
*/
set selector(selector: string): void {
if (selector === this.selectorText) return
this.selectorText = selector
const {renderer, renderable} = this
if (!renderable || !renderer) return
const hasChanged = renderer.setSelector(renderable, selector)
// If selector setter is not implemented, rerender the rule.
if (!hasChanged) {
renderer.replaceRule(renderable, this)
}
}
/**
* Get selector string.
*/
get selector(): string {
return this.selectorText
}
/**
* Apply rule to an element inline.
*/
applyTo(renderable: HTMLElementWithStyleMap): this {
const {renderer} = this
if (renderer) {
const json = this.toJSON()
for (const prop in json) {
renderer.setProperty(renderable, prop, json[prop])
}
}
return this
}
/**
* Returns JSON representation of the rule.
* Fallbacks are not supported.
* Useful for inline styles.
*/
toJSON(): Object {
const json = {}
for (const prop in this.style) {
const value = this.style[prop]
if (typeof value !== 'object') json[prop] = value
else if (Array.isArray(value)) json[prop] = toCssValue(value)
}
return json
}
/**
* Generates a CSS string.
*/
toString(options?: ToCssOptions): string {
const {sheet} = this.options
const link = sheet ? sheet.options.link : false
const opts = link ? {...options, allowEmpty: true} : options
return toCss(this.selectorText, this.style, opts)
}
}
export default {
onCreateRule(name: string, style: JssStyle, options: RuleOptions): StyleRule | null {
if (name[0] === '@' || (options.parent && options.parent.type === 'keyframes')) {
return null
}
return new StyleRule(name, style, options)
}
}