1 | import * as JSS from 'jss';
|
2 | import jssPresetDefault from 'jss-preset-default';
|
3 | import { Signal } from '@lumino/signaling';
|
4 |
|
5 | import { NotebookPanel } from '@jupyterlab/notebook';
|
6 |
|
7 | import { ROOT, IFontFaceOptions } from '.';
|
8 |
|
9 | import * as SCHEMA from './schema';
|
10 |
|
11 | export class Stylist {
|
12 | fonts = new Map<string, IFontFaceOptions>();
|
13 |
|
14 | private _globalStyles: HTMLStyleElement;
|
15 | private _notebookStyles = new Map<NotebookPanel, HTMLStyleElement>();
|
16 | private _jss = JSS.create(jssPresetDefault());
|
17 | private _fontCache = new Map<string, SCHEMA.IFontFacePrimitive[]>();
|
18 | private _cacheUpdated = new Signal<this, void>(this);
|
19 |
|
20 | constructor() {
|
21 | this._globalStyles = document.createElement('style');
|
22 | }
|
23 | get cacheUpdated() {
|
24 | return this._cacheUpdated;
|
25 | }
|
26 |
|
27 | registerNotebook(notebook: NotebookPanel, register: boolean) {
|
28 | if (register) {
|
29 | this._notebookStyles.set(notebook, document.createElement('style'));
|
30 | notebook.disposed.connect(this._onDisposed, this);
|
31 | this.hack();
|
32 | } else {
|
33 | this._onDisposed(notebook);
|
34 | }
|
35 | }
|
36 |
|
37 | private _onDisposed(notebook: NotebookPanel) {
|
38 | if (this._notebookStyles.has(notebook)) {
|
39 | this._notebookStyles.get(notebook)?.remove();
|
40 | this._notebookStyles.delete(notebook);
|
41 | notebook.disposed.disconnect(this._onDisposed, this);
|
42 | }
|
43 | }
|
44 |
|
45 | get stylesheets() {
|
46 | return [this._globalStyles, ...Array.from(this._notebookStyles.values())];
|
47 | }
|
48 |
|
49 | notebooks() {
|
50 | return Array.from(this._notebookStyles.keys());
|
51 | }
|
52 |
|
53 | stylesheet(meta: SCHEMA.ISettings, notebook?: NotebookPanel, clear = false) {
|
54 | let sheet = notebook
|
55 | ? this._notebookStyles.get(notebook)
|
56 | : this._globalStyles;
|
57 |
|
58 | let style = notebook
|
59 | ? this._nbMetaToStyle(meta, notebook)
|
60 | : this._settingsToStyle(meta);
|
61 |
|
62 | let jss = this._jss.createStyleSheet(style as any);
|
63 | let css = jss.toString();
|
64 |
|
65 | if (sheet && sheet.textContent !== css) {
|
66 | sheet.textContent = css;
|
67 | }
|
68 | this.hack();
|
69 | }
|
70 |
|
71 | private _nbMetaToStyle(
|
72 | meta: SCHEMA.ISettings,
|
73 | notebook: NotebookPanel
|
74 | ): SCHEMA.IStyles {
|
75 | const id = notebook.id;
|
76 | let jss: any = { '@font-face': [], '@global': {} };
|
77 | let idStyles: any = (jss['@global'][`.jp-NotebookPanel[id='${id}']`] = {});
|
78 |
|
79 | if (meta.fonts) {
|
80 | for (let fontFamily in meta.fonts) {
|
81 | jss['@font-face'] = jss['@font-face'].concat(meta.fonts[fontFamily]);
|
82 | }
|
83 | }
|
84 |
|
85 | let styles = meta.styles || {};
|
86 | for (let k in styles) {
|
87 | if (k === ROOT) {
|
88 | for (let rootK in styles[k]) {
|
89 | if (styles == null || styles[k] == null) {
|
90 | continue;
|
91 | }
|
92 | idStyles[rootK] = (styles as any)[k][rootK];
|
93 | }
|
94 | } else if (typeof styles[k] === 'object') {
|
95 | idStyles[`& ${k}`] = styles[k];
|
96 | } else {
|
97 | idStyles[k] = styles[k];
|
98 | }
|
99 | }
|
100 | return jss as SCHEMA.IStyles;
|
101 | }
|
102 |
|
103 | private _settingsToStyle(meta: SCHEMA.ISettings): SCHEMA.IStyles {
|
104 | let raw = JSON.stringify(meta.styles);
|
105 | let styles = JSON.parse(raw) as SCHEMA.ISettings;
|
106 | let faces = {} as SCHEMA.IFontFaceObject;
|
107 | for (let font of Array.from(this.fonts.keys())) {
|
108 | if (raw.indexOf(`'${font}'`) > -1 && !faces[font]) {
|
109 | const cachedFont = this._fontCache.get(font);
|
110 | if (cachedFont != null) {
|
111 | faces[font] = cachedFont;
|
112 | } else {
|
113 |
|
114 | new Promise((resolve, reject) => {
|
115 | const options = this.fonts.get(font);
|
116 | if (options == null) {
|
117 | reject();
|
118 | return;
|
119 | } else {
|
120 | options.faces().then(
|
121 | faces => {
|
122 | if (this._fontCache.has(font)) {
|
123 | return;
|
124 | }
|
125 | this._fontCache.set(font, faces);
|
126 | this._cacheUpdated.emit(void 0);
|
127 | resolve();
|
128 | },
|
129 | function(err) {
|
130 | console.error('rejected!', err);
|
131 | reject();
|
132 | }
|
133 | );
|
134 | }
|
135 | });
|
136 | }
|
137 | }
|
138 | }
|
139 |
|
140 | let flatFaces = Object.keys(faces).reduce((m, face) => {
|
141 | const foundFaces = faces[face];
|
142 | if (faces && foundFaces != null) {
|
143 | return m.concat(foundFaces);
|
144 | }
|
145 | }, [] as SCHEMA.IFontFacePrimitive[]);
|
146 |
|
147 | return {
|
148 | '@global': styles as any,
|
149 | '@font-face': flatFaces as any
|
150 | } as SCHEMA.IStyles;
|
151 | }
|
152 |
|
153 | dispose() {
|
154 | this._globalStyles.remove();
|
155 | for (let notebook of Array.from(this._notebookStyles.keys())) {
|
156 | this._onDisposed(notebook);
|
157 | }
|
158 | }
|
159 |
|
160 | hack(show = true) {
|
161 | if (show) {
|
162 | setTimeout(
|
163 | () =>
|
164 | this.stylesheets.map(s => {
|
165 | document.body.appendChild(s);
|
166 | }),
|
167 | 0
|
168 | );
|
169 | } else {
|
170 | this.stylesheets.map(el => el.remove());
|
171 | }
|
172 | }
|
173 | }
|