dialog.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { ArrayExt, each, map, toArray } from '@lumino/algorithm';
  4. import { PromiseDelegate } from '@lumino/coreutils';
  5. import { Message, MessageLoop } from '@lumino/messaging';
  6. import { PanelLayout, Panel, Widget } from '@lumino/widgets';
  7. import { closeIcon, Button, LabIcon } from '@jupyterlab/ui-components';
  8. import * as React from 'react';
  9. import { Styling } from './styling';
  10. import { ReactWidget } from './vdom';
  11. import { WidgetTracker } from './widgettracker';
  12. /**
  13. * Create and show a dialog.
  14. *
  15. * @param options - The dialog setup options.
  16. *
  17. * @returns A promise that resolves with whether the dialog was accepted.
  18. */
  19. export function showDialog<T>(
  20. options: Partial<Dialog.IOptions<T>> = {}
  21. ): Promise<Dialog.IResult<T>> {
  22. const dialog = new Dialog(options);
  23. return dialog.launch();
  24. }
  25. /**
  26. * Show an error message dialog.
  27. *
  28. * @param title - The title of the dialog box.
  29. *
  30. * @param error - the error to show in the dialog body (either a string
  31. * or an object with a string `message` property).
  32. */
  33. export function showErrorMessage(
  34. title: string,
  35. error: any,
  36. buttons: ReadonlyArray<Dialog.IButton> = [
  37. Dialog.okButton({ label: 'Dismiss' })
  38. ]
  39. ): Promise<void> {
  40. console.warn('Showing error:', error);
  41. // Cache promises to prevent multiple copies of identical dialogs showing
  42. // to the user.
  43. const body = typeof error === 'string' ? error : error.message;
  44. const key = title + '----' + body;
  45. const promise = Private.errorMessagePromiseCache.get(key);
  46. if (promise) {
  47. return promise;
  48. } else {
  49. const dialogPromise = showDialog({
  50. title: title,
  51. body: body,
  52. buttons: buttons
  53. }).then(
  54. () => {
  55. Private.errorMessagePromiseCache.delete(key);
  56. },
  57. error => {
  58. // TODO: Use .finally() above when supported
  59. Private.errorMessagePromiseCache.delete(key);
  60. throw error;
  61. }
  62. );
  63. Private.errorMessagePromiseCache.set(key, dialogPromise);
  64. return dialogPromise;
  65. }
  66. }
  67. /**
  68. * A modal dialog widget.
  69. */
  70. export class Dialog<T> extends Widget {
  71. /**
  72. * Create a dialog panel instance.
  73. *
  74. * @param options - The dialog setup options.
  75. */
  76. constructor(options: Partial<Dialog.IOptions<T>> = {}) {
  77. super();
  78. this.addClass('jp-Dialog');
  79. const normalized = Private.handleOptions(options);
  80. const renderer = normalized.renderer;
  81. this._host = normalized.host;
  82. this._defaultButton = normalized.defaultButton;
  83. this._buttons = normalized.buttons;
  84. this._buttonNodes = toArray(
  85. map(this._buttons, button => {
  86. return renderer.createButtonNode(button);
  87. })
  88. );
  89. const layout = (this.layout = new PanelLayout());
  90. const content = new Panel();
  91. content.addClass('jp-Dialog-content');
  92. layout.addWidget(content);
  93. this._body = normalized.body;
  94. const header = renderer.createHeader(
  95. normalized.title,
  96. () => this.reject(),
  97. options
  98. );
  99. const body = renderer.createBody(normalized.body);
  100. const footer = renderer.createFooter(this._buttonNodes);
  101. content.addWidget(header);
  102. content.addWidget(body);
  103. content.addWidget(footer);
  104. this._primary = this._buttonNodes[this._defaultButton];
  105. this._focusNodeSelector = options.focusNodeSelector;
  106. // Add new dialogs to the tracker.
  107. void Dialog.tracker.add(this);
  108. }
  109. /**
  110. * Dispose of the resources used by the dialog.
  111. */
  112. dispose(): void {
  113. const promise = this._promise;
  114. if (promise) {
  115. this._promise = null;
  116. promise.reject(void 0);
  117. ArrayExt.removeFirstOf(Private.launchQueue, promise.promise);
  118. }
  119. super.dispose();
  120. }
  121. /**
  122. * Launch the dialog as a modal window.
  123. *
  124. * @returns a promise that resolves with the result of the dialog.
  125. */
  126. launch(): Promise<Dialog.IResult<T>> {
  127. // Return the existing dialog if already open.
  128. if (this._promise) {
  129. return this._promise.promise;
  130. }
  131. const promise = (this._promise = new PromiseDelegate<Dialog.IResult<T>>());
  132. const promises = Promise.all(Private.launchQueue);
  133. Private.launchQueue.push(this._promise.promise);
  134. return promises.then(() => {
  135. // Do not show Dialog if it was disposed of before it was at the front of the launch queue
  136. if (!this._promise) {
  137. return Promise.resolve({ button: Dialog.cancelButton(), value: null });
  138. }
  139. Widget.attach(this, this._host);
  140. return promise.promise;
  141. });
  142. }
  143. /**
  144. * Resolve the current dialog.
  145. *
  146. * @param index - An optional index to the button to resolve.
  147. *
  148. * #### Notes
  149. * Will default to the defaultIndex.
  150. * Will resolve the current `show()` with the button value.
  151. * Will be a no-op if the dialog is not shown.
  152. */
  153. resolve(index?: number): void {
  154. if (!this._promise) {
  155. return;
  156. }
  157. if (index === undefined) {
  158. index = this._defaultButton;
  159. }
  160. this._resolve(this._buttons[index]);
  161. }
  162. /**
  163. * Reject the current dialog with a default reject value.
  164. *
  165. * #### Notes
  166. * Will be a no-op if the dialog is not shown.
  167. */
  168. reject(): void {
  169. if (!this._promise) {
  170. return;
  171. }
  172. this._resolve(Dialog.cancelButton());
  173. }
  174. /**
  175. * Handle the DOM events for the directory listing.
  176. *
  177. * @param event - The DOM event sent to the widget.
  178. *
  179. * #### Notes
  180. * This method implements the DOM `EventListener` interface and is
  181. * called in response to events on the panel's DOM node. It should
  182. * not be called directly by user code.
  183. */
  184. handleEvent(event: Event): void {
  185. switch (event.type) {
  186. case 'keydown':
  187. this._evtKeydown(event as KeyboardEvent);
  188. break;
  189. case 'click':
  190. this._evtClick(event as MouseEvent);
  191. break;
  192. case 'focus':
  193. this._evtFocus(event as FocusEvent);
  194. break;
  195. case 'contextmenu':
  196. event.preventDefault();
  197. event.stopPropagation();
  198. break;
  199. default:
  200. break;
  201. }
  202. }
  203. /**
  204. * A message handler invoked on an `'after-attach'` message.
  205. */
  206. protected onAfterAttach(msg: Message): void {
  207. const node = this.node;
  208. node.addEventListener('keydown', this, true);
  209. node.addEventListener('contextmenu', this, true);
  210. node.addEventListener('click', this, true);
  211. document.addEventListener('focus', this, true);
  212. this._first = Private.findFirstFocusable(this.node);
  213. this._original = document.activeElement as HTMLElement;
  214. if (this._focusNodeSelector) {
  215. const body = this.node.querySelector('.jp-Dialog-body');
  216. const el = body?.querySelector(this._focusNodeSelector);
  217. if (el) {
  218. this._primary = el as HTMLElement;
  219. }
  220. }
  221. this._primary.focus();
  222. }
  223. /**
  224. * A message handler invoked on an `'after-detach'` message.
  225. */
  226. protected onAfterDetach(msg: Message): void {
  227. const node = this.node;
  228. node.removeEventListener('keydown', this, true);
  229. node.removeEventListener('contextmenu', this, true);
  230. node.removeEventListener('click', this, true);
  231. document.removeEventListener('focus', this, true);
  232. this._original.focus();
  233. }
  234. /**
  235. * A message handler invoked on a `'close-request'` message.
  236. */
  237. protected onCloseRequest(msg: Message): void {
  238. if (this._promise) {
  239. this.reject();
  240. }
  241. super.onCloseRequest(msg);
  242. }
  243. /**
  244. * Handle the `'click'` event for a dialog button.
  245. *
  246. * @param event - The DOM event sent to the widget
  247. */
  248. protected _evtClick(event: MouseEvent): void {
  249. const content = this.node.getElementsByClassName(
  250. 'jp-Dialog-content'
  251. )[0] as HTMLElement;
  252. if (!content.contains(event.target as HTMLElement)) {
  253. event.stopPropagation();
  254. event.preventDefault();
  255. this.reject();
  256. return;
  257. }
  258. for (const buttonNode of this._buttonNodes) {
  259. if (buttonNode.contains(event.target as HTMLElement)) {
  260. const index = this._buttonNodes.indexOf(buttonNode);
  261. this.resolve(index);
  262. }
  263. }
  264. }
  265. /**
  266. * Handle the `'keydown'` event for the widget.
  267. *
  268. * @param event - The DOM event sent to the widget
  269. */
  270. protected _evtKeydown(event: KeyboardEvent): void {
  271. // Check for escape key
  272. switch (event.keyCode) {
  273. case 27: // Escape.
  274. event.stopPropagation();
  275. event.preventDefault();
  276. this.reject();
  277. break;
  278. case 9: {
  279. // Tab.
  280. // Handle a tab on the last button.
  281. const node = this._buttonNodes[this._buttons.length - 1];
  282. if (document.activeElement === node && !event.shiftKey) {
  283. event.stopPropagation();
  284. event.preventDefault();
  285. this._first.focus();
  286. }
  287. break;
  288. }
  289. case 13: // Enter.
  290. event.stopPropagation();
  291. event.preventDefault();
  292. this.resolve();
  293. break;
  294. default:
  295. break;
  296. }
  297. }
  298. /**
  299. * Handle the `'focus'` event for the widget.
  300. *
  301. * @param event - The DOM event sent to the widget
  302. */
  303. protected _evtFocus(event: FocusEvent): void {
  304. const target = event.target as HTMLElement;
  305. if (!this.node.contains(target as HTMLElement)) {
  306. event.stopPropagation();
  307. this._buttonNodes[this._defaultButton].focus();
  308. }
  309. }
  310. /**
  311. * Resolve a button item.
  312. */
  313. private _resolve(button: Dialog.IButton): void {
  314. // Prevent loopback.
  315. const promise = this._promise;
  316. if (!promise) {
  317. this.dispose();
  318. return;
  319. }
  320. this._promise = null;
  321. ArrayExt.removeFirstOf(Private.launchQueue, promise.promise);
  322. const body = this._body;
  323. let value: T | null = null;
  324. if (
  325. button.accept &&
  326. body instanceof Widget &&
  327. typeof body.getValue === 'function'
  328. ) {
  329. value = body.getValue();
  330. }
  331. this.dispose();
  332. promise.resolve({ button, value });
  333. }
  334. private _buttonNodes: ReadonlyArray<HTMLElement>;
  335. private _buttons: ReadonlyArray<Dialog.IButton>;
  336. private _original: HTMLElement;
  337. private _first: HTMLElement;
  338. private _primary: HTMLElement;
  339. private _promise: PromiseDelegate<Dialog.IResult<T>> | null;
  340. private _defaultButton: number;
  341. private _host: HTMLElement;
  342. private _body: Dialog.Body<T>;
  343. private _focusNodeSelector: string | undefined = '';
  344. }
  345. /**
  346. * The namespace for Dialog class statics.
  347. */
  348. export namespace Dialog {
  349. /**
  350. * The body input types.
  351. */
  352. export type Body<T> = IBodyWidget<T> | React.ReactElement<any> | string;
  353. /**
  354. * The header input types.
  355. */
  356. export type Header = React.ReactElement<any> | string;
  357. /**
  358. * A widget used as a dialog body.
  359. */
  360. export interface IBodyWidget<T = string> extends Widget {
  361. /**
  362. * Get the serialized value of the widget.
  363. */
  364. getValue?(): T;
  365. }
  366. /**
  367. * The options used to make a button item.
  368. */
  369. export interface IButton {
  370. /**
  371. * The label for the button.
  372. */
  373. label: string;
  374. /**
  375. * The icon class for the button.
  376. */
  377. iconClass: string;
  378. /**
  379. * The icon label for the button.
  380. */
  381. iconLabel: string;
  382. /**
  383. * The caption for the button.
  384. */
  385. caption: string;
  386. /**
  387. * The extra class name for the button.
  388. */
  389. className: string;
  390. /**
  391. * The dialog action to perform when the button is clicked.
  392. */
  393. accept: boolean;
  394. /**
  395. * The additional dialog actions to perform when the button is clicked.
  396. */
  397. actions: Array<string>;
  398. /**
  399. * The button display type.
  400. */
  401. displayType: 'default' | 'warn';
  402. }
  403. /**
  404. * The options used to create a dialog.
  405. */
  406. export interface IOptions<T> {
  407. /**
  408. * The top level text for the dialog. Defaults to an empty string.
  409. */
  410. title: Header;
  411. /**
  412. * The main body element for the dialog or a message to display.
  413. * Defaults to an empty string.
  414. *
  415. * #### Notes
  416. * If a widget is given as the body, it will be disposed after the
  417. * dialog is resolved. If the widget has a `getValue()` method,
  418. * the method will be called prior to disposal and the value
  419. * will be provided as part of the dialog result.
  420. * A string argument will be used as raw `textContent`.
  421. * All `input` and `select` nodes will be wrapped and styled.
  422. */
  423. body: Body<T>;
  424. /**
  425. * The host element for the dialog. Defaults to `document.body`.
  426. */
  427. host: HTMLElement;
  428. /**
  429. * The buttons to display. Defaults to cancel and accept buttons.
  430. */
  431. buttons: ReadonlyArray<IButton>;
  432. /**
  433. * The index of the default button. Defaults to the last button.
  434. */
  435. defaultButton: number;
  436. /**
  437. * A selector for the primary element that should take focus in the dialog.
  438. * Defaults to an empty string, causing the [[defaultButton]] to take
  439. * focus.
  440. */
  441. focusNodeSelector: string;
  442. /**
  443. * When "true", renders a close button for the dialog
  444. */
  445. hasClose: boolean;
  446. /**
  447. * An optional renderer for dialog items. Defaults to a shared
  448. * default renderer.
  449. */
  450. renderer: IRenderer;
  451. }
  452. /**
  453. * A dialog renderer.
  454. */
  455. export interface IRenderer {
  456. /**
  457. * Create the header of the dialog.
  458. *
  459. * @param title - The title of the dialog.
  460. *
  461. * @returns A widget for the dialog header.
  462. */
  463. createHeader<T>(
  464. title: Header,
  465. reject: () => void,
  466. options: Partial<Dialog.IOptions<T>>
  467. ): Widget;
  468. /**
  469. * Create the body of the dialog.
  470. *
  471. * @param value - The input value for the body.
  472. *
  473. * @returns A widget for the body.
  474. */
  475. createBody(body: Body<any>): Widget;
  476. /**
  477. * Create the footer of the dialog.
  478. *
  479. * @param buttons - The button nodes to add to the footer.
  480. *
  481. * @returns A widget for the footer.
  482. */
  483. createFooter(buttons: ReadonlyArray<HTMLElement>): Widget;
  484. /**
  485. * Create a button node for the dialog.
  486. *
  487. * @param button - The button data.
  488. *
  489. * @returns A node for the button.
  490. */
  491. createButtonNode(button: IButton): HTMLElement;
  492. }
  493. /**
  494. * The result of a dialog.
  495. */
  496. export interface IResult<T> {
  497. /**
  498. * The button that was pressed.
  499. */
  500. button: IButton;
  501. /**
  502. * The value retrieved from `.getValue()` if given on the widget.
  503. */
  504. value: T | null;
  505. }
  506. /**
  507. * Create a button item.
  508. */
  509. export function createButton(value: Partial<IButton>): Readonly<IButton> {
  510. value.accept = value.accept !== false;
  511. const defaultLabel = value.accept ? 'OK' : 'Cancel';
  512. return {
  513. label: value.label || defaultLabel,
  514. iconClass: value.iconClass || '',
  515. iconLabel: value.iconLabel || '',
  516. caption: value.caption || '',
  517. className: value.className || '',
  518. accept: value.accept,
  519. actions: value.actions || [],
  520. displayType: value.displayType || 'default'
  521. };
  522. }
  523. /**
  524. * Create a reject button.
  525. */
  526. export function cancelButton(
  527. options: Partial<IButton> = {}
  528. ): Readonly<IButton> {
  529. options.accept = false;
  530. return createButton(options);
  531. }
  532. /**
  533. * Create an accept button.
  534. */
  535. export function okButton(options: Partial<IButton> = {}): Readonly<IButton> {
  536. options.accept = true;
  537. return createButton(options);
  538. }
  539. /**
  540. * Create a warn button.
  541. */
  542. export function warnButton(
  543. options: Partial<IButton> = {}
  544. ): Readonly<IButton> {
  545. options.displayType = 'warn';
  546. return createButton(options);
  547. }
  548. /**
  549. * Disposes all dialog instances.
  550. *
  551. * #### Notes
  552. * This function should only be used in tests or cases where application state
  553. * may be discarded.
  554. */
  555. export function flush(): void {
  556. tracker.forEach(dialog => {
  557. dialog.dispose();
  558. });
  559. }
  560. /**
  561. * The default implementation of a dialog renderer.
  562. */
  563. export class Renderer {
  564. /**
  565. * Create the header of the dialog.
  566. *
  567. * @param title - The title of the dialog.
  568. *
  569. * @returns A widget for the dialog header.
  570. */
  571. createHeader<T>(
  572. title: Header,
  573. reject: () => void = () => {
  574. /* empty */
  575. },
  576. options: Partial<Dialog.IOptions<T>> = {}
  577. ): Widget {
  578. let header: Widget;
  579. const handleMouseDown = (event: React.MouseEvent) => {
  580. // Fire action only when left button is pressed.
  581. if (event.button === 0) {
  582. event.preventDefault();
  583. reject();
  584. }
  585. };
  586. const handleKeyDown = (event: React.KeyboardEvent) => {
  587. const { key } = event;
  588. if (key === 'Enter' || key === ' ') {
  589. reject();
  590. }
  591. };
  592. if (typeof title === 'string') {
  593. header = ReactWidget.create(
  594. <>
  595. {title}
  596. {options.hasClose && (
  597. <Button
  598. className="jp-Dialog-close-button"
  599. onMouseDown={handleMouseDown}
  600. onKeyDown={handleKeyDown}
  601. title="Cancel"
  602. minimal
  603. >
  604. <LabIcon.resolveReact
  605. icon={closeIcon}
  606. iconClass="jp-Icon"
  607. className="jp-ToolbarButtonComponent-icon"
  608. tag="span"
  609. />
  610. </Button>
  611. )}
  612. </>
  613. );
  614. } else {
  615. header = ReactWidget.create(title);
  616. }
  617. header.addClass('jp-Dialog-header');
  618. Styling.styleNode(header.node);
  619. return header;
  620. }
  621. /**
  622. * Create the body of the dialog.
  623. *
  624. * @param value - The input value for the body.
  625. *
  626. * @returns A widget for the body.
  627. */
  628. createBody(value: Body<any>): Widget {
  629. let body: Widget;
  630. if (typeof value === 'string') {
  631. body = new Widget({ node: document.createElement('span') });
  632. body.node.textContent = value;
  633. } else if (value instanceof Widget) {
  634. body = value;
  635. } else {
  636. body = ReactWidget.create(value);
  637. // Immediately update the body even though it has not yet attached in
  638. // order to trigger a render of the DOM nodes from the React element.
  639. MessageLoop.sendMessage(body, Widget.Msg.UpdateRequest);
  640. }
  641. body.addClass('jp-Dialog-body');
  642. Styling.styleNode(body.node);
  643. return body;
  644. }
  645. /**
  646. * Create the footer of the dialog.
  647. *
  648. * @param buttonNodes - The buttons nodes to add to the footer.
  649. *
  650. * @returns A widget for the footer.
  651. */
  652. createFooter(buttons: ReadonlyArray<HTMLElement>): Widget {
  653. const footer = new Widget();
  654. footer.addClass('jp-Dialog-footer');
  655. each(buttons, button => {
  656. footer.node.appendChild(button);
  657. });
  658. Styling.styleNode(footer.node);
  659. return footer;
  660. }
  661. /**
  662. * Create a button node for the dialog.
  663. *
  664. * @param button - The button data.
  665. *
  666. * @returns A node for the button.
  667. */
  668. createButtonNode(button: IButton): HTMLElement {
  669. const e = document.createElement('button');
  670. e.className = this.createItemClass(button);
  671. e.appendChild(this.renderIcon(button));
  672. e.appendChild(this.renderLabel(button));
  673. return e;
  674. }
  675. /**
  676. * Create the class name for the button.
  677. *
  678. * @param data - The data to use for the class name.
  679. *
  680. * @returns The full class name for the button.
  681. */
  682. createItemClass(data: IButton): string {
  683. // Setup the initial class name.
  684. let name = 'jp-Dialog-button';
  685. // Add the other state classes.
  686. if (data.accept) {
  687. name += ' jp-mod-accept';
  688. } else {
  689. name += ' jp-mod-reject';
  690. }
  691. if (data.displayType === 'warn') {
  692. name += ' jp-mod-warn';
  693. }
  694. // Add the extra class.
  695. const extra = data.className;
  696. if (extra) {
  697. name += ` ${extra}`;
  698. }
  699. // Return the complete class name.
  700. return name;
  701. }
  702. /**
  703. * Render an icon element for a dialog item.
  704. *
  705. * @param data - The data to use for rendering the icon.
  706. *
  707. * @returns An HTML element representing the icon.
  708. */
  709. renderIcon(data: IButton): HTMLElement {
  710. const e = document.createElement('div');
  711. e.className = this.createIconClass(data);
  712. e.appendChild(document.createTextNode(data.iconLabel));
  713. return e;
  714. }
  715. /**
  716. * Create the class name for the button icon.
  717. *
  718. * @param data - The data to use for the class name.
  719. *
  720. * @returns The full class name for the item icon.
  721. */
  722. createIconClass(data: IButton): string {
  723. const name = 'jp-Dialog-buttonIcon';
  724. const extra = data.iconClass;
  725. return extra ? `${name} ${extra}` : name;
  726. }
  727. /**
  728. * Render the label element for a button.
  729. *
  730. * @param data - The data to use for rendering the label.
  731. *
  732. * @returns An HTML element representing the item label.
  733. */
  734. renderLabel(data: IButton): HTMLElement {
  735. const e = document.createElement('div');
  736. e.className = 'jp-Dialog-buttonLabel';
  737. e.title = data.caption;
  738. e.appendChild(document.createTextNode(data.label));
  739. return e;
  740. }
  741. }
  742. /**
  743. * The default renderer instance.
  744. */
  745. export const defaultRenderer = new Renderer();
  746. /**
  747. * The dialog widget tracker.
  748. */
  749. export const tracker = new WidgetTracker<Dialog<any>>({
  750. namespace: '@jupyterlab/apputils:Dialog'
  751. });
  752. }
  753. /**
  754. * The namespace for module private data.
  755. */
  756. namespace Private {
  757. /**
  758. * The queue for launching dialogs.
  759. */
  760. export const launchQueue: Promise<Dialog.IResult<any>>[] = [];
  761. export const errorMessagePromiseCache: Map<string, Promise<void>> = new Map();
  762. /**
  763. * Handle the input options for a dialog.
  764. *
  765. * @param options - The input options.
  766. *
  767. * @returns A new options object with defaults applied.
  768. */
  769. export function handleOptions<T>(
  770. options: Partial<Dialog.IOptions<T>> = {}
  771. ): Dialog.IOptions<T> {
  772. const buttons = options.buttons || [
  773. Dialog.cancelButton(),
  774. Dialog.okButton()
  775. ];
  776. let defaultButton = buttons.length - 1;
  777. if (options.defaultButton !== undefined) {
  778. defaultButton = options.defaultButton;
  779. }
  780. return {
  781. title: options.title || '',
  782. body: options.body || '',
  783. host: options.host || document.body,
  784. buttons,
  785. defaultButton,
  786. renderer: options.renderer || Dialog.defaultRenderer,
  787. focusNodeSelector: options.focusNodeSelector || '',
  788. hasClose: options.hasClose || false
  789. };
  790. }
  791. /**
  792. * Find the first focusable item in the dialog.
  793. */
  794. export function findFirstFocusable(node: HTMLElement): HTMLElement {
  795. const candidateSelectors = [
  796. 'input',
  797. 'select',
  798. 'a[href]',
  799. 'textarea',
  800. 'button',
  801. '[tabindex]'
  802. ].join(',');
  803. return node.querySelectorAll(candidateSelectors)[0] as HTMLElement;
  804. }
  805. }