pluginlist.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. /* -----------------------------------------------------------------------------
  2. | Copyright (c) Jupyter Development Team.
  3. | Distributed under the terms of the Modified BSD License.
  4. |----------------------------------------------------------------------------*/
  5. import { ISettingRegistry } from '@jupyterlab/settingregistry';
  6. import { nullTranslator, ITranslator } from '@jupyterlab/translation';
  7. import { classes, LabIcon, settingsIcon } from '@jupyterlab/ui-components';
  8. import { Message } from '@lumino/messaging';
  9. import { ISignal, Signal } from '@lumino/signaling';
  10. import { Widget } from '@lumino/widgets';
  11. import * as React from 'react';
  12. import * as ReactDOM from 'react-dom';
  13. /**
  14. * A list of plugins with editable settings.
  15. */
  16. export class PluginList extends Widget {
  17. /**
  18. * Create a new plugin list.
  19. */
  20. constructor(options: PluginList.IOptions) {
  21. super();
  22. this.registry = options.registry;
  23. this.translator = options.translator || nullTranslator;
  24. this.addClass('jp-PluginList');
  25. this._confirm = options.confirm;
  26. this.registry.pluginChanged.connect(() => {
  27. this.update();
  28. }, this);
  29. }
  30. /**
  31. * The setting registry.
  32. */
  33. readonly registry: ISettingRegistry;
  34. /**
  35. * A signal emitted when a list user interaction happens.
  36. */
  37. get changed(): ISignal<this, void> {
  38. return this._changed;
  39. }
  40. /**
  41. * The selection value of the plugin list.
  42. */
  43. get scrollTop(): number | undefined {
  44. return this.node.querySelector('ul')?.scrollTop;
  45. }
  46. /**
  47. * The selection value of the plugin list.
  48. */
  49. get selection(): string {
  50. return this._selection;
  51. }
  52. set selection(selection: string) {
  53. if (this._selection === selection) {
  54. return;
  55. }
  56. this._selection = selection;
  57. this.update();
  58. }
  59. /**
  60. * Handle the DOM events for the widget.
  61. *
  62. * @param event - The DOM event sent to the widget.
  63. *
  64. * #### Notes
  65. * This method implements the DOM `EventListener` interface and is
  66. * called in response to events on the plugin list's node. It should
  67. * not be called directly by user code.
  68. */
  69. handleEvent(event: Event): void {
  70. switch (event.type) {
  71. case 'mousedown':
  72. this._evtMousedown(event as MouseEvent);
  73. break;
  74. default:
  75. break;
  76. }
  77. }
  78. /**
  79. * Handle `'after-attach'` messages.
  80. */
  81. protected onAfterAttach(msg: Message): void {
  82. this.node.addEventListener('mousedown', this);
  83. this.update();
  84. }
  85. /**
  86. * Handle `before-detach` messages for the widget.
  87. */
  88. protected onBeforeDetach(msg: Message): void {
  89. this.node.removeEventListener('mousedown', this);
  90. }
  91. /**
  92. * Handle `'update-request'` messages.
  93. */
  94. protected onUpdateRequest(msg: Message): void {
  95. const { node, registry } = this;
  96. const selection = this._selection;
  97. const translation = this.translator;
  98. Private.populateList(registry, selection, node, translation);
  99. const ul = node.querySelector('ul');
  100. if (ul && this._scrollTop !== undefined) {
  101. ul.scrollTop = this._scrollTop;
  102. }
  103. }
  104. /**
  105. * Handle the `'mousedown'` event for the plugin list.
  106. *
  107. * @param event - The DOM event sent to the widget
  108. */
  109. private _evtMousedown(event: MouseEvent): void {
  110. event.preventDefault();
  111. let target = event.target as HTMLElement;
  112. let id = target.getAttribute('data-id');
  113. if (id === this._selection) {
  114. return;
  115. }
  116. if (!id) {
  117. while (!id && target !== this.node) {
  118. target = target.parentElement as HTMLElement;
  119. id = target.getAttribute('data-id');
  120. }
  121. }
  122. if (!id) {
  123. return;
  124. }
  125. this._confirm()
  126. .then(() => {
  127. this._scrollTop = this.scrollTop;
  128. this._selection = id!;
  129. this._changed.emit(undefined);
  130. this.update();
  131. })
  132. .catch(() => {
  133. /* no op */
  134. });
  135. }
  136. protected translator: ITranslator;
  137. private _changed = new Signal<this, void>(this);
  138. private _confirm: () => Promise<void>;
  139. private _scrollTop: number | undefined = 0;
  140. private _selection = '';
  141. }
  142. /**
  143. * A namespace for `PluginList` statics.
  144. */
  145. export namespace PluginList {
  146. /**
  147. * The instantiation options for a plugin list.
  148. */
  149. export interface IOptions {
  150. /**
  151. * A function that allows for asynchronously confirming a selection.
  152. *
  153. * #### Notest
  154. * If the promise returned by the function resolves, then the selection will
  155. * succeed and emit an event. If the promise rejects, the selection is not
  156. * made.
  157. */
  158. confirm: () => Promise<void>;
  159. /**
  160. * The setting registry for the plugin list.
  161. */
  162. registry: ISettingRegistry;
  163. /**
  164. * The setting registry for the plugin list.
  165. */
  166. translator?: ITranslator;
  167. }
  168. }
  169. /**
  170. * A namespace for private module data.
  171. */
  172. namespace Private {
  173. /**
  174. * The JupyterLab plugin schema key for the setting editor
  175. * icon class of a plugin.
  176. */
  177. const ICON_KEY = 'jupyter.lab.setting-icon';
  178. /**
  179. * The JupyterLab plugin schema key for the setting editor
  180. * icon class of a plugin.
  181. */
  182. const ICON_CLASS_KEY = 'jupyter.lab.setting-icon-class';
  183. /**
  184. * The JupyterLab plugin schema key for the setting editor
  185. * icon label of a plugin.
  186. */
  187. const ICON_LABEL_KEY = 'jupyter.lab.setting-icon-label';
  188. /**
  189. * Check the plugin for a rendering hint's value.
  190. *
  191. * #### Notes
  192. * The order of priority for overridden hints is as follows, from most
  193. * important to least:
  194. * 1. Data set by the end user in a settings file.
  195. * 2. Data set by the plugin author as a schema default.
  196. * 3. Data set by the plugin author as a top-level key of the schema.
  197. */
  198. function getHint(
  199. key: string,
  200. registry: ISettingRegistry,
  201. plugin: ISettingRegistry.IPlugin
  202. ): string {
  203. // First, give priority to checking if the hint exists in the user data.
  204. let hint = plugin.data.user[key];
  205. // Second, check to see if the hint exists in composite data, which folds
  206. // in default values from the schema.
  207. if (!hint) {
  208. hint = plugin.data.composite[key];
  209. }
  210. // Third, check to see if the plugin schema has defined the hint.
  211. if (!hint) {
  212. hint = plugin.schema[key];
  213. }
  214. // Finally, use the defaults from the registry schema.
  215. if (!hint) {
  216. const { properties } = registry.schema;
  217. hint = properties && properties[key] && properties[key].default;
  218. }
  219. return typeof hint === 'string' ? hint : '';
  220. }
  221. /**
  222. * Populate the plugin list.
  223. */
  224. export function populateList(
  225. registry: ISettingRegistry,
  226. selection: string,
  227. node: HTMLElement,
  228. translator?: ITranslator
  229. ): void {
  230. translator = translator || nullTranslator;
  231. const trans = translator.load('jupyterlab');
  232. const plugins = sortPlugins(registry).filter(plugin => {
  233. const { schema } = plugin;
  234. const deprecated = schema['jupyter.lab.setting-deprecated'] === true;
  235. const editable = Object.keys(schema.properties || {}).length > 0;
  236. const extensible = schema.additionalProperties !== false;
  237. return !deprecated && (editable || extensible);
  238. });
  239. const items = plugins.map(plugin => {
  240. const { id, schema, version } = plugin;
  241. const title =
  242. typeof schema.title === 'string'
  243. ? trans._p('schema', schema.title)
  244. : id;
  245. const description =
  246. typeof schema.description === 'string'
  247. ? trans._p('schema', schema.description)
  248. : '';
  249. const itemTitle = `${description}\n${id}\n${version}`;
  250. const icon = getHint(ICON_KEY, registry, plugin);
  251. const iconClass = getHint(ICON_CLASS_KEY, registry, plugin);
  252. const iconTitle = getHint(ICON_LABEL_KEY, registry, plugin);
  253. return (
  254. <li
  255. className={id === selection ? 'jp-mod-selected' : ''}
  256. data-id={id}
  257. key={id}
  258. title={itemTitle}
  259. >
  260. <LabIcon.resolveReact
  261. icon={icon || (iconClass ? undefined : settingsIcon)}
  262. iconClass={classes(iconClass, 'jp-Icon')}
  263. title={iconTitle}
  264. tag="span"
  265. stylesheet="settingsEditor"
  266. />
  267. <span>{title}</span>
  268. </li>
  269. );
  270. });
  271. ReactDOM.unmountComponentAtNode(node);
  272. ReactDOM.render(<ul>{items}</ul>, node);
  273. }
  274. /**
  275. * Sort a list of plugins by title and ID.
  276. */
  277. function sortPlugins(registry: ISettingRegistry): ISettingRegistry.IPlugin[] {
  278. return Object.keys(registry.plugins)
  279. .map(plugin => registry.plugins[plugin]!)
  280. .sort((a, b) => {
  281. return (a.schema.title || a.id).localeCompare(b.schema.title || b.id);
  282. });
  283. }
  284. }