Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
958 lines (844 sloc)
26.9 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (c) Jupyter Development Team. | |
// Distributed under the terms of the Modified BSD License. | |
import { UUID } from '@lumino/coreutils'; | |
import { Signal } from '@lumino/signaling'; | |
import { ElementAttrs, VirtualElement, VirtualNode } from '@lumino/virtualdom'; | |
import React from 'react'; | |
import ReactDOM from 'react-dom'; | |
import { LabIconStyle } from '../style'; | |
import { getReactAttrs, classes } from '../utils'; | |
import badSvgstr from '../../style/debug/bad.svg'; | |
import blankSvgstr from '../../style/debug/blank.svg'; | |
import refreshSvgstr from '../../style/icons/toolbar/refresh.svg'; | |
export class LabIcon implements LabIcon.ILabIcon, VirtualElement.IRenderer { | |
/** ********* | |
* statics * | |
***********/ | |
/** | |
* Remove any rendered icon from the element that contains it | |
* | |
* @param container - a DOM node into which an icon was | |
* previously rendered | |
* | |
* @returns the cleaned container | |
*/ | |
static remove(container: HTMLElement) { | |
// clean up all children | |
while (container.firstChild) { | |
container.firstChild.remove(); | |
} | |
// remove all classes | |
container.className = ''; | |
return container; | |
} | |
/** | |
* Resolve an icon name or a {name, svgstr} pair into an | |
* actual LabIcon. | |
* | |
* @param icon - either a string with the name of an existing icon | |
* or an object with {name: string, svgstr: string} fields. | |
* | |
* @returns a LabIcon instance | |
*/ | |
static resolve({ icon }: { icon: LabIcon.IResolvable }): LabIcon { | |
if (icon instanceof LabIcon) { | |
// icon already is a LabIcon; nothing to do here | |
return icon; | |
} | |
if (typeof icon === 'string') { | |
// do a dynamic lookup of existing icon by name | |
const resolved = LabIcon._instances.get(icon); | |
if (resolved) { | |
return resolved; | |
} | |
// lookup failed | |
if (LabIcon._debug) { | |
// fail noisily | |
console.warn( | |
`Lookup failed for icon, creating loading icon. icon: ${icon}` | |
); | |
} | |
// no matching icon currently registered, create a new loading icon | |
// TODO: find better icon (maybe animate?) for loading icon | |
return new LabIcon({ name: icon, svgstr: refreshSvgstr, _loading: true }); | |
} | |
// icon was provided as a non-LabIcon {name, svgstr} pair, communicating | |
// an intention to create a new icon | |
return new LabIcon(icon); | |
} | |
/** | |
* Resolve an icon name or a {name, svgstr} pair into a DOM element. | |
* If icon arg is undefined, the function will fall back to trying to render | |
* the icon as a CSS background image, via the iconClass arg. | |
* If both icon and iconClass are undefined, this function will return | |
* an empty div. | |
* | |
* @param icon - optional, either a string with the name of an existing icon | |
* or an object with {name: string, svgstr: string} fields | |
* | |
* @param iconClass - optional, if the icon arg is not set, the iconClass arg | |
* should be a CSS class associated with an existing CSS background-image | |
* | |
* @deprecated fallback - don't use, optional, a LabIcon instance that will | |
* be used if neither icon nor iconClass are defined | |
* | |
* @param props - any additional args are passed though to the element method | |
* of the resolved icon on render | |
* | |
* @returns a DOM node with the resolved icon rendered into it | |
*/ | |
static resolveElement({ | |
icon, | |
iconClass, | |
fallback, | |
...props | |
}: Partial<LabIcon.IResolverProps> & LabIcon.IProps) { | |
if (!Private.isResolvable(icon)) { | |
if (!iconClass && fallback) { | |
// if neither icon nor iconClass are defined/resolvable, use fallback | |
return fallback.element(props); | |
} | |
// set the icon's class to iconClass plus props.className | |
props.className = classes(iconClass, props.className); | |
// render icon as css background image, assuming one is set on iconClass | |
return Private.blankElement(props); | |
} | |
return LabIcon.resolve({ icon }).element(props); | |
} | |
/** | |
* Resolve an icon name or a {name, svgstr} pair into a React component. | |
* If icon arg is undefined, the function will fall back to trying to render | |
* the icon as a CSS background image, via the iconClass arg. | |
* If both icon and iconClass are undefined, the returned component | |
* will simply render an empty div. | |
* | |
* @param icon - optional, either a string with the name of an existing icon | |
* or an object with {name: string, svgstr: string} fields | |
* | |
* @param iconClass - optional, if the icon arg is not set, the iconClass arg | |
* should be a CSS class associated with an existing CSS background-image | |
* | |
* @deprecated fallback - don't use, optional, a LabIcon instance that will | |
* be used if neither icon nor iconClass are defined | |
* | |
* @param props - any additional args are passed though to the React component | |
* of the resolved icon on render | |
* | |
* @returns a React component that will render the resolved icon | |
*/ | |
static resolveReact({ | |
icon, | |
iconClass, | |
fallback, | |
...props | |
}: Partial<LabIcon.IResolverProps> & LabIcon.IReactProps) { | |
if (!Private.isResolvable(icon)) { | |
if (!iconClass && fallback) { | |
// if neither icon nor iconClass are defined/resolvable, use fallback | |
return <fallback.react {...props} />; | |
} | |
// set the icon's class to iconClass plus props.className | |
props.className = classes(iconClass, props.className); | |
// render icon as css background image, assuming one is set on iconClass | |
return <Private.blankReact {...props} />; | |
} | |
const resolved = LabIcon.resolve({ icon }); | |
return <resolved.react {...props} />; | |
} | |
/** | |
* Resolve a {name, svgstr} pair into an actual svg node. | |
*/ | |
static resolveSvg({ name, svgstr }: LabIcon.IIcon): HTMLElement | null { | |
const svgDoc = new DOMParser().parseFromString( | |
Private.svgstrShim(svgstr), | |
'image/svg+xml' | |
); | |
const svgError = svgDoc.querySelector('parsererror'); | |
// structure of error element varies by browser, search at top level | |
if (svgError) { | |
// parse failed, svgElement will be an error box | |
const errmsg = `SVG HTML was malformed for LabIcon instance.\nname: ${name}, svgstr: ${svgstr}`; | |
if (LabIcon._debug) { | |
// fail noisily, render the error box | |
console.error(errmsg); | |
return svgError as HTMLElement; | |
} else { | |
// bad svg is always a real error, fail silently but warn | |
console.warn(errmsg); | |
return null; | |
} | |
} else { | |
// parse succeeded | |
return svgDoc.documentElement; | |
} | |
} | |
/** | |
* Toggle icon debug from off-to-on, or vice-versa. | |
* | |
* @param debug - optional boolean to force debug on or off | |
*/ | |
static toggleDebug(debug?: boolean) { | |
LabIcon._debug = debug ?? !LabIcon._debug; | |
} | |
private static _debug: boolean = false; | |
private static _instances = new Map<string, LabIcon>(); | |
/** ********* | |
* members * | |
***********/ | |
constructor({ | |
name, | |
svgstr, | |
render, | |
unrender, | |
_loading = false | |
}: LabIcon.IOptions & { _loading?: boolean }) { | |
if (!(name && svgstr)) { | |
// sanity check failed | |
console.error( | |
`When defining a new LabIcon, name and svgstr must both be non-empty strings. name: ${name}, svgstr: ${svgstr}` | |
); | |
return badIcon; | |
} | |
// currently this needs to be set early, before checks for existing icons | |
this._loading = _loading; | |
// check to see if this is a redefinition of an existing icon | |
if (LabIcon._instances.has(name)) { | |
// fetch the existing icon, replace its svg, then return it | |
const icon = LabIcon._instances.get(name)!; | |
if (this._loading) { | |
// replace the placeholder svg in icon | |
icon.svgstr = svgstr; | |
this._loading = false; | |
return icon; | |
} else { | |
// already loaded icon svg exists; replace it and warn | |
// TODO: need to see if this warning is useful or just noisy | |
console.warn( | |
`Redefining previously loaded icon svgstr. name: ${name}, svgstrOld: ${icon.svgstr}, svgstr: ${svgstr}` | |
); | |
icon.svgstr = svgstr; | |
return icon; | |
} | |
} | |
this.name = name; | |
this.react = this._initReact(name); | |
this.svgstr = svgstr; | |
// setup custom render/unrender methods, if passed in | |
this._initRender({ render, unrender }); | |
LabIcon._instances.set(this.name, this); | |
} | |
/** | |
* Get a view of this icon that is bound to the specified icon/style props | |
* | |
* @param optional icon/style props (same as args for .element | |
* and .react methods). These will be bound to the resulting view | |
* | |
* @returns a view of this LabIcon instance | |
*/ | |
bindprops(props?: LabIcon.IProps) { | |
const view = Object.create(this); | |
view._props = props; | |
view.react = view._initReact(view.name + '_bind'); | |
return view; | |
} | |
/** | |
* Create an icon as a DOM element | |
* | |
* @param className - a string that will be used as the class | |
* of the container element. Overrides any existing class | |
* | |
* @param container - a preexisting DOM element that | |
* will be used as the container for the svg element | |
* | |
* @param label - text that will be displayed adjacent | |
* to the icon | |
* | |
* @param title - a tooltip for the icon | |
* | |
* @param tag - if container is not explicitly | |
* provided, this tag will be used when creating the container | |
* | |
* @param stylesheet - optional string naming a builtin icon | |
* stylesheet, for example 'menuItem' or `statusBar`. Can also be an | |
* object defining a custom icon stylesheet, or a list of builtin | |
* stylesheet names and/or custom stylesheet objects. If array, | |
* the given stylesheets will be merged. | |
* | |
* See @jupyterlab/ui-components/src/style/icon.ts for details | |
* | |
* @param elementPosition - optional position for the inner svg element | |
* | |
* @param elementSize - optional size for the inner svg element. | |
* Set to 'normal' to get a standard 16px x 16px icon | |
* | |
* @param ...elementCSS - all additional args are treated as | |
* overrides for the CSS props applied to the inner svg element | |
* | |
* @returns A DOM element that contains an (inline) svg element | |
* that displays an icon | |
*/ | |
element(props: LabIcon.IProps = {}): HTMLElement { | |
let { | |
className, | |
container, | |
label, | |
title, | |
tag = 'div', | |
...styleProps | |
}: LabIcon.IProps = { ...this._props, ...props }; | |
// 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 | |
if (!this.svgElement) { | |
// bail if failing silently, return blank element | |
return document.createElement('div'); | |
} | |
let returnSvgElement = true; | |
if (container) { | |
// take ownership by removing any existing children | |
while (container.firstChild) { | |
container.firstChild.remove(); | |
} | |
} else { | |
// create a container if needed | |
container = document.createElement(tag); | |
returnSvgElement = false; | |
} | |
if (label != null) { | |
container.textContent = label; | |
} | |
Private.initContainer({ container, className, styleProps, title }); | |
// add the svg node to the container | |
const svgElement = this.svgElement.cloneNode(true) as HTMLElement; | |
container.appendChild(svgElement); | |
return returnSvgElement ? svgElement : container; | |
} | |
render(container: HTMLElement, options?: LabIcon.IRendererOptions): void { | |
let label = options?.children?.[0]; | |
// narrow type of label | |
if (typeof label !== 'string') { | |
label = undefined; | |
} | |
this.element({ | |
container, | |
label, | |
...options?.props | |
}); | |
} | |
protected get svgElement(): HTMLElement | null { | |
if (this._svgElement === undefined) { | |
this._svgElement = this._initSvg({ uuid: this._uuid }); | |
} | |
return this._svgElement; | |
} | |
protected get svgInnerHTML(): string | null { | |
if (this._svgInnerHTML === undefined) { | |
if (this.svgElement === null) { | |
// the svg element resolved to null, mark this null too | |
this._svgInnerHTML = null; | |
} else { | |
this._svgInnerHTML = this.svgElement.innerHTML; | |
} | |
} | |
return this._svgInnerHTML; | |
} | |
protected get svgReactAttrs(): any | null { | |
if (this._svgReactAttrs === undefined) { | |
if (this.svgElement === null) { | |
// the svg element resolved to null, mark this null too | |
this._svgReactAttrs = null; | |
} else { | |
this._svgReactAttrs = getReactAttrs(this.svgElement, { | |
ignore: ['data-icon-id'] | |
}); | |
} | |
} | |
return this._svgReactAttrs; | |
} | |
get svgstr() { | |
return this._svgstr; | |
} | |
set svgstr(svgstr: string) { | |
this._svgstr = svgstr; | |
// associate a new unique id with this particular svgstr | |
const uuid = UUID.uuid4(); | |
const uuidOld = this._uuid; | |
this._uuid = uuid; | |
// empty the svg parsing intermediates cache | |
this._svgElement = undefined; | |
this._svgInnerHTML = undefined; | |
this._svgReactAttrs = undefined; | |
// update icon elements created using .element method | |
document | |
.querySelectorAll(`[data-icon-id="${uuidOld}"]`) | |
.forEach(oldSvgElement => { | |
if (this.svgElement) { | |
oldSvgElement.replaceWith(this.svgElement.cloneNode(true)); | |
} | |
}); | |
// trigger update of icon elements created using other methods | |
this._svgReplaced.emit(); | |
} | |
unrender?(container: HTMLElement, options?: LabIcon.IRendererOptions): void; | |
protected _initReact(displayName: string) { | |
const component = React.forwardRef( | |
(props: LabIcon.IProps = {}, ref: LabIcon.IReactRef) => { | |
const { | |
className, | |
container, | |
label, | |
title, | |
tag = 'div', | |
...styleProps | |
}: LabIcon.IProps = { ...this._props, ...props }; | |
// set up component state via useState hook | |
const [, setId] = React.useState(this._uuid); | |
// subscribe to svg replacement via useEffect hook | |
React.useEffect(() => { | |
const onSvgReplaced = () => { | |
setId(this._uuid); | |
}; | |
this._svgReplaced.connect(onSvgReplaced); | |
// specify cleanup callback as hook return | |
return () => { | |
this._svgReplaced.disconnect(onSvgReplaced); | |
}; | |
}); | |
// make it so that tag can be used as a jsx component | |
const Tag = tag; | |
// ensure that svg html is valid | |
if (!(this.svgInnerHTML && this.svgReactAttrs)) { | |
// bail if failing silently | |
return <></>; | |
} | |
const svgComponent = ( | |
<svg | |
{...this.svgReactAttrs} | |
dangerouslySetInnerHTML={{ __html: this.svgInnerHTML }} | |
ref={ref} | |
/> | |
); | |
if (container) { | |
Private.initContainer({ container, className, styleProps, title }); | |
return ( | |
<React.Fragment> | |
{svgComponent} | |
{label} | |
</React.Fragment> | |
); | |
} else { | |
return ( | |
<Tag | |
className={classes( | |
className, | |
LabIconStyle.styleClass(styleProps) | |
)} | |
> | |
{svgComponent} | |
{label} | |
</Tag> | |
); | |
} | |
} | |
); | |
component.displayName = `LabIcon_${displayName}`; | |
return component; | |
} | |
protected _initRender({ | |
render, | |
unrender | |
}: Partial<VirtualElement.IRenderer>) { | |
if (render) { | |
this.render = render; | |
if (unrender) { | |
this.unrender = unrender; | |
} | |
} else if (unrender) { | |
console.warn( | |
'In _initRender, ignoring unrender arg since render is undefined' | |
); | |
} | |
} | |
protected _initSvg({ | |
title, | |
uuid | |
}: { title?: string; uuid?: string } = {}): HTMLElement | null { | |
const svgElement = LabIcon.resolveSvg(this); | |
if (!svgElement) { | |
// bail on null svg element | |
return svgElement; | |
} | |
if (svgElement.tagName !== 'parsererror') { | |
// svgElement is an actual svg node, augment it | |
svgElement.dataset.icon = this.name; | |
if (uuid) { | |
svgElement.dataset.iconId = uuid; | |
} | |
if (title) { | |
Private.setTitleSvg(svgElement, title); | |
} | |
} | |
return svgElement; | |
} | |
readonly name: string; | |
/** | |
* A React component that will create the icon. | |
* | |
* @param className - a string that will be used as the class | |
* of the container element. Overrides any existing class | |
* | |
* @param container - a preexisting DOM element that | |
* will be used as the container for the svg element | |
* | |
* @param label - text that will be displayed adjacent | |
* to the icon | |
* | |
* @param title - a tooltip for the icon | |
* | |
* @param tag - if container is not explicitly | |
* provided, this tag will be used when creating the container | |
* | |
* @param stylesheet - optional string naming a builtin icon | |
* stylesheet, for example 'menuItem' or `statusBar`. Can also be an | |
* object defining a custom icon stylesheet, or a list of builtin | |
* stylesheet names and/or custom stylesheet objects. If array, | |
* the given stylesheets will be merged. | |
* | |
* See @jupyterlab/ui-components/src/style/icon.ts for details | |
* | |
* @param elementPosition - optional position for the inner svg element | |
* | |
* @param elementSize - optional size for the inner svg element. | |
* Set to 'normal' to get a standard 16px x 16px icon | |
* | |
* @param ...elementCSS - all additional args are treated as | |
* overrides for the CSS props applied to the inner svg element | |
* | |
* @param ref - forwarded to the ref prop of the icon's svg element | |
*/ | |
readonly react: LabIcon.IReact; | |
protected _className: string; | |
protected _loading: boolean; | |
protected _props: LabIcon.IProps = {}; | |
protected _svgReplaced = new Signal<this, void>(this); | |
protected _svgstr: string; | |
protected _uuid: string; | |
/** | |
* Cache for svg parsing intermediates | |
* - undefined: the cache has not yet been populated | |
* - null: a valid, but empty, value | |
*/ | |
protected _svgElement: HTMLElement | null | undefined = undefined; | |
protected _svgInnerHTML: string | null | undefined = undefined; | |
protected _svgReactAttrs: any | null | undefined = undefined; | |
} | |
/** | |
* A namespace for LabIcon statics. | |
*/ | |
export namespace LabIcon { | |
/** *********** | |
* interfaces * | |
*************/ | |
/** | |
* The simplest possible interface for defining a generic icon. | |
*/ | |
export interface IIcon { | |
/** | |
* The name of the icon. By convention, the icon name will be namespaced | |
* as so: | |
* | |
* "pkg-name:icon-name" | |
*/ | |
readonly name: string; | |
/** | |
* A string containing the raw contents of an svg file. | |
*/ | |
svgstr: string; | |
} | |
export interface IRendererOptions { | |
attrs?: ElementAttrs; | |
children?: ReadonlyArray<VirtualNode>; | |
props?: IProps; | |
} | |
/** | |
* The ILabIcon interface. Outside of this interface the actual | |
* implementation of LabIcon may vary | |
*/ | |
export interface ILabIcon extends IIcon, VirtualElement.IRenderer {} | |
/** | |
* Interface defining the parameters to be passed to the LabIcon | |
* constructor | |
*/ | |
export interface IOptions extends IIcon, Partial<VirtualElement.IRenderer> {} | |
/** | |
* The input props for creating a new LabIcon | |
*/ | |
export interface IProps extends LabIconStyle.IProps { | |
/** | |
* 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; | |
/** | |
* Optional text label that will be added as a sibling to the icon's | |
* svg node | |
*/ | |
label?: string; | |
/** | |
* 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 outermost container node | |
*/ | |
title?: string; | |
} | |
export interface IResolverProps { | |
icon?: IMaybeResolvable; | |
iconClass?: string; | |
fallback?: LabIcon; | |
} | |
/** ****** | |
* types * | |
*********/ | |
/** | |
* A type that can be resolved to a LabIcon instance. | |
*/ | |
export type IResolvable = | |
| string | |
| (IIcon & Partial<VirtualElement.IRenderer>); | |
/** | |
* A type that maybe can be resolved to a LabIcon instance. | |
*/ | |
export type IMaybeResolvable = | |
| IResolvable | |
| VirtualElement.IRenderer | |
| undefined; | |
/** | |
* The type of the svg node ref that can be passed into icon React components | |
*/ | |
export type IReactRef = React.RefObject<SVGElement>; | |
/** | |
* The properties that can be passed into the React component stored in | |
* the .react field of a LabIcon. | |
*/ | |
export type IReactProps = IProps & React.RefAttributes<SVGElement>; | |
/** | |
* The complete type of the React component stored in the .react | |
* field of a LabIcon. | |
*/ | |
export type IReact = React.ForwardRefExoticComponent<IReactProps>; | |
} | |
namespace Private { | |
export function blankElement({ | |
className = '', | |
container, | |
label, | |
title, | |
tag = 'div', | |
...styleProps | |
}: LabIcon.IProps): HTMLElement { | |
if (container?.className === className) { | |
// nothing needs doing, return the icon node | |
return container; | |
} | |
if (container) { | |
// take ownership by removing any existing children | |
while (container.firstChild) { | |
container.firstChild.remove(); | |
} | |
} else { | |
// create a container if needed | |
container = document.createElement(tag); | |
} | |
if (label != null) { | |
container.textContent = label; | |
} | |
Private.initContainer({ container, className, styleProps, title }); | |
return container; | |
} | |
export const blankReact = React.forwardRef( | |
( | |
{ | |
className = '', | |
container, | |
label, | |
title, | |
tag = 'div', | |
...styleProps | |
}: LabIcon.IProps, | |
ref: LabIcon.IReactRef | |
) => { | |
// make it so that tag can be used as a jsx component | |
const Tag = tag; | |
if (container) { | |
initContainer({ container, className, styleProps, title }); | |
return <></>; | |
} else { | |
// if ref is defined, we create a blank svg node and point ref to it | |
return ( | |
<Tag | |
className={classes(className, LabIconStyle.styleClass(styleProps))} | |
> | |
{ref && blankIcon.react({ ref })} | |
{label} | |
</Tag> | |
); | |
} | |
} | |
); | |
blankReact.displayName = 'BlankReact'; | |
export function initContainer({ | |
container, | |
className, | |
styleProps, | |
title | |
}: { | |
container: HTMLElement; | |
className?: string; | |
styleProps?: LabIconStyle.IProps; | |
title?: string; | |
}): string { | |
if (title != null) { | |
container.title = title; | |
} | |
const styleClass = LabIconStyle.styleClass(styleProps); | |
if (className != null) { | |
// override the container class with explicitly passed-in class + style class | |
const classResolved = classes(className, styleClass); | |
container.className = classResolved; | |
return classResolved; | |
} else if (styleClass) { | |
// add the style class to the container class | |
container.classList.add(styleClass); | |
return styleClass; | |
} else { | |
return ''; | |
} | |
} | |
export function isResolvable( | |
icon: LabIcon.IMaybeResolvable | |
): icon is LabIcon.IResolvable { | |
return !!( | |
icon && | |
(typeof icon === 'string' || | |
((icon as LabIcon.IIcon).name && (icon as LabIcon.IIcon).svgstr)) | |
); | |
} | |
export function setTitleSvg(svgNode: HTMLElement, title: string): void { | |
// add a title node to the top level svg node | |
const titleNodes = svgNode.getElementsByTagName('title'); | |
if (titleNodes.length) { | |
titleNodes[0].textContent = title; | |
} else { | |
const titleNode = document.createElement('title'); | |
titleNode.textContent = title; | |
svgNode.appendChild(titleNode); | |
} | |
} | |
/** | |
* A shim for svgstrs loaded using any loader other than raw-loader. | |
* This function assumes that svgstr will look like one of: | |
* | |
* - the raw contents of an .svg file: | |
* <svg...</svg> | |
* | |
* - a data URL: | |
* data:[<mediatype>][;base64],<svg...</svg> | |
* | |
* See https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs | |
*/ | |
export function svgstrShim(svgstr: string, strict: boolean = true): string { | |
// decode any uri escaping, condense leading/lagging whitespace, | |
// then match to raw svg string | |
const [, base64, raw] = decodeURIComponent(svgstr) | |
.replace(/>\s*\n\s*</g, '><') | |
.replace(/\s*\n\s*/g, ' ') | |
.match( | |
strict | |
? // match based on data url schema | |
/^(?:data:.*?(;base64)?,)?(.*)/ | |
: // match based on open of svg tag | |
/(?:(base64).*)?(<svg.*)/ | |
)!; | |
// decode from base64, if needed | |
return base64 ? atob(raw) : raw; | |
} | |
/** | |
* TODO: figure out story for independent Renderers. | |
* Base implementation of IRenderer. | |
*/ | |
export class Renderer implements VirtualElement.IRenderer { | |
constructor( | |
protected _icon: LabIcon, | |
protected _rendererOptions?: LabIcon.IRendererOptions | |
) {} | |
// eslint-disable-next-line | |
render(container: HTMLElement, options?: LabIcon.IRendererOptions): void {} | |
unrender?(container: HTMLElement, options?: LabIcon.IRendererOptions): void; | |
} | |
/** | |
* TODO: figure out story for independent Renderers. | |
* Implementation of IRenderer that creates the icon svg node | |
* as a DOM element. | |
*/ | |
export class ElementRenderer extends Renderer { | |
render(container: HTMLElement, options?: LabIcon.IRendererOptions): void { | |
let label = options?.children?.[0]; | |
// narrow type of label | |
if (typeof label !== 'string') { | |
label = undefined; | |
} | |
this._icon.element({ | |
container, | |
label, | |
...this._rendererOptions?.props, | |
...options?.props | |
}); | |
} | |
} | |
/** | |
* TODO: figure out story for independent Renderers. | |
* Implementation of IRenderer that creates the icon svg node | |
* as a React component. | |
*/ | |
export class ReactRenderer extends Renderer { | |
render(container: HTMLElement, options?: LabIcon.IRendererOptions): void { | |
let label = options?.children?.[0]; | |
// narrow type of label | |
if (typeof label !== 'string') { | |
label = undefined; | |
} | |
ReactDOM.render( | |
<this._icon.react | |
container={container} | |
label={label} | |
{...{ ...this._rendererOptions?.props, ...options?.props }} | |
/>, | |
container | |
); | |
} | |
unrender(container: HTMLElement): void { | |
ReactDOM.unmountComponentAtNode(container); | |
} | |
} | |
} | |
// need to be at the bottom since constructor depends on Private | |
export const badIcon = new LabIcon({ | |
name: 'ui-components:bad', | |
svgstr: badSvgstr | |
}); | |
export const blankIcon = new LabIcon({ | |
name: 'ui-components:blank', | |
svgstr: blankSvgstr | |
}); |