reactive-di
Version:
Reactive dependency injection
203 lines (172 loc) • 6.04 kB
JavaScript
// @flow
import type {ISheet, ISheetManager} from './interfaces'
interface JSSSRule {
key: string;
}
interface JSSSheet {
classes: {+[id: string]: string};
options: {
meta: string;
classNamePrefix: string;
};
attach(): void;
addRule(name?: string, data: Object): JSSSRule;
getRule(name: string): JSSSRule;
deleteRule(name: string): void;
}
interface JSS {
createStyleSheet(cssObj?: Object, options: any): JSSSheet;
removeStyleSheet(sheet: JSSSheet): void;
}
interface IOwner {
update(sheet: JssSheet): void;
}
const subRulesId = Symbol('rdi_sub_rules')
export const nameId = Symbol('rdi_theme_name')
const ruleId = Symbol('rdi_rule_id')
const BAD_CLASS_SYMBOLS = new RegExp('[^\\w\\d\\-\\_]', 'g')
const KEYFRAMES = '@keyframes '
function replaceProps<V: {[id: string]: string | Object}>(obj: V, placeholder: string, replaceTo: string): V {
for (let key in obj) {
const val = obj[key]
if (val && key.indexOf('animation') === 0) {
obj[key] = typeof val === 'object'
? replaceProps(val, placeholder, replaceTo)
: val.replace(placeholder, replaceTo)
}
}
return obj
}
class JssSheet implements ISheet {
jssSheet: JSSSheet
key: string
usageCount = 0
_scheduled = false
_owner: IOwner
constructor(jssSheet: JSSSheet, owner: IOwner, key: string) {
this.jssSheet = jssSheet
this._owner = owner
this.key = key
}
_lastAttached: Set<JSSSRule> = new Set()
_toDelete: Set<JSSSRule> = new Set()
addRule<V: {
[typeof ruleId | string]: string | void
}>(css: V, debugName?: string): string {
const sheet = this.jssSheet
let id = css[ruleId]
let rule: JSSSRule | void = undefined
if (id === undefined) {
id = css[ruleId] = (css[nameId] || JSON.stringify(css))
.replace(BAD_CLASS_SYMBOLS, '')
rule = sheet.getRule(id)
if (rule && css._dynamic === true) {
rule = undefined
}
} else {
rule = sheet.getRule(id)
}
if (!rule) {
const prefix = sheet.options.classNamePrefix
const placeholder = '@.'
const newCss = {}
let subRules: JSSSRule[] | void = undefined
for (let key in css) {
const item = css[key]
if (item === true) continue // _dynamic
if (key.indexOf(KEYFRAMES) === 0 && typeof item === 'object') {
if (subRules === undefined) subRules = []
subRules.push(sheet.addRule(key.replace(placeholder, prefix), item))
} else {
newCss[key] = item !== null && typeof item === 'object'
? replaceProps(item, placeholder, prefix)
: key.indexOf('animation') === 0 ? (item: any).replace(placeholder, prefix) : item
}
}
rule = sheet.addRule(id, newCss)
if (subRules) (rule: Object)[subRulesId] = subRules
if (!this._scheduled) this._owner.update(this)
this._scheduled = true
}
this._lastAttached.add(rule)
this._toDelete.delete(rule)
return sheet.classes[id]
}
_deleteRule(rule: JSSSRule) {
const sheet = this.jssSheet
const subRules: JSSSRule[] = (rule: Object)[subRulesId]
if (subRules !== undefined) {
for (let i = 0; i < subRules.length; i++) {
sheet.deleteRule(subRules[i].key)
}
}
sheet.deleteRule(rule.key)
}
attach() {
this._scheduled = false
this._toDelete.forEach(this._deleteRule, this)
this._toDelete = this._lastAttached
this._lastAttached = new Set()
this.jssSheet.attach()
}
destructor() {
this.usageCount--
if (this.usageCount !== 0) return
this._toDelete = (undefined: any)
this._lastAttached = (undefined: any)
this._owner.update(this)
}
}
function defaultSchedule(cb: () => void) {
setTimeout(cb, 16)
}
export default class JssSheetManager implements ISheetManager {
_jss: JSS
_cache: Map<string, JssSheet> = new Map()
_badClassSymbols = BAD_CLASS_SYMBOLS
_schedule: (cb: () => void) => void
constructor(jss: any, schedule?: (cb: () => void) => void = defaultSchedule) {
this._jss = jss
this._schedule = schedule
}
static createGenerateClassName(): (a: any, b: any) => string {
return function generateClassName(rule: {+key?: string}, sheet?: {options: {classNamePrefix: string}}): string {
return `${sheet ? sheet.options.classNamePrefix : ''}${rule.key ? `-${rule.key}` : ''}`
}
}
createSheet(sheetKey: string): ISheet {
let sheet = this._cache.get(sheetKey)
if (sheet === undefined) {
const meta = sheetKey.replace(this._badClassSymbols, '')
const options = {
meta,
classNamePrefix: meta
}
sheet = new JssSheet(this._jss.createStyleSheet(undefined, options), this, sheetKey)
this._cache.set(sheetKey, sheet)
}
sheet.usageCount++
return sheet
}
_scheduled: JssSheet[] = []
update(sheet: JssSheet) {
const {_scheduled: scheduled} = this
if (scheduled.length === 0) {
this._schedule(this._sync)
}
scheduled.push(sheet)
}
_sync = () => {
const {_scheduled: scheduled, _cache: cache, _jss: jss} = this
for (let i = 0; i < scheduled.length; i++) {
const sheet = scheduled[i]
if (sheet.usageCount === 0) {
cache.delete(sheet.key)
jss.removeStyleSheet(sheet.jssSheet)
} else {
sheet.attach()
}
}
this._scheduled = []
}
}