typestyle
Version:
223 lines (193 loc) • 6.63 kB
text/typescript
import * as FreeStyle from "free-style";
import * as types from '../types';
import { convertToStyles, convertToKeyframes } from './formatting';
import { extend, raf } from './utilities';
export type StylesTarget = { textContent: string | null };
/**
* Creates an instance of free style with our options
*/
const createFreeStyle = () => FreeStyle.create();
/**
* Maintains a single stylesheet and keeps it in sync with requested styles
*/
export class TypeStyle {
private _autoGenerateTag: boolean;
private _freeStyle: FreeStyle.FreeStyle;
private _pending: number;
private _pendingRawChange: boolean;
private _raw: string;
private _tag?: StylesTarget;
/**
* We have a single stylesheet that we update as components register themselves
*/
private _lastFreeStyleChangeId: number;
constructor({ autoGenerateTag }: { autoGenerateTag: boolean }) {
const freeStyle = createFreeStyle();
this._autoGenerateTag = autoGenerateTag;
this._freeStyle = freeStyle;
this._lastFreeStyleChangeId = freeStyle.changeId;
this._pending = 0;
this._pendingRawChange = false;
this._raw = '';
this._tag = undefined;
// rebind prototype to TypeStyle. It might be better to do a function() { return this.style.apply(this, arguments)}
this.style = this.style.bind(this);
}
/**
* Only calls cb all sync operations settle
*/
private _afterAllSync(cb: () => void): void {
this._pending++;
const pending = this._pending;
raf(() => {
if (pending !== this._pending) {
return;
}
cb();
});
}
private _getTag(): StylesTarget | undefined {
if (this._tag) {
return this._tag;
}
if (this._autoGenerateTag) {
const tag = typeof window === 'undefined'
? { textContent: '' }
: document.createElement('style');
if (typeof document !== 'undefined') {
document.head.appendChild(tag as any);
}
this._tag = tag;
return tag;
}
return undefined;
}
/** Checks if the style tag needs updating and if so queues up the change */
private _styleUpdated(): void {
const changeId = this._freeStyle.changeId;
const lastChangeId = this._lastFreeStyleChangeId;
if (!this._pendingRawChange && changeId === lastChangeId) {
return;
}
this._lastFreeStyleChangeId = changeId;
this._pendingRawChange = false;
this._afterAllSync(() => this.forceRenderStyles());
}
/**
* Insert `raw` CSS as a string. This is useful for e.g.
* - third party CSS that you are customizing with template strings
* - generating raw CSS in JavaScript
* - reset libraries like normalize.css that you can use without loaders
*/
public cssRaw = (mustBeValidCSS: string): void => {
if (!mustBeValidCSS) {
return;
}
this._raw += mustBeValidCSS || '';
this._pendingRawChange = true;
this._styleUpdated();
}
/**
* Takes CSSProperties and registers it to a global selector (body, html, etc.)
*/
public cssRule = (selector: string, ...objects: types.NestedCSSProperties[]): void => {
const styles = convertToStyles(extend(...objects));
this._freeStyle.registerRule(selector, styles);
this._styleUpdated();
return;
}
/**
* Renders styles to the singleton tag imediately
* NOTE: You should only call it on initial render to prevent any non CSS flash.
* After that it is kept sync using `requestAnimationFrame` and we haven't noticed any bad flashes.
**/
public forceRenderStyles = (): void => {
const target = this._getTag();
if (!target) {
return;
}
target.textContent = this.getStyles();
}
/**
* Utility function to register an @font-face
*/
public fontFace = (...fontFace: types.FontFace[]): void => {
const freeStyle = this._freeStyle;
for (const face of fontFace as FreeStyle.Styles[]) {
freeStyle.registerRule('@font-face', face);
}
this._styleUpdated();
return;
}
/**
* Allows use to use the stylesheet in a node.js environment
*/
public getStyles = () => {
return (this._raw || '') + this._freeStyle.getStyles();
}
/**
* Takes keyframes and returns a generated animationName
*/
public keyframes = (frames: types.KeyFrames): string => {
const keyframes = convertToKeyframes(frames);
// TODO: replace $debugName with display name
const animationName = this._freeStyle.registerKeyframes(keyframes);
this._styleUpdated();
return animationName;
}
/**
* Helps with testing. Reinitializes FreeStyle + raw
*/
public reinit = (): void => {
/** reinit freestyle */
const freeStyle = createFreeStyle();
this._freeStyle = freeStyle;
this._lastFreeStyleChangeId = freeStyle.changeId;
/** reinit raw */
this._raw = '';
this._pendingRawChange = false;
/** Clear any styles that were flushed */
const target = this._getTag();
if (target) {
target.textContent = '';
}
}
/** Sets the target tag where we write the css on style updates */
public setStylesTarget = (tag: StylesTarget): void => {
/** Clear any data in any previous tag */
if (this._tag) {
this._tag.textContent = '';
}
this._tag = tag;
/** This special time buffer immediately */
this.forceRenderStyles();
}
/**
* Takes CSSProperties and return a generated className you can use on your component
*/
public style(...objects: (types.NestedCSSProperties | undefined)[]): string;
public style(...objects: (types.NestedCSSProperties | null | false | undefined)[]): string;
public style() {
const className = this._freeStyle.registerStyle(
convertToStyles(extend.apply(undefined, arguments)));
this._styleUpdated();
return className;
}
/**
* Takes an object where property names are ideal class names and property values are CSSProperties, and
* returns an object where property names are the same ideal class names and the property values are
* the actual generated class names using the ideal class name as the $debugName
*/
public stylesheet = <Classes extends string>(classes: types.CSSClasses<Classes>): { [ClassName in Classes]: string} => {
const classNames = Object.getOwnPropertyNames(classes) as Classes[];
const result = {} as { [ClassName in Classes]: string};
for (let className of classNames) {
const classDef = classes[className] as types.NestedCSSProperties
if (classDef) {
classDef.$debugName = className as string
result[className] = this.style(classDef);
}
}
return result;
}
}