123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- // 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';
- const blankDiv = document.createElement('div');
- 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 {
- for (let className of name.split(/\s+/)) {
- if (JLIcon._instances.has(className)) {
- return JLIcon._instances.get(className);
- }
- }
- // lookup failed
- 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,
- label,
- title,
- tag = 'div',
- ...propsStyle
- }: JLIcon.IProps = {}): HTMLElement {
- // 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 blankDiv;
- }
- 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;
- }
- if (label != null) {
- container.textContent = label;
- }
- this._initContainer({ container, className, propsStyle, title });
- // add the svg node to the container
- container.appendChild(svgElement);
- return ret;
- }
- recycle({
- className,
- container,
- ...propsStyle
- }: { className?: string; container: HTMLElement } & IIconStyle): HTMLElement {
- // clean up all children
- while (container.firstChild) {
- container.firstChild.remove();
- }
- // clean up any icon-related class names
- const cls = this.class({ className, ...propsStyle });
- if (cls) {
- container.classList.remove(cls);
- }
- return container;
- }
- render(host: HTMLElement, props: JLIcon.IProps = {}): void {
- // TODO: move this title fix to the Lumino side
- host.removeAttribute('title');
- 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;
- // structure of error element varies by browser, search at top level
- if (svgDoc.getElementsByTagName('parsererror').length > 0) {
- const errmsg = `SVG HTML was malformed for JLIcon instance.\nname: ${name}, svgstr: ${this._svgstr}`;
- // 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.icon = this.name;
- 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 != null) {
- // 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 != null) {
- container.title = title;
- }
- }
- protected _initReact() {
- const component = React.forwardRef(
- (
- {
- className,
- container,
- label,
- 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 (
- <React.Fragment>
- {svgComponent}
- {label}
- </React.Fragment>
- );
- } else {
- return (
- <Tag className={classes(className, iconStyle(propsStyle))}>
- {svgComponent}
- {label}
- </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;
- /**
- * 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 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 });
|