UNPKG

6.63 kBPlain TextView Raw
1import * as FreeStyle from "free-style";
2import * as types from '../types';
3
4import { convertToStyles, convertToKeyframes } from './formatting';
5import { extend, raf } from './utilities';
6
7export type StylesTarget = { textContent: string | null };
8
9/**
10 * Creates an instance of free style with our options
11 */
12const createFreeStyle = () => FreeStyle.create();
13
14/**
15 * Maintains a single stylesheet and keeps it in sync with requested styles
16 */
17export class TypeStyle {
18 private _autoGenerateTag: boolean;
19 private _freeStyle: FreeStyle.FreeStyle;
20 private _pending: number;
21 private _pendingRawChange: boolean;
22 private _raw: string;
23 private _tag?: StylesTarget;
24
25 /**
26 * We have a single stylesheet that we update as components register themselves
27 */
28 private _lastFreeStyleChangeId: number;
29
30 constructor({ autoGenerateTag }: { autoGenerateTag: boolean }) {
31 const freeStyle = createFreeStyle();
32
33 this._autoGenerateTag = autoGenerateTag;
34 this._freeStyle = freeStyle;
35 this._lastFreeStyleChangeId = freeStyle.changeId;
36 this._pending = 0;
37 this._pendingRawChange = false;
38 this._raw = '';
39 this._tag = undefined;
40
41 // rebind prototype to TypeStyle. It might be better to do a function() { return this.style.apply(this, arguments)}
42 this.style = this.style.bind(this);
43 }
44
45 /**
46 * Only calls cb all sync operations settle
47 */
48 private _afterAllSync(cb: () => void): void {
49 this._pending++;
50 const pending = this._pending;
51 raf(() => {
52 if (pending !== this._pending) {
53 return;
54 }
55 cb();
56 });
57 }
58
59 private _getTag(): StylesTarget | undefined {
60 if (this._tag) {
61 return this._tag;
62 }
63
64 if (this._autoGenerateTag) {
65 const tag = typeof window === 'undefined'
66 ? { textContent: '' }
67 : document.createElement('style');
68
69 if (typeof document !== 'undefined') {
70 document.head.appendChild(tag as any);
71 }
72 this._tag = tag;
73 return tag;
74 }
75
76 return undefined;
77 }
78
79 /** Checks if the style tag needs updating and if so queues up the change */
80 private _styleUpdated(): void {
81 const changeId = this._freeStyle.changeId;
82 const lastChangeId = this._lastFreeStyleChangeId;
83
84 if (!this._pendingRawChange && changeId === lastChangeId) {
85 return;
86 }
87
88 this._lastFreeStyleChangeId = changeId;
89 this._pendingRawChange = false;
90
91 this._afterAllSync(() => this.forceRenderStyles());
92 }
93
94 /**
95 * Insert `raw` CSS as a string. This is useful for e.g.
96 * - third party CSS that you are customizing with template strings
97 * - generating raw CSS in JavaScript
98 * - reset libraries like normalize.css that you can use without loaders
99 */
100 public cssRaw = (mustBeValidCSS: string): void => {
101 if (!mustBeValidCSS) {
102 return;
103 }
104 this._raw += mustBeValidCSS || '';
105 this._pendingRawChange = true;
106 this._styleUpdated();
107 }
108
109 /**
110 * Takes CSSProperties and registers it to a global selector (body, html, etc.)
111 */
112 public cssRule = (selector: string, ...objects: types.NestedCSSProperties[]): void => {
113 const styles = convertToStyles(extend(...objects));
114 this._freeStyle.registerRule(selector, styles);
115 this._styleUpdated();
116 return;
117 }
118
119 /**
120 * Renders styles to the singleton tag imediately
121 * NOTE: You should only call it on initial render to prevent any non CSS flash.
122 * After that it is kept sync using `requestAnimationFrame` and we haven't noticed any bad flashes.
123 **/
124 public forceRenderStyles = (): void => {
125 const target = this._getTag();
126 if (!target) {
127 return;
128 }
129 target.textContent = this.getStyles();
130 }
131
132 /**
133 * Utility function to register an @font-face
134 */
135 public fontFace = (...fontFace: types.FontFace[]): void => {
136 const freeStyle = this._freeStyle;
137 for (const face of fontFace as FreeStyle.Styles[]) {
138 freeStyle.registerRule('@font-face', face);
139 }
140 this._styleUpdated();
141 return;
142 }
143
144 /**
145 * Allows use to use the stylesheet in a node.js environment
146 */
147 public getStyles = () => {
148 return (this._raw || '') + this._freeStyle.getStyles();
149 }
150
151 /**
152 * Takes keyframes and returns a generated animationName
153 */
154 public keyframes = (frames: types.KeyFrames): string => {
155 const keyframes = convertToKeyframes(frames);
156 // TODO: replace $debugName with display name
157 const animationName = this._freeStyle.registerKeyframes(keyframes);
158 this._styleUpdated();
159 return animationName;
160 }
161
162 /**
163 * Helps with testing. Reinitializes FreeStyle + raw
164 */
165 public reinit = (): void => {
166 /** reinit freestyle */
167 const freeStyle = createFreeStyle();
168 this._freeStyle = freeStyle;
169 this._lastFreeStyleChangeId = freeStyle.changeId;
170
171 /** reinit raw */
172 this._raw = '';
173 this._pendingRawChange = false;
174
175 /** Clear any styles that were flushed */
176 const target = this._getTag();
177 if (target) {
178 target.textContent = '';
179 }
180 }
181
182 /** Sets the target tag where we write the css on style updates */
183 public setStylesTarget = (tag: StylesTarget): void => {
184 /** Clear any data in any previous tag */
185 if (this._tag) {
186 this._tag.textContent = '';
187 }
188 this._tag = tag;
189 /** This special time buffer immediately */
190 this.forceRenderStyles();
191 }
192
193 /**
194 * Takes CSSProperties and return a generated className you can use on your component
195 */
196 public style(...objects: (types.NestedCSSProperties | undefined)[]): string;
197 public style(...objects: (types.NestedCSSProperties | null | false | undefined)[]): string;
198 public style() {
199 const className = this._freeStyle.registerStyle(
200 convertToStyles(extend.apply(undefined, arguments)));
201 this._styleUpdated();
202 return className;
203 }
204
205 /**
206 * Takes an object where property names are ideal class names and property values are CSSProperties, and
207 * returns an object where property names are the same ideal class names and the property values are
208 * the actual generated class names using the ideal class name as the $debugName
209 */
210 public stylesheet = <Classes extends string>(classes: types.CSSClasses<Classes>): { [ClassName in Classes]: string} => {
211 const classNames = Object.getOwnPropertyNames(classes) as Classes[];
212 const result = {} as { [ClassName in Classes]: string};
213 for (let className of classNames) {
214 const classDef = classes[className] as types.NestedCSSProperties
215 if (classDef) {
216 classDef.$debugName = className as string
217 result[className] = this.style(classDef);
218 }
219 }
220 return result;
221 }
222}