UNPKG

25.4 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3var __rest = (this && this.__rest) || function (s, e) {
4 var t = {};
5 for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
6 t[p] = s[p];
7 if (s != null && typeof Object.getOwnPropertySymbols === "function")
8 for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
9 if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
10 t[p[i]] = s[p[i]];
11 }
12 return t;
13};
14import { UUID } from '@lumino/coreutils';
15import { Signal } from '@lumino/signaling';
16import React from 'react';
17import ReactDOM from 'react-dom';
18import badSvgstr from '../../style/debug/bad.svg';
19import blankSvgstr from '../../style/debug/blank.svg';
20import refreshSvgstr from '../../style/icons/toolbar/refresh.svg';
21import { LabIconStyle } from '../style';
22import { classes, getReactAttrs } from '../utils';
23export class LabIcon {
24 /** *********
25 * members *
26 ***********/
27 constructor({ name, svgstr, render, unrender, _loading = false }) {
28 this._props = {};
29 this._svgReplaced = new Signal(this);
30 /**
31 * Cache for svg parsing intermediates
32 * - undefined: the cache has not yet been populated
33 * - null: a valid, but empty, value
34 */
35 this._svgElement = undefined;
36 this._svgInnerHTML = undefined;
37 this._svgReactAttrs = undefined;
38 if (!(name && svgstr)) {
39 // sanity check failed
40 console.error(`When defining a new LabIcon, name and svgstr must both be non-empty strings. name: ${name}, svgstr: ${svgstr}`);
41 return badIcon;
42 }
43 // currently this needs to be set early, before checks for existing icons
44 this._loading = _loading;
45 // check to see if this is a redefinition of an existing icon
46 if (LabIcon._instances.has(name)) {
47 // fetch the existing icon, replace its svg, then return it
48 const icon = LabIcon._instances.get(name);
49 if (this._loading) {
50 // replace the placeholder svg in icon
51 icon.svgstr = svgstr;
52 this._loading = false;
53 return icon;
54 }
55 else {
56 // already loaded icon svg exists; replace it and warn
57 if (LabIcon._debug) {
58 console.warn(`Redefining previously loaded icon svgstr. name: ${name}, svgstrOld: ${icon.svgstr}, svgstr: ${svgstr}`);
59 }
60 icon.svgstr = svgstr;
61 return icon;
62 }
63 }
64 this.name = name;
65 this.react = this._initReact(name);
66 this.svgstr = svgstr;
67 // setup custom render/unrender methods, if passed in
68 this._initRender({ render, unrender });
69 LabIcon._instances.set(this.name, this);
70 }
71 /** *********
72 * statics *
73 ***********/
74 /**
75 * Remove any rendered icon from the element that contains it
76 *
77 * @param container - a DOM node into which an icon was
78 * previously rendered
79 *
80 * @returns the cleaned container
81 */
82 static remove(container) {
83 // clean up all children
84 while (container.firstChild) {
85 container.firstChild.remove();
86 }
87 // remove all classes
88 container.className = '';
89 return container;
90 }
91 /**
92 * Resolve an icon name or a {name, svgstr} pair into an
93 * actual LabIcon.
94 *
95 * @param icon - either a string with the name of an existing icon
96 * or an object with {name: string, svgstr: string} fields.
97 *
98 * @returns a LabIcon instance
99 */
100 static resolve({ icon }) {
101 if (icon instanceof LabIcon) {
102 // icon already is a LabIcon; nothing to do here
103 return icon;
104 }
105 if (typeof icon === 'string') {
106 // do a dynamic lookup of existing icon by name
107 const resolved = LabIcon._instances.get(icon);
108 if (resolved) {
109 return resolved;
110 }
111 // lookup failed
112 if (LabIcon._debug) {
113 // fail noisily
114 console.warn(`Lookup failed for icon, creating loading icon. icon: ${icon}`);
115 }
116 // no matching icon currently registered, create a new loading icon
117 // TODO: find better icon (maybe animate?) for loading icon
118 return new LabIcon({ name: icon, svgstr: refreshSvgstr, _loading: true });
119 }
120 // icon was provided as a non-LabIcon {name, svgstr} pair, communicating
121 // an intention to create a new icon
122 return new LabIcon(icon);
123 }
124 /**
125 * Resolve an icon name or a {name, svgstr} pair into a DOM element.
126 * If icon arg is undefined, the function will fall back to trying to render
127 * the icon as a CSS background image, via the iconClass arg.
128 * If both icon and iconClass are undefined, this function will return
129 * an empty div.
130 *
131 * @param icon - optional, either a string with the name of an existing icon
132 * or an object with {name: string, svgstr: string} fields
133 *
134 * @param iconClass - optional, if the icon arg is not set, the iconClass arg
135 * should be a CSS class associated with an existing CSS background-image
136 *
137 * @deprecated fallback - don't use, optional, a LabIcon instance that will
138 * be used if neither icon nor iconClass are defined
139 *
140 * @param props - any additional args are passed though to the element method
141 * of the resolved icon on render
142 *
143 * @returns a DOM node with the resolved icon rendered into it
144 */
145 static resolveElement(_a) {
146 var { icon, iconClass, fallback } = _a, props = __rest(_a, ["icon", "iconClass", "fallback"]);
147 if (!Private.isResolvable(icon)) {
148 if (!iconClass && fallback) {
149 // if neither icon nor iconClass are defined/resolvable, use fallback
150 return fallback.element(props);
151 }
152 // set the icon's class to iconClass plus props.className
153 props.className = classes(iconClass, props.className);
154 // render icon as css background image, assuming one is set on iconClass
155 return Private.blankElement(props);
156 }
157 return LabIcon.resolve({ icon }).element(props);
158 }
159 /**
160 * Resolve an icon name or a {name, svgstr} pair into a React component.
161 * If icon arg is undefined, the function will fall back to trying to render
162 * the icon as a CSS background image, via the iconClass arg.
163 * If both icon and iconClass are undefined, the returned component
164 * will simply render an empty div.
165 *
166 * @param icon - optional, either a string with the name of an existing icon
167 * or an object with {name: string, svgstr: string} fields
168 *
169 * @param iconClass - optional, if the icon arg is not set, the iconClass arg
170 * should be a CSS class associated with an existing CSS background-image
171 *
172 * @deprecated fallback - don't use, optional, a LabIcon instance that will
173 * be used if neither icon nor iconClass are defined
174 *
175 * @param props - any additional args are passed though to the React component
176 * of the resolved icon on render
177 *
178 * @returns a React component that will render the resolved icon
179 */
180 static resolveReact(_a) {
181 var { icon, iconClass, fallback } = _a, props = __rest(_a, ["icon", "iconClass", "fallback"]);
182 if (!Private.isResolvable(icon)) {
183 if (!iconClass && fallback) {
184 // if neither icon nor iconClass are defined/resolvable, use fallback
185 return React.createElement(fallback.react, Object.assign({}, props));
186 }
187 // set the icon's class to iconClass plus props.className
188 props.className = classes(iconClass, props.className);
189 // render icon as css background image, assuming one is set on iconClass
190 return React.createElement(Private.blankReact, Object.assign({}, props));
191 }
192 const resolved = LabIcon.resolve({ icon });
193 return React.createElement(resolved.react, Object.assign({}, props));
194 }
195 /**
196 * Resolve a {name, svgstr} pair into an actual svg node.
197 */
198 static resolveSvg({ name, svgstr }) {
199 const svgDoc = new DOMParser().parseFromString(Private.svgstrShim(svgstr), 'image/svg+xml');
200 const svgError = svgDoc.querySelector('parsererror');
201 // structure of error element varies by browser, search at top level
202 if (svgError) {
203 // parse failed, svgElement will be an error box
204 const errmsg = `SVG HTML was malformed for LabIcon instance.\nname: ${name}, svgstr: ${svgstr}`;
205 if (LabIcon._debug) {
206 // fail noisily, render the error box
207 console.error(errmsg);
208 return svgError;
209 }
210 else {
211 // bad svg is always a real error, fail silently but warn
212 console.warn(errmsg);
213 return null;
214 }
215 }
216 else {
217 // parse succeeded
218 return svgDoc.documentElement;
219 }
220 }
221 /**
222 * Toggle icon debug from off-to-on, or vice-versa.
223 *
224 * @param debug - optional boolean to force debug on or off
225 */
226 static toggleDebug(debug) {
227 LabIcon._debug = debug !== null && debug !== void 0 ? debug : !LabIcon._debug;
228 }
229 /**
230 * Get a view of this icon that is bound to the specified icon/style props
231 *
232 * @param optional icon/style props (same as args for .element
233 * and .react methods). These will be bound to the resulting view
234 *
235 * @returns a view of this LabIcon instance
236 */
237 bindprops(props) {
238 const view = Object.create(this);
239 view._props = props;
240 view.react = view._initReact(view.name + '_bind');
241 return view;
242 }
243 /**
244 * Create an icon as a DOM element
245 *
246 * @param className - a string that will be used as the class
247 * of the container element. Overrides any existing class
248 *
249 * @param container - a preexisting DOM element that
250 * will be used as the container for the svg element
251 *
252 * @param label - text that will be displayed adjacent
253 * to the icon
254 *
255 * @param title - a tooltip for the icon
256 *
257 * @param tag - if container is not explicitly
258 * provided, this tag will be used when creating the container
259 *
260 * @param stylesheet - optional string naming a builtin icon
261 * stylesheet, for example 'menuItem' or `statusBar`. Can also be an
262 * object defining a custom icon stylesheet, or a list of builtin
263 * stylesheet names and/or custom stylesheet objects. If array,
264 * the given stylesheets will be merged.
265 *
266 * See @jupyterlab/ui-components/src/style/icon.ts for details
267 *
268 * @param elementPosition - optional position for the inner svg element
269 *
270 * @param elementSize - optional size for the inner svg element.
271 * Set to 'normal' to get a standard 16px x 16px icon
272 *
273 * @param ...elementCSS - all additional args are treated as
274 * overrides for the CSS props applied to the inner svg element
275 *
276 * @returns A DOM element that contains an (inline) svg element
277 * that displays an icon
278 */
279 element(props = {}) {
280 var _a;
281 let _b = Object.assign(Object.assign({}, this._props), props), { className, container, label, title, tag = 'div' } = _b, styleProps = __rest(_b, ["className", "container", "label", "title", "tag"]);
282 // check if icon element is already set
283 const maybeSvgElement = container === null || container === void 0 ? void 0 : container.firstChild;
284 if (((_a = maybeSvgElement === null || maybeSvgElement === void 0 ? void 0 : maybeSvgElement.dataset) === null || _a === void 0 ? void 0 : _a.iconId) === this._uuid) {
285 // return the existing icon element
286 return maybeSvgElement;
287 }
288 // ensure that svg html is valid
289 if (!this.svgElement) {
290 // bail if failing silently, return blank element
291 return document.createElement('div');
292 }
293 let returnSvgElement = true;
294 if (container) {
295 // take ownership by removing any existing children
296 while (container.firstChild) {
297 container.firstChild.remove();
298 }
299 }
300 else {
301 // create a container if needed
302 container = document.createElement(tag);
303 returnSvgElement = false;
304 }
305 if (label != null) {
306 container.textContent = label;
307 }
308 Private.initContainer({ container, className, styleProps, title });
309 // add the svg node to the container
310 const svgElement = this.svgElement.cloneNode(true);
311 container.appendChild(svgElement);
312 return returnSvgElement ? svgElement : container;
313 }
314 render(container, options) {
315 var _a;
316 let label = (_a = options === null || options === void 0 ? void 0 : options.children) === null || _a === void 0 ? void 0 : _a[0];
317 // narrow type of label
318 if (typeof label !== 'string') {
319 label = undefined;
320 }
321 this.element(Object.assign({ container,
322 label }, options === null || options === void 0 ? void 0 : options.props));
323 }
324 get svgElement() {
325 if (this._svgElement === undefined) {
326 this._svgElement = this._initSvg({ uuid: this._uuid });
327 }
328 return this._svgElement;
329 }
330 get svgInnerHTML() {
331 if (this._svgInnerHTML === undefined) {
332 if (this.svgElement === null) {
333 // the svg element resolved to null, mark this null too
334 this._svgInnerHTML = null;
335 }
336 else {
337 this._svgInnerHTML = this.svgElement.innerHTML;
338 }
339 }
340 return this._svgInnerHTML;
341 }
342 get svgReactAttrs() {
343 if (this._svgReactAttrs === undefined) {
344 if (this.svgElement === null) {
345 // the svg element resolved to null, mark this null too
346 this._svgReactAttrs = null;
347 }
348 else {
349 this._svgReactAttrs = getReactAttrs(this.svgElement, {
350 ignore: ['data-icon-id']
351 });
352 }
353 }
354 return this._svgReactAttrs;
355 }
356 get svgstr() {
357 return this._svgstr;
358 }
359 set svgstr(svgstr) {
360 this._svgstr = svgstr;
361 // associate a new unique id with this particular svgstr
362 const uuid = UUID.uuid4();
363 const uuidOld = this._uuid;
364 this._uuid = uuid;
365 // empty the svg parsing intermediates cache
366 this._svgElement = undefined;
367 this._svgInnerHTML = undefined;
368 this._svgReactAttrs = undefined;
369 // update icon elements created using .element method
370 document
371 .querySelectorAll(`[data-icon-id="${uuidOld}"]`)
372 .forEach(oldSvgElement => {
373 if (this.svgElement) {
374 oldSvgElement.replaceWith(this.svgElement.cloneNode(true));
375 }
376 });
377 // trigger update of icon elements created using other methods
378 this._svgReplaced.emit();
379 }
380 _initReact(displayName) {
381 const component = React.forwardRef((props = {}, ref) => {
382 const _a = Object.assign(Object.assign({}, this._props), props), { className, container, label, title, tag = 'div' } = _a, styleProps = __rest(_a, ["className", "container", "label", "title", "tag"]);
383 // set up component state via useState hook
384 const [, setId] = React.useState(this._uuid);
385 // subscribe to svg replacement via useEffect hook
386 React.useEffect(() => {
387 const onSvgReplaced = () => {
388 setId(this._uuid);
389 };
390 this._svgReplaced.connect(onSvgReplaced);
391 // specify cleanup callback as hook return
392 return () => {
393 this._svgReplaced.disconnect(onSvgReplaced);
394 };
395 });
396 // make it so that tag can be used as a jsx component
397 const Tag = tag;
398 // ensure that svg html is valid
399 if (!(this.svgInnerHTML && this.svgReactAttrs)) {
400 // bail if failing silently
401 return React.createElement(React.Fragment, null);
402 }
403 const svgComponent = (React.createElement("svg", Object.assign({}, this.svgReactAttrs, { dangerouslySetInnerHTML: { __html: this.svgInnerHTML }, ref: ref })));
404 if (container) {
405 Private.initContainer({ container, className, styleProps, title });
406 return (React.createElement(React.Fragment, null,
407 svgComponent,
408 label));
409 }
410 else {
411 return (React.createElement(Tag, { className: classes(className, LabIconStyle.styleClass(styleProps)) },
412 svgComponent,
413 label));
414 }
415 });
416 component.displayName = `LabIcon_${displayName}`;
417 return component;
418 }
419 _initRender({ render, unrender }) {
420 if (render) {
421 this.render = render;
422 if (unrender) {
423 this.unrender = unrender;
424 }
425 }
426 else if (unrender) {
427 console.warn('In _initRender, ignoring unrender arg since render is undefined');
428 }
429 }
430 _initSvg({ title, uuid } = {}) {
431 const svgElement = LabIcon.resolveSvg(this);
432 if (!svgElement) {
433 // bail on null svg element
434 return svgElement;
435 }
436 if (svgElement.tagName !== 'parsererror') {
437 // svgElement is an actual svg node, augment it
438 svgElement.dataset.icon = this.name;
439 if (uuid) {
440 svgElement.dataset.iconId = uuid;
441 }
442 if (title) {
443 Private.setTitleSvg(svgElement, title);
444 }
445 }
446 return svgElement;
447 }
448}
449LabIcon._debug = false;
450LabIcon._instances = new Map();
451var Private;
452(function (Private) {
453 function blankElement(_a) {
454 var { className = '', container, label, title, tag = 'div' } = _a, styleProps = __rest(_a, ["className", "container", "label", "title", "tag"]);
455 if ((container === null || container === void 0 ? void 0 : container.className) === className) {
456 // nothing needs doing, return the icon node
457 return container;
458 }
459 if (container) {
460 // take ownership by removing any existing children
461 while (container.firstChild) {
462 container.firstChild.remove();
463 }
464 }
465 else {
466 // create a container if needed
467 container = document.createElement(tag);
468 }
469 if (label != null) {
470 container.textContent = label;
471 }
472 Private.initContainer({ container, className, styleProps, title });
473 return container;
474 }
475 Private.blankElement = blankElement;
476 Private.blankReact = React.forwardRef((_a, ref) => {
477 var { className = '', container, label, title, tag = 'div' } = _a, styleProps = __rest(_a, ["className", "container", "label", "title", "tag"]);
478 // make it so that tag can be used as a jsx component
479 const Tag = tag;
480 if (container) {
481 initContainer({ container, className, styleProps, title });
482 return React.createElement(React.Fragment, null);
483 }
484 else {
485 // if ref is defined, we create a blank svg node and point ref to it
486 return (React.createElement(Tag, { className: classes(className, LabIconStyle.styleClass(styleProps)) },
487 ref && blankIcon.react({ ref }),
488 label));
489 }
490 });
491 Private.blankReact.displayName = 'BlankReact';
492 function initContainer({ container, className, styleProps, title }) {
493 if (title != null) {
494 container.title = title;
495 }
496 const styleClass = LabIconStyle.styleClass(styleProps);
497 if (className != null) {
498 // override the container class with explicitly passed-in class + style class
499 const classResolved = classes(className, styleClass);
500 container.className = classResolved;
501 return classResolved;
502 }
503 else if (styleClass) {
504 // add the style class to the container class
505 container.classList.add(styleClass);
506 return styleClass;
507 }
508 else {
509 return '';
510 }
511 }
512 Private.initContainer = initContainer;
513 function isResolvable(icon) {
514 return !!(icon &&
515 (typeof icon === 'string' ||
516 (icon.name && icon.svgstr)));
517 }
518 Private.isResolvable = isResolvable;
519 function setTitleSvg(svgNode, title) {
520 // add a title node to the top level svg node
521 const titleNodes = svgNode.getElementsByTagName('title');
522 if (titleNodes.length) {
523 titleNodes[0].textContent = title;
524 }
525 else {
526 const titleNode = document.createElement('title');
527 titleNode.textContent = title;
528 svgNode.appendChild(titleNode);
529 }
530 }
531 Private.setTitleSvg = setTitleSvg;
532 /**
533 * A shim for svgstrs loaded using any loader other than raw-loader.
534 * This function assumes that svgstr will look like one of:
535 *
536 * - the raw contents of an .svg file:
537 * <svg...</svg>
538 *
539 * - a data URL:
540 * data:[<mediatype>][;base64],<svg...</svg>
541 *
542 * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
543 */
544 function svgstrShim(svgstr, strict = true) {
545 // decode any uri escaping, condense leading/lagging whitespace,
546 // then match to raw svg string
547 const [, base64, raw] = decodeURIComponent(svgstr)
548 .replace(/>\s*\n\s*</g, '><')
549 .replace(/\s*\n\s*/g, ' ')
550 .match(strict
551 ? // match based on data url schema
552 /^(?:data:.*?(;base64)?,)?(.*)/
553 : // match based on open of svg tag
554 /(?:(base64).*)?(<svg.*)/);
555 // decode from base64, if needed
556 return base64 ? atob(raw) : raw;
557 }
558 Private.svgstrShim = svgstrShim;
559 /**
560 * TODO: figure out story for independent Renderers.
561 * Base implementation of IRenderer.
562 */
563 class Renderer {
564 constructor(_icon, _rendererOptions) {
565 this._icon = _icon;
566 this._rendererOptions = _rendererOptions;
567 }
568 // eslint-disable-next-line
569 render(container, options) { }
570 }
571 Private.Renderer = Renderer;
572 /**
573 * TODO: figure out story for independent Renderers.
574 * Implementation of IRenderer that creates the icon svg node
575 * as a DOM element.
576 */
577 class ElementRenderer extends Renderer {
578 render(container, options) {
579 var _a, _b;
580 let label = (_a = options === null || options === void 0 ? void 0 : options.children) === null || _a === void 0 ? void 0 : _a[0];
581 // narrow type of label
582 if (typeof label !== 'string') {
583 label = undefined;
584 }
585 this._icon.element(Object.assign(Object.assign({ container,
586 label }, (_b = this._rendererOptions) === null || _b === void 0 ? void 0 : _b.props), options === null || options === void 0 ? void 0 : options.props));
587 }
588 }
589 Private.ElementRenderer = ElementRenderer;
590 /**
591 * TODO: figure out story for independent Renderers.
592 * Implementation of IRenderer that creates the icon svg node
593 * as a React component.
594 */
595 class ReactRenderer extends Renderer {
596 render(container, options) {
597 var _a, _b;
598 let label = (_a = options === null || options === void 0 ? void 0 : options.children) === null || _a === void 0 ? void 0 : _a[0];
599 // narrow type of label
600 if (typeof label !== 'string') {
601 label = undefined;
602 }
603 ReactDOM.render(React.createElement(this._icon.react, Object.assign({ container: container, label: label }, Object.assign(Object.assign({}, (_b = this._rendererOptions) === null || _b === void 0 ? void 0 : _b.props), options === null || options === void 0 ? void 0 : options.props))), container);
604 }
605 unrender(container) {
606 ReactDOM.unmountComponentAtNode(container);
607 }
608 }
609 Private.ReactRenderer = ReactRenderer;
610})(Private || (Private = {}));
611// need to be at the bottom since constructor depends on Private
612export const badIcon = new LabIcon({
613 name: 'ui-components:bad',
614 svgstr: badSvgstr
615});
616export const blankIcon = new LabIcon({
617 name: 'ui-components:blank',
618 svgstr: blankSvgstr
619});
620//# sourceMappingURL=labicon.js.map
\No newline at end of file