iconregistry.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import React from 'react';
  4. import { classes } from 'typestyle/lib';
  5. import { camelize } from '../utils';
  6. import { IIconStyle, iconStyle, iconStyleFlat } from '../style/icon';
  7. /**
  8. * To add an icon to the defaultIconRegistry requires two lines of code:
  9. * 1. import the icon's .svg
  10. * 2. add a relevant entry to _defaultIcons
  11. */
  12. /* tslint:disable */
  13. import badSvg from '../../style/icons/bad.svg';
  14. import jupyterFaviconSvg from '../../style/icons/jupyter-favicon.svg';
  15. // filetype icons
  16. import html5Svg from '../../style/icons/filetype/html5.svg';
  17. // statusbar icons
  18. import kernelSvg from '../../style/icons/statusbar/kernel.svg';
  19. import lineFormSvg from '../../style/icons/statusbar/line-form.svg';
  20. import notTrustedSvg from '../../style/icons/statusbar/not-trusted.svg';
  21. import statusBarSvg from '../../style/icons/statusbar/status-bar.svg';
  22. import terminalSvg from '../../style/icons/statusbar/terminal.svg';
  23. import trustedSvg from '../../style/icons/statusbar/trusted.svg';
  24. // sidebar icons
  25. import buildSvg from '../../style/icons/sidebar/build.svg'; // originally ic-build-24px.svg
  26. import extensionSvg from '../../style/icons/sidebar/extension.svg'; // originally ic-extension-24px.svg
  27. import folderSvg from '../../style/icons/sidebar/folder.svg'; // originally ic-folder-24px.svg
  28. import paletteSvg from '../../style/icons/sidebar/palette.svg'; // originally ic-palette-24px.svg
  29. import runningSvg from '../../style/icons/sidebar/running.svg'; // originally stop-circle.svg
  30. import tabSvg from '../../style/icons/sidebar/tab.svg'; // originally ic-tab-24px.svg
  31. const _defaultIcons: ReadonlyArray<IconRegistry.IModel> = [
  32. { name: 'jupyter-favicon', svg: jupyterFaviconSvg },
  33. { name: 'html5', svg: html5Svg },
  34. { name: 'kernel', svg: kernelSvg },
  35. { name: 'line-form', svg: lineFormSvg },
  36. { name: 'not-trusted', svg: notTrustedSvg },
  37. { name: 'status-bar', svg: statusBarSvg },
  38. { name: 'terminal', svg: terminalSvg },
  39. { name: 'trusted', svg: trustedSvg },
  40. { name: 'build', svg: buildSvg },
  41. { name: 'extension', svg: extensionSvg },
  42. { name: 'folder', svg: folderSvg },
  43. { name: 'palette', svg: paletteSvg },
  44. { name: 'running', svg: runningSvg },
  45. { name: 'tab', svg: tabSvg }
  46. ];
  47. /* tslint:enable */
  48. /**
  49. * The icon registry class.
  50. */
  51. export class IconRegistry {
  52. constructor(...icons: IconRegistry.IModel[]) {
  53. if (icons.length) {
  54. this.addIcon(...icons);
  55. } else {
  56. this.addIcon(..._defaultIcons);
  57. }
  58. // add the bad state icon
  59. this.addIcon({ name: 'bad', svg: badSvg });
  60. }
  61. addIcon(...icons: IconRegistry.IModel[]): void {
  62. icons.forEach((icon: IconRegistry.IModel) => {
  63. let className = icon.className
  64. ? icon.className
  65. : IconRegistry.iconClassName(icon.name);
  66. this._classNameToName[className] = icon.name;
  67. this._nameToClassName[icon.name] = className;
  68. this._svg[icon.name] = icon.svg;
  69. });
  70. }
  71. resolveName(name: string): string {
  72. if (!(name in this._svg)) {
  73. // assume name is really a className, split the className into parts and check each part
  74. for (let className of name.split(/\s+/)) {
  75. if (className in this._classNameToName) {
  76. return this._nameToClassName[className];
  77. }
  78. }
  79. // couldn't resolve name, mark as bad
  80. return 'bad';
  81. }
  82. return name;
  83. }
  84. /**
  85. * Get the icon as an HTMLElement of tag <svg><svg/>
  86. */
  87. icon(
  88. props: IconRegistry.IIconOptions & { container: HTMLElement } & IIconStyle
  89. ): HTMLElement {
  90. const { name, className, title, skipbad, container, ...propsStyle } = props;
  91. // if name not in _svg, assume we've been handed a className in place of name
  92. let svg = this.svg(name, skipbad);
  93. if (!svg) {
  94. // bail
  95. return;
  96. }
  97. let svgNode = Private.parseSvg(svg);
  98. if (title) {
  99. Private.setTitleSvg(svgNode, title);
  100. }
  101. if (container) {
  102. // clear any existing icon in container (and all other child elements)
  103. container.textContent = '';
  104. container.appendChild(svgNode);
  105. let styleClass = propsStyle ? iconStyle(propsStyle) : '';
  106. if (className) {
  107. // override the className, if explicitly passed
  108. container.className = classes(className, styleClass);
  109. } else if (!(styleClass in container.classList)) {
  110. // add icon styling class to the container's class, if not already present
  111. container.className = classes(container.className, styleClass);
  112. }
  113. } else {
  114. // add icon styling class directly to the svg node
  115. svgNode.setAttribute(
  116. 'class',
  117. classes(className, propsStyle ? iconStyleFlat(propsStyle) : '')
  118. );
  119. }
  120. return svgNode;
  121. }
  122. /**
  123. * Get the icon as a ReactElement of tag <tag><svg><svg/><tag/>
  124. * TODO: figure out how to remove the unnecessary outer <tag>
  125. */
  126. iconReact(
  127. props: IconRegistry.IIconOptions & { tag?: 'div' | 'span' } & IIconStyle
  128. ): React.ReactElement {
  129. const { name, className, title, skipbad, tag, ...propsStyle } = props;
  130. const Tag = tag || 'div';
  131. let svg = this.svg(name, skipbad);
  132. if (!svg) {
  133. // bail
  134. return;
  135. }
  136. return (
  137. <Tag
  138. className={classes(className, propsStyle ? iconStyle(propsStyle) : '')}
  139. dangerouslySetInnerHTML={{ __html: svg }}
  140. />
  141. );
  142. }
  143. // override(
  144. // props: { className: string; name?: string; title?: string } & IIconStyle
  145. // ) {
  146. // let { name, className, title, ...propsStyle } = props;
  147. //
  148. // // try to resolve name
  149. // name = name ? name : this.classNameToName(className);
  150. // if (!name) {
  151. // // bail
  152. // return;
  153. // }
  154. //
  155. // for (let container of document.getElementsByClassName(
  156. // className
  157. // ) as HTMLCollectionOf<HTMLElement>) {
  158. // this.icon({
  159. // name: name,
  160. // title: title,
  161. // container: container,
  162. // ...propsStyle
  163. // });
  164. // }
  165. // }
  166. svg(name: string, skipbad: boolean = false): string {
  167. let svgname = this.resolveName(name);
  168. if (name === 'bad') {
  169. if (!skipbad) {
  170. // log a warning and mark missing icons with an X
  171. console.error(`Invalid icon name: ${name}`);
  172. } else {
  173. // silently return empty string
  174. return '';
  175. }
  176. }
  177. return this._svg[svgname];
  178. }
  179. static iconClassName(name: string): string {
  180. return 'jp-' + camelize(name, true) + 'Icon';
  181. }
  182. private _classNameToName: { [key: string]: string } = Object.create(null);
  183. private _nameToClassName: { [key: string]: string } = Object.create(null);
  184. private _svg: { [key: string]: string } = Object.create(null);
  185. }
  186. /**
  187. * The defaultIconRegistry instance.
  188. */
  189. export const defaultIconRegistry: IconRegistry = new IconRegistry();
  190. /**
  191. * Alias for defaultIconRegistry.iconReact that can be used as a React component
  192. */
  193. export const IconReact = (
  194. props: IconRegistry.IIconOptions & { tag?: 'div' | 'span' } & IIconStyle
  195. ): React.ReactElement => {
  196. return defaultIconRegistry.iconReact(props);
  197. };
  198. export namespace IconRegistry {
  199. export interface IModel {
  200. name: string;
  201. className?: string;
  202. svg: string;
  203. }
  204. export interface IIconOptions {
  205. name: string;
  206. className?: string;
  207. title?: string;
  208. skipbad?: boolean;
  209. }
  210. // needs the explicit type to avoid a typedoc issue
  211. export const defaultIcons: ReadonlyArray<IconRegistry.IModel> = _defaultIcons;
  212. }
  213. namespace Private {
  214. export function parseSvg(svg: string): HTMLElement {
  215. let parser = new DOMParser();
  216. return parser.parseFromString(svg, 'image/svg+xml').documentElement;
  217. }
  218. export function setTitleSvg(svgNode: HTMLElement, title: string): void {
  219. // add a title node to the top level svg node
  220. let titleNodes = svgNode.getElementsByTagName('title');
  221. if (titleNodes) {
  222. titleNodes[0].textContent = title;
  223. } else {
  224. let titleNode = document.createElement('title');
  225. titleNode.textContent = title;
  226. svgNode.appendChild(titleNode);
  227. }
  228. }
  229. }