notebooktools.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { ArrayExt, each, chain } from '@phosphor/algorithm';
  4. import { JSONObject, JSONValue, Token } from '@phosphor/coreutils';
  5. import { ConflatableMessage, Message, MessageLoop } from '@phosphor/messaging';
  6. import { h, VirtualDOM, VirtualNode } from '@phosphor/virtualdom';
  7. import { PanelLayout, Widget } from '@phosphor/widgets';
  8. import { Collapse, Styling } from '@jupyterlab/apputils';
  9. import { Cell, ICellModel } from '@jupyterlab/cells';
  10. import {
  11. CodeEditor,
  12. CodeEditorWrapper,
  13. JSONEditor
  14. } from '@jupyterlab/codeeditor';
  15. import { nbformat } from '@jupyterlab/coreutils';
  16. import { IObservableMap, ObservableJSON } from '@jupyterlab/observables';
  17. import { INotebookTracker } from './';
  18. import { NotebookPanel } from './panel';
  19. import { INotebookModel } from './model';
  20. /* tslint:disable */
  21. /**
  22. * The notebook tools token.
  23. */
  24. export const INotebookTools = new Token<INotebookTools>(
  25. '@jupyterlab/notebook:INotebookTools'
  26. );
  27. /* tslint:enable */
  28. /**
  29. * The interface for notebook metadata tools.
  30. */
  31. export interface INotebookTools extends Widget {
  32. activeNotebookPanel: NotebookPanel | null;
  33. activeCell: Cell | null;
  34. selectedCells: Cell[];
  35. addItem(options: NotebookTools.IAddOptions): void;
  36. }
  37. class RankedPanel<T extends Widget = Widget> extends Widget {
  38. constructor() {
  39. super();
  40. this.layout = new PanelLayout();
  41. this.addClass('jp-RankedPanel');
  42. }
  43. addWidget(widget: Widget, rank: number): void {
  44. const rankItem = { widget, rank };
  45. const index = ArrayExt.upperBound(this._items, rankItem, Private.itemCmp);
  46. ArrayExt.insert(this._items, index, rankItem);
  47. const layout = this.layout as PanelLayout;
  48. layout.insertWidget(index, widget);
  49. }
  50. /**
  51. * Handle the removal of a child
  52. *
  53. */
  54. protected onChildRemoved(msg: Widget.ChildMessage): void {
  55. let index = ArrayExt.findFirstIndex(
  56. this._items,
  57. item => item.widget === msg.child
  58. );
  59. if (index !== -1) {
  60. ArrayExt.removeAt(this._items, index);
  61. }
  62. }
  63. private _items: Private.IRankItem<T>[] = [];
  64. }
  65. /**
  66. * A widget that provides cell metadata tools.
  67. */
  68. export class NotebookTools extends Widget implements INotebookTools {
  69. /**
  70. * Construct a new NotebookTools object.
  71. */
  72. constructor(options: NotebookTools.IOptions) {
  73. super();
  74. this.addClass('jp-NotebookTools');
  75. this._commonTools = new RankedPanel<NotebookTools.Tool>();
  76. this._advancedTools = new RankedPanel<NotebookTools.Tool>();
  77. this._advancedTools.title.label = 'Advanced Tools';
  78. const layout = (this.layout = new PanelLayout());
  79. layout.addWidget(this._commonTools);
  80. layout.addWidget(new Collapse({ widget: this._advancedTools }));
  81. this._tracker = options.tracker;
  82. this._tracker.currentChanged.connect(
  83. this._onActiveNotebookPanelChanged,
  84. this
  85. );
  86. this._tracker.activeCellChanged.connect(
  87. this._onActiveCellChanged,
  88. this
  89. );
  90. this._tracker.selectionChanged.connect(
  91. this._onSelectionChanged,
  92. this
  93. );
  94. this._onActiveNotebookPanelChanged();
  95. this._onActiveCellChanged();
  96. this._onSelectionChanged();
  97. }
  98. /**
  99. * The active cell widget.
  100. */
  101. get activeCell(): Cell | null {
  102. return this._tracker.activeCell;
  103. }
  104. /**
  105. * The currently selected cells.
  106. */
  107. get selectedCells(): Cell[] {
  108. const panel = this._tracker.currentWidget;
  109. if (!panel) {
  110. return [];
  111. }
  112. const notebook = panel.content;
  113. return notebook.widgets.filter(cell => notebook.isSelectedOrActive(cell));
  114. }
  115. /**
  116. * The current notebook.
  117. */
  118. get activeNotebookPanel(): NotebookPanel | null {
  119. return this._tracker.currentWidget;
  120. }
  121. /**
  122. * Add a cell tool item.
  123. */
  124. addItem(options: NotebookTools.IAddOptions): void {
  125. let tool = options.tool;
  126. let rank = 'rank' in options ? options.rank : 100;
  127. let section: RankedPanel<NotebookTools.Tool>;
  128. if (options.section === 'advanced') {
  129. section = this._advancedTools;
  130. } else {
  131. section = this._commonTools;
  132. }
  133. tool.addClass('jp-NotebookTools-tool');
  134. section.addWidget(tool, rank);
  135. // TODO: perhaps the necessary notebookTools functionality should be
  136. // consolidated into a single object, rather than a broad reference to this.
  137. tool.notebookTools = this;
  138. // Trigger the tool to update its active notebook and cell.
  139. MessageLoop.sendMessage(tool, NotebookTools.ActiveNotebookPanelMessage);
  140. MessageLoop.sendMessage(tool, NotebookTools.ActiveCellMessage);
  141. }
  142. /**
  143. * Handle a change to the notebook panel.
  144. */
  145. private _onActiveNotebookPanelChanged(): void {
  146. if (
  147. this._prevActiveNotebookModel &&
  148. !this._prevActiveNotebookModel.isDisposed
  149. ) {
  150. this._prevActiveNotebookModel.metadata.changed.disconnect(
  151. this._onActiveNotebookPanelMetadataChanged,
  152. this
  153. );
  154. }
  155. const activeNBModel =
  156. this.activeNotebookPanel && this.activeNotebookPanel.content
  157. ? this.activeNotebookPanel.content.model
  158. : null;
  159. this._prevActiveNotebookModel = activeNBModel;
  160. if (activeNBModel) {
  161. activeNBModel.metadata.changed.connect(
  162. this._onActiveNotebookPanelMetadataChanged,
  163. this
  164. );
  165. }
  166. each(this._toolChildren(), widget => {
  167. MessageLoop.sendMessage(widget, NotebookTools.ActiveNotebookPanelMessage);
  168. });
  169. }
  170. /**
  171. * Handle a change to the active cell.
  172. */
  173. private _onActiveCellChanged(): void {
  174. if (this._prevActiveCell && !this._prevActiveCell.isDisposed) {
  175. this._prevActiveCell.metadata.changed.disconnect(
  176. this._onActiveCellMetadataChanged,
  177. this
  178. );
  179. }
  180. const activeCell = this.activeCell ? this.activeCell.model : null;
  181. this._prevActiveCell = activeCell;
  182. if (activeCell) {
  183. activeCell.metadata.changed.connect(
  184. this._onActiveCellMetadataChanged,
  185. this
  186. );
  187. }
  188. each(this._toolChildren(), widget => {
  189. MessageLoop.sendMessage(widget, NotebookTools.ActiveCellMessage);
  190. });
  191. }
  192. /**
  193. * Handle a change in the selection.
  194. */
  195. private _onSelectionChanged(): void {
  196. each(this._toolChildren(), widget => {
  197. MessageLoop.sendMessage(widget, NotebookTools.SelectionMessage);
  198. });
  199. }
  200. /**
  201. * Handle a change in the active cell metadata.
  202. */
  203. private _onActiveNotebookPanelMetadataChanged(
  204. sender: IObservableMap<JSONValue>,
  205. args: IObservableMap.IChangedArgs<JSONValue>
  206. ): void {
  207. let message = new ObservableJSON.ChangeMessage(
  208. 'activenotebookpanel-metadata-changed',
  209. args
  210. );
  211. each(this._toolChildren(), widget => {
  212. MessageLoop.sendMessage(widget, message);
  213. });
  214. }
  215. /**
  216. * Handle a change in the notebook model metadata.
  217. */
  218. private _onActiveCellMetadataChanged(
  219. sender: IObservableMap<JSONValue>,
  220. args: IObservableMap.IChangedArgs<JSONValue>
  221. ): void {
  222. let message = new ObservableJSON.ChangeMessage(
  223. 'activecell-metadata-changed',
  224. args
  225. );
  226. each(this._toolChildren(), widget => {
  227. MessageLoop.sendMessage(widget, message);
  228. });
  229. }
  230. private _toolChildren() {
  231. return chain(this._commonTools.children(), this._advancedTools.children());
  232. }
  233. private _commonTools: RankedPanel<NotebookTools.Tool>;
  234. private _advancedTools: RankedPanel<NotebookTools.Tool>;
  235. private _tracker: INotebookTracker;
  236. private _prevActiveCell: ICellModel | null;
  237. private _prevActiveNotebookModel: INotebookModel | null;
  238. }
  239. /**
  240. * The namespace for NotebookTools class statics.
  241. */
  242. export namespace NotebookTools {
  243. /**
  244. * The options used to create a NotebookTools object.
  245. */
  246. export interface IOptions {
  247. /**
  248. * The notebook tracker used by the notebook tools.
  249. */
  250. tracker: INotebookTracker;
  251. }
  252. /**
  253. * The options used to add an item to the notebook tools.
  254. */
  255. export interface IAddOptions {
  256. /**
  257. * The tool to add to the notebook tools area.
  258. */
  259. tool: Tool;
  260. /**
  261. * The section to which the tool should be added.
  262. */
  263. section?: 'common' | 'advanced';
  264. /**
  265. * The rank order of the widget among its siblings.
  266. */
  267. rank?: number;
  268. }
  269. /**
  270. * A singleton conflatable `'activenotebookpanel-changed'` message.
  271. */
  272. export const ActiveNotebookPanelMessage = new ConflatableMessage(
  273. 'activenotebookpanel-changed'
  274. );
  275. /**
  276. * A singleton conflatable `'activecell-changed'` message.
  277. */
  278. export const ActiveCellMessage = new ConflatableMessage('activecell-changed');
  279. /**
  280. * A singleton conflatable `'selection-changed'` message.
  281. */
  282. export const SelectionMessage = new ConflatableMessage('selection-changed');
  283. /**
  284. * The base notebook tool, meant to be subclassed.
  285. */
  286. export class Tool extends Widget {
  287. /**
  288. * The notebook tools object.
  289. */
  290. notebookTools: INotebookTools;
  291. dispose() {
  292. super.dispose();
  293. this.notebookTools = null;
  294. }
  295. /**
  296. * Process a message sent to the widget.
  297. *
  298. * @param msg - The message sent to the widget.
  299. */
  300. processMessage(msg: Message): void {
  301. super.processMessage(msg);
  302. switch (msg.type) {
  303. case 'activenotebookpanel-changed':
  304. this.onActiveNotebookPanelChanged(msg);
  305. break;
  306. case 'activecell-changed':
  307. this.onActiveCellChanged(msg);
  308. break;
  309. case 'selection-changed':
  310. this.onSelectionChanged(msg);
  311. break;
  312. case 'activecell-metadata-changed':
  313. this.onActiveCellMetadataChanged(msg as ObservableJSON.ChangeMessage);
  314. break;
  315. case 'activenotebookpanel-metadata-changed':
  316. this.onActiveNotebookPanelMetadataChanged(
  317. msg as ObservableJSON.ChangeMessage
  318. );
  319. break;
  320. default:
  321. break;
  322. }
  323. }
  324. /**
  325. * Handle a change to the notebook panel.
  326. *
  327. * #### Notes
  328. * The default implementation is a no-op.
  329. */
  330. protected onActiveNotebookPanelChanged(msg: Message): void {
  331. /* no-op */
  332. }
  333. /**
  334. * Handle a change to the active cell.
  335. *
  336. * #### Notes
  337. * The default implementation is a no-op.
  338. */
  339. protected onActiveCellChanged(msg: Message): void {
  340. /* no-op */
  341. }
  342. /**
  343. * Handle a change to the selection.
  344. *
  345. * #### Notes
  346. * The default implementation is a no-op.
  347. */
  348. protected onSelectionChanged(msg: Message): void {
  349. /* no-op */
  350. }
  351. /**
  352. * Handle a change to the metadata of the active cell.
  353. *
  354. * #### Notes
  355. * The default implementation is a no-op.
  356. */
  357. protected onActiveCellMetadataChanged(
  358. msg: ObservableJSON.ChangeMessage
  359. ): void {
  360. /* no-op */
  361. }
  362. /**
  363. * Handle a change to the metadata of the active cell.
  364. *
  365. * #### Notes
  366. * The default implementation is a no-op.
  367. */
  368. protected onActiveNotebookPanelMetadataChanged(
  369. msg: ObservableJSON.ChangeMessage
  370. ): void {
  371. /* no-op */
  372. }
  373. }
  374. /**
  375. * A cell tool displaying the active cell contents.
  376. */
  377. export class ActiveCellTool extends Tool {
  378. /**
  379. * Construct a new active cell tool.
  380. */
  381. constructor() {
  382. super();
  383. this.addClass('jp-ActiveCellTool');
  384. this.addClass('jp-InputArea');
  385. this.layout = new PanelLayout();
  386. }
  387. /**
  388. * Dispose of the resources used by the tool.
  389. */
  390. dispose() {
  391. if (this._model === null) {
  392. return;
  393. }
  394. this._model.dispose();
  395. this._model = null;
  396. super.dispose();
  397. }
  398. /**
  399. * Handle a change to the active cell.
  400. */
  401. protected onActiveCellChanged(): void {
  402. let activeCell = this.notebookTools.activeCell;
  403. let layout = this.layout as PanelLayout;
  404. let count = layout.widgets.length;
  405. for (let i = 0; i < count; i++) {
  406. layout.widgets[0].dispose();
  407. }
  408. if (this._cellModel && !this._cellModel.isDisposed) {
  409. this._cellModel.value.changed.disconnect(this._onValueChanged, this);
  410. this._cellModel.mimeTypeChanged.disconnect(
  411. this._onMimeTypeChanged,
  412. this
  413. );
  414. }
  415. if (!activeCell) {
  416. let cell = new Widget();
  417. cell.addClass('jp-InputArea-editor');
  418. cell.addClass('jp-InputArea-editor');
  419. layout.addWidget(cell);
  420. this._cellModel = null;
  421. return;
  422. }
  423. let promptNode = activeCell.promptNode
  424. ? (activeCell.promptNode.cloneNode(true) as HTMLElement)
  425. : null;
  426. let prompt = new Widget({ node: promptNode });
  427. let factory = activeCell.contentFactory.editorFactory;
  428. let cellModel = (this._cellModel = activeCell.model);
  429. cellModel.value.changed.connect(
  430. this._onValueChanged,
  431. this
  432. );
  433. cellModel.mimeTypeChanged.connect(
  434. this._onMimeTypeChanged,
  435. this
  436. );
  437. this._model.value.text = cellModel.value.text.split('\n')[0];
  438. this._model.mimeType = cellModel.mimeType;
  439. let model = this._model;
  440. let editorWidget = new CodeEditorWrapper({ model, factory });
  441. editorWidget.addClass('jp-InputArea-editor');
  442. editorWidget.addClass('jp-InputArea-editor');
  443. editorWidget.editor.setOption('readOnly', true);
  444. layout.addWidget(prompt);
  445. layout.addWidget(editorWidget);
  446. }
  447. /**
  448. * Handle a change to the current editor value.
  449. */
  450. private _onValueChanged(): void {
  451. this._model.value.text = this._cellModel.value.text.split('\n')[0];
  452. }
  453. /**
  454. * Handle a change to the current editor mimetype.
  455. */
  456. private _onMimeTypeChanged(): void {
  457. this._model.mimeType = this._cellModel.mimeType;
  458. }
  459. private _model = new CodeEditor.Model();
  460. private _cellModel: CodeEditor.IModel;
  461. }
  462. /**
  463. * A raw metadata editor.
  464. */
  465. export class MetadataEditorTool extends Tool {
  466. /**
  467. * Construct a new raw metadata tool.
  468. */
  469. constructor(options: MetadataEditorTool.IOptions) {
  470. super();
  471. const { editorFactory } = options;
  472. this.addClass('jp-MetadataEditorTool');
  473. let layout = (this.layout = new PanelLayout());
  474. this.editor = new JSONEditor({
  475. editorFactory
  476. });
  477. this.editor.title.label = options.label || 'Edit Metadata';
  478. const titleNode = new Widget({ node: document.createElement('label') });
  479. titleNode.node.textContent = options.label || 'Edit Metadata';
  480. layout.addWidget(titleNode);
  481. layout.addWidget(this.editor);
  482. }
  483. /**
  484. * The editor used by the tool.
  485. */
  486. readonly editor: JSONEditor;
  487. }
  488. /**
  489. * The namespace for `MetadataEditorTool` static data.
  490. */
  491. export namespace MetadataEditorTool {
  492. /**
  493. * The options used to initialize a metadata editor tool.
  494. */
  495. export interface IOptions {
  496. /**
  497. * The editor factory used by the tool.
  498. */
  499. editorFactory: CodeEditor.Factory;
  500. /**
  501. * The label for the JSON editor
  502. */
  503. label?: string;
  504. /**
  505. * Initial collapse state, defaults to true.
  506. */
  507. collapsed?: boolean;
  508. }
  509. }
  510. /**
  511. * A notebook metadata editor
  512. */
  513. export class NotebookMetadataEditorTool extends MetadataEditorTool {
  514. constructor(options: MetadataEditorTool.IOptions) {
  515. options.label = options.label || 'Notebook Metadata';
  516. super(options);
  517. }
  518. /**
  519. * Handle a change to the notebook.
  520. */
  521. protected onActiveNotebookPanelChanged(msg: Message): void {
  522. this._update();
  523. }
  524. /**
  525. * Handle a change to the notebook metadata.
  526. */
  527. protected onActiveNotebookPanelMetadataChanged(msg: Message): void {
  528. this._update();
  529. }
  530. private _update() {
  531. const nb =
  532. this.notebookTools.activeNotebookPanel &&
  533. this.notebookTools.activeNotebookPanel.content;
  534. this.editor.source = nb ? nb.model.metadata : null;
  535. }
  536. }
  537. /**
  538. * A cell metadata editor
  539. */
  540. export class CellMetadataEditorTool extends MetadataEditorTool {
  541. constructor(options: MetadataEditorTool.IOptions) {
  542. options.label = options.label || 'Cell Metadata';
  543. super(options);
  544. }
  545. /**
  546. * Handle a change to the active cell.
  547. */
  548. protected onActiveCellChanged(msg: Message): void {
  549. this._update();
  550. }
  551. /**
  552. * Handle a change to the active cell metadata.
  553. */
  554. protected onActiveCellMetadataChanged(msg: Message): void {
  555. this._update();
  556. }
  557. private _update() {
  558. let cell = this.notebookTools.activeCell;
  559. this.editor.source = cell ? cell.model.metadata : null;
  560. }
  561. }
  562. /**
  563. * A cell tool that provides a selection for a given metadata key.
  564. */
  565. export class KeySelector extends Tool {
  566. /**
  567. * Construct a new KeySelector.
  568. */
  569. constructor(options: KeySelector.IOptions) {
  570. // TODO: use react
  571. super({ node: Private.createSelectorNode(options) });
  572. this.addClass('jp-KeySelector');
  573. this.key = options.key;
  574. this._default = options.default;
  575. this._validCellTypes = options.validCellTypes || [];
  576. this._getter = options.getter || this._getValue;
  577. this._setter = options.setter || this._setValue;
  578. }
  579. /**
  580. * The metadata key used by the selector.
  581. */
  582. readonly key: string;
  583. /**
  584. * The select node for the widget.
  585. */
  586. get selectNode(): HTMLSelectElement {
  587. return this.node.getElementsByTagName('select')[0] as HTMLSelectElement;
  588. }
  589. /**
  590. * Handle the DOM events for the widget.
  591. *
  592. * @param event - The DOM event sent to the widget.
  593. *
  594. * #### Notes
  595. * This method implements the DOM `EventListener` interface and is
  596. * called in response to events on the notebook panel's node. It should
  597. * not be called directly by user code.
  598. */
  599. handleEvent(event: Event): void {
  600. switch (event.type) {
  601. case 'change':
  602. this.onValueChanged();
  603. break;
  604. default:
  605. break;
  606. }
  607. }
  608. /**
  609. * Handle `after-attach` messages for the widget.
  610. */
  611. protected onAfterAttach(msg: Message): void {
  612. let node = this.selectNode;
  613. node.addEventListener('change', this);
  614. }
  615. /**
  616. * Handle `before-detach` messages for the widget.
  617. */
  618. protected onBeforeDetach(msg: Message): void {
  619. let node = this.selectNode;
  620. node.removeEventListener('change', this);
  621. }
  622. /**
  623. * Handle a change to the active cell.
  624. */
  625. protected onActiveCellChanged(msg: Message): void {
  626. let select = this.selectNode;
  627. let activeCell = this.notebookTools.activeCell;
  628. if (!activeCell) {
  629. select.disabled = true;
  630. select.value = '';
  631. return;
  632. }
  633. let cellType = activeCell.model.type;
  634. if (
  635. this._validCellTypes.length &&
  636. this._validCellTypes.indexOf(cellType) === -1
  637. ) {
  638. select.value = undefined;
  639. select.disabled = true;
  640. return;
  641. }
  642. select.disabled = false;
  643. this._changeGuard = true;
  644. let getter = this._getter;
  645. select.value = JSON.stringify(getter(activeCell));
  646. this._changeGuard = false;
  647. }
  648. /**
  649. * Handle a change to the metadata of the active cell.
  650. */
  651. protected onActiveCellMetadataChanged(msg: ObservableJSON.ChangeMessage) {
  652. if (this._changeGuard) {
  653. return;
  654. }
  655. let select = this.selectNode;
  656. let cell = this.notebookTools.activeCell;
  657. if (msg.args.key === this.key && cell) {
  658. this._changeGuard = true;
  659. let getter = this._getter;
  660. select.value = JSON.stringify(getter(cell));
  661. this._changeGuard = false;
  662. }
  663. }
  664. /**
  665. * Handle a change to the value.
  666. */
  667. protected onValueChanged(): void {
  668. let activeCell = this.notebookTools.activeCell;
  669. if (!activeCell || this._changeGuard) {
  670. return;
  671. }
  672. this._changeGuard = true;
  673. let select = this.selectNode;
  674. let setter = this._setter;
  675. setter(activeCell, JSON.parse(select.value));
  676. this._changeGuard = false;
  677. }
  678. /**
  679. * Get the value for the data.
  680. */
  681. private _getValue = (cell: Cell) => {
  682. let value = cell.model.metadata.get(this.key);
  683. if (value === undefined) {
  684. value = this._default;
  685. }
  686. return value;
  687. };
  688. /**
  689. * Set the value for the data.
  690. */
  691. private _setValue = (cell: Cell, value: JSONValue) => {
  692. if (value === this._default) {
  693. cell.model.metadata.delete(this.key);
  694. } else {
  695. cell.model.metadata.set(this.key, value);
  696. }
  697. };
  698. private _changeGuard = false;
  699. private _validCellTypes: string[];
  700. private _getter: (cell: Cell) => JSONValue;
  701. private _setter: (cell: Cell, value: JSONValue) => void;
  702. private _default: JSONValue;
  703. }
  704. /**
  705. * The namespace for `KeySelector` static data.
  706. */
  707. export namespace KeySelector {
  708. /**
  709. * The options used to initialize a keyselector.
  710. */
  711. export interface IOptions {
  712. /**
  713. * The metadata key of interest.
  714. */
  715. key: string;
  716. /**
  717. * The map of options to values.
  718. *
  719. * #### Notes
  720. * If a value equals the default, choosing it may erase the key from the
  721. * metadata.
  722. */
  723. optionsMap: { [key: string]: JSONValue };
  724. /**
  725. * The optional title of the selector - defaults to capitalized `key`.
  726. */
  727. title?: string;
  728. /**
  729. * The optional valid cell types - defaults to all valid types.
  730. */
  731. validCellTypes?: nbformat.CellType[];
  732. /**
  733. * An optional value getter for the selector.
  734. *
  735. * @param cell - The currently active cell.
  736. *
  737. * @returns The appropriate value for the selector.
  738. */
  739. getter?: (cell: Cell) => JSONValue;
  740. /**
  741. * An optional value setter for the selector.
  742. *
  743. * @param cell - The currently active cell.
  744. *
  745. * @param value - The value of the selector.
  746. *
  747. * #### Notes
  748. * The setter should set the appropriate metadata value given the value of
  749. * the selector.
  750. */
  751. setter?: (cell: Cell, value: JSONValue) => void;
  752. /**
  753. * Default value for default setters and getters if value is not found.
  754. */
  755. default?: JSONValue;
  756. }
  757. }
  758. /**
  759. * Create a slideshow selector.
  760. */
  761. export function createSlideShowSelector(): KeySelector {
  762. let options: KeySelector.IOptions = {
  763. key: 'slideshow',
  764. title: 'Slide Type',
  765. optionsMap: {
  766. '-': null,
  767. Slide: 'slide',
  768. 'Sub-Slide': 'subslide',
  769. Fragment: 'fragment',
  770. Skip: 'skip',
  771. Notes: 'notes'
  772. },
  773. getter: cell => {
  774. let value = cell.model.metadata.get('slideshow');
  775. return value && (value as JSONObject)['slide_type'];
  776. },
  777. setter: (cell, value) => {
  778. let data = cell.model.metadata.get('slideshow') || Object.create(null);
  779. if (value === null) {
  780. // Make a shallow copy so we aren't modifying the original metadata.
  781. data = { ...data };
  782. delete data.slide_type;
  783. } else {
  784. data = { ...data, slide_type: value };
  785. }
  786. if (Object.keys(data).length > 0) {
  787. cell.model.metadata.set('slideshow', data);
  788. } else {
  789. cell.model.metadata.delete('slideshow');
  790. }
  791. }
  792. };
  793. return new KeySelector(options);
  794. }
  795. /**
  796. * Create an nbconvert selector.
  797. */
  798. export function createNBConvertSelector(optionsMap: {
  799. [key: string]: JSONValue;
  800. }): KeySelector {
  801. return new KeySelector({
  802. key: 'raw_mimetype',
  803. title: 'Raw NBConvert Format',
  804. optionsMap: optionsMap,
  805. validCellTypes: ['raw']
  806. });
  807. }
  808. }
  809. /**
  810. * A namespace for private data.
  811. */
  812. namespace Private {
  813. /**
  814. * An object which holds a widget and its sort rank.
  815. */
  816. export interface IRankItem<T extends Widget = Widget> {
  817. /**
  818. * The widget for the item.
  819. */
  820. widget: T;
  821. /**
  822. * The sort rank of the menu.
  823. */
  824. rank: number;
  825. }
  826. /**
  827. * A comparator function for widget rank items.
  828. */
  829. export function itemCmp(first: IRankItem, second: IRankItem): number {
  830. return first.rank - second.rank;
  831. }
  832. /**
  833. * Create the node for a KeySelector.
  834. */
  835. export function createSelectorNode(
  836. options: NotebookTools.KeySelector.IOptions
  837. ): HTMLElement {
  838. let name = options.key;
  839. let title = options.title || name[0].toLocaleUpperCase() + name.slice(1);
  840. let optionNodes: VirtualNode[] = [];
  841. for (let label in options.optionsMap) {
  842. let value = JSON.stringify(options.optionsMap[label]);
  843. optionNodes.push(h.option({ value }, label));
  844. }
  845. let node = VirtualDOM.realize(
  846. h.div({}, h.label(title, h.select({}, optionNodes)))
  847. );
  848. Styling.styleNode(node);
  849. return node;
  850. }
  851. }