/* @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) } }