/
jlicon.tsx
372 lines (317 loc) · 9.63 KB
/
jlicon.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { UUID } from '@lumino/coreutils';
import React from 'react';
import ReactDOM from 'react-dom';
import { Text } from '@jupyterlab/coreutils';
import { iconStyle, IIconStyle } from '../style/icon';
import { getReactAttrs, classes, classesDedupe } from '../utils';
import badSvg from '../../style/debug/bad.svg';
import blankSvg from '../../style/debug/blank.svg';
export class JLIcon {
private static _debug: boolean = false;
private static _instances = new Map<string, JLIcon>();
/**
* Get any existing JLIcon instance by name.
*
* @param name - name of the JLIcon instance to fetch
*
* @param fallback - optional default JLIcon instance to use if
* name is not found
*
* @returns A JLIcon instance
*/
private static _get(name: string, fallback?: JLIcon): JLIcon | undefined {
const icon = JLIcon._instances.get(name);
if (icon) {
return icon;
} else {
if (JLIcon._debug) {
// fail noisily
console.error(`Invalid icon name: ${name}`);
return badIcon;
}
// fail silently
return fallback;
}
}
/**
* Get any existing JLIcon instance by name, construct a DOM element
* from it, then return said element.
*
* @param name - name of the JLIcon instance to fetch
*
* @param fallback - if left undefined, use automatic fallback to
* icons-as-css-background behavior: elem will be constructed using
* a blank icon with `elem.className = classes(name, props.className)`,
* where elem is the return value. Otherwise, fallback can be used to
* define the default JLIcon instance, returned whenever lookup fails
*
* @param props - passed directly to JLIcon.element
*
* @returns an SVGElement
*/
static getElement({
name,
fallback,
...props
}: { name: string; fallback?: JLIcon } & JLIcon.IProps) {
let icon = JLIcon._get(name, fallback);
if (!icon) {
icon = blankIcon;
props.className = classesDedupe(name, props.className);
}
return icon.element(props);
}
/**
* Get any existing JLIcon instance by name, construct a React element
* from it, then return said element.
*
* @param name - name of the JLIcon instance to fetch
*
* @param fallback - if left undefined, use automatic fallback to
* icons-as-css-background behavior: elem will be constructed using
* a blank icon with `elem.className = classes(name, props.className)`,
* where elem is the return value. Otherwise, fallback can be used to
* define the default JLIcon instance, used to construct the return
* elem whenever lookup fails
*
* @param props - passed directly to JLIcon.react
*
* @returns a React element
*/
static getReact({
name,
fallback,
...props
}: { name: string; fallback?: JLIcon } & JLIcon.IReactProps) {
let icon = JLIcon._get(name, fallback);
if (!icon) {
icon = blankIcon;
props.className = classesDedupe(name, props.className);
}
return <icon.react {...props} />;
}
/**
* Toggle icon debug from off-to-on, or vice-versa.
*
* @param debug - optional boolean to force debug on or off
*/
static toggleDebug(debug?: boolean) {
JLIcon._debug = debug ?? !JLIcon._debug;
}
constructor({ name, svgstr }: JLIcon.IOptions) {
this.name = name;
this._className = Private.nameToClassName(name);
this.svgstr = svgstr;
this.react = this._initReact();
JLIcon._instances.set(this.name, this);
JLIcon._instances.set(this._className, this);
}
class({ className, ...propsStyle }: { className?: string } & IIconStyle) {
return classesDedupe(className, iconStyle(propsStyle));
}
element({
className,
container,
title,
tag = 'div',
...propsStyle
}: JLIcon.IProps = {}): HTMLElement | null {
// check if icon element is already set
const maybeSvgElement = container?.firstChild as HTMLElement;
if (maybeSvgElement?.dataset?.iconid === this._uuid) {
// return the existing icon element
return maybeSvgElement;
}
// ensure that svg html is valid
const svgElement = this.resolveSvg();
if (!svgElement) {
// bail if failing silently
return null;
}
let ret: HTMLElement;
if (container) {
// take ownership by removing any existing children
while (container.firstChild) {
container.firstChild.remove();
}
ret = svgElement;
} else {
// create a container if needed
container = document.createElement(tag);
ret = container;
}
this._initContainer({ container, className, propsStyle, title });
// add the svg node to the container
container.appendChild(svgElement);
return ret;
}
render(host: HTMLElement, props: JLIcon.IProps = {}): void {
return ReactDOM.render(<this.react container={host} {...props} />, host);
}
resolveSvg(title?: string): HTMLElement | null {
const svgDoc = new DOMParser().parseFromString(
this._svgstr,
'image/svg+xml'
);
const svgElement = svgDoc.documentElement;
if (svgElement.getElementsByTagName('parsererror').length > 0) {
const errmsg = `SVG HTML was malformed for icon name: ${name}`;
// parse failed, svgElement will be an error box
if (JLIcon._debug) {
// fail noisily, render the error box
console.error(errmsg);
return svgElement;
} else {
// bad svg is always a real error, fail silently but warn
console.warn(errmsg);
return null;
}
} else {
// parse succeeded
svgElement.dataset.iconid = this._uuid;
if (title) {
Private.setTitleSvg(svgElement, title);
}
return svgElement;
}
}
get svgstr() {
return this._svgstr;
}
set svgstr(svgstr: string) {
this._svgstr = svgstr;
// associate a unique id with this particular svgstr
this._uuid = UUID.uuid4();
}
unrender(host: HTMLElement): void {
ReactDOM.unmountComponentAtNode(host);
}
protected _initContainer({
container,
className,
propsStyle,
title
}: {
container: HTMLElement;
className?: string;
propsStyle?: IIconStyle;
title?: string;
}) {
const classStyle = iconStyle(propsStyle);
if (className || className === '') {
// override the container class with explicitly passed-in class + style class
container.className = classes(className, classStyle);
} else if (classStyle) {
// add the style class to the container class
container.classList.add(classStyle);
}
if (title) {
container.title = title;
}
}
protected _initReact() {
const component = React.forwardRef(
(
{
className,
container,
title,
tag = 'div',
...propsStyle
}: JLIcon.IProps = {},
ref: React.RefObject<SVGElement>
) => {
const Tag = tag;
// ensure that svg html is valid
const svgElement = this.resolveSvg();
if (!svgElement) {
// bail if failing silently
return <></>;
}
const svgComponent = (
<svg
{...getReactAttrs(svgElement)}
dangerouslySetInnerHTML={{ __html: svgElement.innerHTML }}
ref={ref}
/>
);
if (container) {
this._initContainer({ container, className, propsStyle, title });
return svgComponent;
} else {
return (
<Tag className={classes(className, iconStyle(propsStyle))}>
{svgComponent}
</Tag>
);
}
}
);
component.displayName = `JLIcon_${this.name}`;
return component;
}
readonly name: string;
readonly react: JLIcon.IReact;
protected _className: string;
protected _svgstr: string;
protected _uuid: string;
}
/**
* A namespace for JLIcon statics.
*/
export namespace JLIcon {
/**
* The type of the JLIcon contructor params
*/
export interface IOptions {
name: string;
svgstr: string;
}
/**
* The input props for creating a new JLIcon
*/
export interface IProps extends IIconStyle {
/**
* Extra classNames. Used in addition to the typestyle className to
* set the className of the icon's outermost container node
*/
className?: string;
/**
* The icon's outermost node, which acts as a container for the actual
* svg node. If container is not supplied, it will be created
*/
container?: HTMLElement;
/**
* HTML element tag used to create the icon's outermost container node,
* if no container is passed in
*/
tag?: 'div' | 'span';
/**
* Optional title that will be set on the icon's svg node
*/
title?: string;
}
export type IReactProps = IProps & React.RefAttributes<SVGElement>;
export type IReact = React.ForwardRefExoticComponent<IReactProps>;
}
namespace Private {
export function nameToClassName(name: string): string {
return 'jp-' + Text.camelCase(name, true) + 'Icon';
}
export function setTitleSvg(svgNode: HTMLElement, title: string): void {
// add a title node to the top level svg node
let titleNodes = svgNode.getElementsByTagName('title');
if (titleNodes.length) {
titleNodes[0].textContent = title;
} else {
let titleNode = document.createElement('title');
titleNode.textContent = title;
svgNode.appendChild(titleNode);
}
}
}
// need to be at the bottom since constructor depends on Private
export const badIcon = new JLIcon({ name: 'bad', svgstr: badSvg });
export const blankIcon = new JLIcon({ name: 'blank', svgstr: blankSvg });