celltools.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { ArrayExt, each } 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 { 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. /**
  19. * The class name added to a CellTools instance.
  20. */
  21. const CELLTOOLS_CLASS = 'jp-CellTools';
  22. /**
  23. * The class name added to a CellTools tool.
  24. */
  25. const CHILD_CLASS = 'jp-CellTools-tool';
  26. /**
  27. * The class name added to a CellTools active cell.
  28. */
  29. const ACTIVE_CELL_CLASS = 'jp-ActiveCellTool';
  30. /**
  31. * The class name added to an Editor instance.
  32. */
  33. const EDITOR_CLASS = 'jp-MetadataEditorTool';
  34. /**
  35. * The class name added to a KeySelector instance.
  36. */
  37. const KEYSELECTOR_CLASS = 'jp-KeySelector';
  38. /* tslint:disable */
  39. /**
  40. * The main menu token.
  41. */
  42. export const ICellTools = new Token<ICellTools>(
  43. '@jupyterlab/notebook:ICellTools'
  44. );
  45. /* tslint:enable */
  46. /**
  47. * The interface for cell metadata tools.
  48. */
  49. export interface ICellTools extends CellTools {}
  50. /**
  51. * A widget that provides cell metadata tools.
  52. */
  53. export class CellTools extends Widget {
  54. /**
  55. * Construct a new CellTools object.
  56. */
  57. constructor(options: CellTools.IOptions) {
  58. super();
  59. this.addClass(CELLTOOLS_CLASS);
  60. this.layout = new PanelLayout();
  61. this._tracker = options.tracker;
  62. this._tracker.activeCellChanged.connect(
  63. this._onActiveCellChanged,
  64. this
  65. );
  66. this._tracker.selectionChanged.connect(
  67. this._onSelectionChanged,
  68. this
  69. );
  70. this._onActiveCellChanged();
  71. this._onSelectionChanged();
  72. }
  73. /**
  74. * The active cell widget.
  75. */
  76. get activeCell(): Cell | null {
  77. return this._tracker.activeCell;
  78. }
  79. /**
  80. * The currently selected cells.
  81. */
  82. get selectedCells(): Cell[] {
  83. let selected: Cell[] = [];
  84. let panel = this._tracker.currentWidget;
  85. if (!panel) {
  86. return selected;
  87. }
  88. each(panel.content.widgets, widget => {
  89. if (panel.content.isSelectedOrActive(widget)) {
  90. selected.push(widget);
  91. }
  92. });
  93. return selected;
  94. }
  95. /**
  96. * Add a cell tool item.
  97. */
  98. addItem(options: CellTools.IAddOptions): void {
  99. let tool = options.tool;
  100. let rank = 'rank' in options ? options.rank : 100;
  101. let rankItem = { tool, rank };
  102. let index = ArrayExt.upperBound(this._items, rankItem, Private.itemCmp);
  103. tool.addClass(CHILD_CLASS);
  104. // Add the tool.
  105. ArrayExt.insert(this._items, index, rankItem);
  106. let layout = this.layout as PanelLayout;
  107. layout.insertWidget(index, tool);
  108. // Trigger the tool to update its active cell.
  109. MessageLoop.sendMessage(tool, CellTools.ActiveCellMessage);
  110. }
  111. /**
  112. * Handle the removal of a child
  113. */
  114. protected onChildRemoved(msg: Widget.ChildMessage): void {
  115. let index = ArrayExt.findFirstIndex(
  116. this._items,
  117. item => item.tool === msg.child
  118. );
  119. if (index !== -1) {
  120. ArrayExt.removeAt(this._items, index);
  121. }
  122. }
  123. /**
  124. * Handle a change to the active cell.
  125. */
  126. private _onActiveCellChanged(): void {
  127. if (this._prevActive && !this._prevActive.isDisposed) {
  128. this._prevActive.metadata.changed.disconnect(
  129. this._onMetadataChanged,
  130. this
  131. );
  132. }
  133. let activeCell = this._tracker.activeCell;
  134. this._prevActive = activeCell ? activeCell.model : null;
  135. if (activeCell) {
  136. activeCell.model.metadata.changed.connect(
  137. this._onMetadataChanged,
  138. this
  139. );
  140. }
  141. each(this.children(), widget => {
  142. MessageLoop.sendMessage(widget, CellTools.ActiveCellMessage);
  143. });
  144. }
  145. /**
  146. * Handle a change in the selection.
  147. */
  148. private _onSelectionChanged(): void {
  149. each(this.children(), widget => {
  150. MessageLoop.sendMessage(widget, CellTools.SelectionMessage);
  151. });
  152. }
  153. /**
  154. * Handle a change in the metadata.
  155. */
  156. private _onMetadataChanged(
  157. sender: IObservableMap<JSONValue>,
  158. args: IObservableMap.IChangedArgs<JSONValue>
  159. ): void {
  160. let message = new ObservableJSON.ChangeMessage(args);
  161. each(this.children(), widget => {
  162. MessageLoop.sendMessage(widget, message);
  163. });
  164. }
  165. private _items: Private.IRankItem[] = [];
  166. private _tracker: INotebookTracker;
  167. private _prevActive: ICellModel | null;
  168. }
  169. /**
  170. * The namespace for CellTools class statics.
  171. */
  172. export namespace CellTools {
  173. /**
  174. * The options used to create a CellTools object.
  175. */
  176. export interface IOptions {
  177. /**
  178. * The notebook tracker used by the cell tools.
  179. */
  180. tracker: INotebookTracker;
  181. }
  182. /**
  183. * The options used to add an item to the cell tools.
  184. */
  185. export interface IAddOptions {
  186. /**
  187. * The tool to add to the cell tools area.
  188. */
  189. tool: Tool;
  190. /**
  191. * The rank order of the widget among its siblings.
  192. */
  193. rank?: number;
  194. }
  195. /**
  196. * A singleton conflatable `'activecell-changed'` message.
  197. */
  198. // tslint:disable-next-line
  199. export const ActiveCellMessage = new ConflatableMessage('activecell-changed');
  200. /**
  201. * A singleton conflatable `'selection-changed'` message.
  202. */
  203. // tslint:disable-next-line
  204. export const SelectionMessage = new ConflatableMessage('selection-changed');
  205. /**
  206. * The base cell tool, meant to be subclassed.
  207. */
  208. export class Tool extends Widget {
  209. /**
  210. * The cell tools object.
  211. */
  212. readonly parent: ICellTools;
  213. /**
  214. * Process a message sent to the widget.
  215. *
  216. * @param msg - The message sent to the widget.
  217. */
  218. processMessage(msg: Message): void {
  219. super.processMessage(msg);
  220. switch (msg.type) {
  221. case 'activecell-changed':
  222. this.onActiveCellChanged(msg);
  223. break;
  224. case 'selection-changed':
  225. this.onSelectionChanged(msg);
  226. break;
  227. case 'jsonvalue-changed':
  228. this.onMetadataChanged(msg as ObservableJSON.ChangeMessage);
  229. break;
  230. default:
  231. break;
  232. }
  233. }
  234. /**
  235. * Handle a change to the active cell.
  236. *
  237. * #### Notes
  238. * The default implementation is a no-op.
  239. */
  240. protected onActiveCellChanged(msg: Message): void {
  241. /* no-op */
  242. }
  243. /**
  244. * Handle a change to the selection.
  245. *
  246. * #### Notes
  247. * The default implementation is a no-op.
  248. */
  249. protected onSelectionChanged(msg: Message): void {
  250. /* no-op */
  251. }
  252. /**
  253. * Handle a change to the metadata of the active cell.
  254. *
  255. * #### Notes
  256. * The default implementation is a no-op.
  257. */
  258. protected onMetadataChanged(msg: ObservableJSON.ChangeMessage): void {
  259. /* no-op */
  260. }
  261. }
  262. /**
  263. * A cell tool displaying the active cell contents.
  264. */
  265. export class ActiveCellTool extends Tool {
  266. /**
  267. * Construct a new active cell tool.
  268. */
  269. constructor() {
  270. super();
  271. this.addClass(ACTIVE_CELL_CLASS);
  272. this.addClass('jp-InputArea');
  273. this.layout = new PanelLayout();
  274. }
  275. /**
  276. * Dispose of the resources used by the tool.
  277. */
  278. dispose() {
  279. if (this._model === null) {
  280. return;
  281. }
  282. this._model.dispose();
  283. this._model = null;
  284. super.dispose();
  285. }
  286. /**
  287. * Handle a change to the active cell.
  288. */
  289. protected onActiveCellChanged(): void {
  290. let activeCell = this.parent.activeCell;
  291. let layout = this.layout as PanelLayout;
  292. let count = layout.widgets.length;
  293. for (let i = 0; i < count; i++) {
  294. layout.widgets[0].dispose();
  295. }
  296. if (this._cellModel && !this._cellModel.isDisposed) {
  297. this._cellModel.value.changed.disconnect(this._onValueChanged, this);
  298. this._cellModel.mimeTypeChanged.disconnect(
  299. this._onMimeTypeChanged,
  300. this
  301. );
  302. }
  303. if (!activeCell) {
  304. let cell = new Widget();
  305. cell.addClass('jp-InputArea-editor');
  306. cell.addClass('jp-InputArea-editor');
  307. layout.addWidget(cell);
  308. this._cellModel = null;
  309. return;
  310. }
  311. let promptNode = activeCell.promptNode
  312. ? (activeCell.promptNode.cloneNode(true) as HTMLElement)
  313. : null;
  314. let prompt = new Widget({ node: promptNode });
  315. let factory = activeCell.contentFactory.editorFactory;
  316. let cellModel = (this._cellModel = activeCell.model);
  317. cellModel.value.changed.connect(
  318. this._onValueChanged,
  319. this
  320. );
  321. cellModel.mimeTypeChanged.connect(
  322. this._onMimeTypeChanged,
  323. this
  324. );
  325. this._model.value.text = cellModel.value.text.split('\n')[0];
  326. this._model.mimeType = cellModel.mimeType;
  327. let model = this._model;
  328. let editorWidget = new CodeEditorWrapper({ model, factory });
  329. editorWidget.addClass('jp-InputArea-editor');
  330. editorWidget.addClass('jp-InputArea-editor');
  331. editorWidget.editor.setOption('readOnly', true);
  332. layout.addWidget(prompt);
  333. layout.addWidget(editorWidget);
  334. }
  335. /**
  336. * Handle a change to the current editor value.
  337. */
  338. private _onValueChanged(): void {
  339. this._model.value.text = this._cellModel.value.text.split('\n')[0];
  340. }
  341. /**
  342. * Handle a change to the current editor mimetype.
  343. */
  344. private _onMimeTypeChanged(): void {
  345. this._model.mimeType = this._cellModel.mimeType;
  346. }
  347. private _model = new CodeEditor.Model();
  348. private _cellModel: CodeEditor.IModel;
  349. }
  350. /**
  351. * A raw metadata editor.
  352. */
  353. export class MetadataEditorTool extends Tool {
  354. /**
  355. * Construct a new raw metadata tool.
  356. */
  357. constructor(options: MetadataEditorTool.IOptions) {
  358. super();
  359. let editorFactory = options.editorFactory;
  360. this.addClass(EDITOR_CLASS);
  361. let layout = (this.layout = new PanelLayout());
  362. this.editor = new JSONEditor({
  363. editorFactory,
  364. title: 'Edit Metadata',
  365. collapsible: true
  366. });
  367. layout.addWidget(this.editor);
  368. }
  369. /**
  370. * The editor used by the tool.
  371. */
  372. readonly editor: JSONEditor;
  373. /**
  374. * Handle a change to the active cell.
  375. */
  376. protected onActiveCellChanged(msg: Message): void {
  377. let cell = this.parent.activeCell;
  378. this.editor.source = cell ? cell.model.metadata : null;
  379. }
  380. }
  381. /**
  382. * The namespace for `MetadataEditorTool` static data.
  383. */
  384. export namespace MetadataEditorTool {
  385. /**
  386. * The options used to initialize a metadata editor tool.
  387. */
  388. export interface IOptions {
  389. /**
  390. * The editor factory used by the tool.
  391. */
  392. editorFactory: CodeEditor.Factory;
  393. }
  394. }
  395. /**
  396. * A cell tool that provides a selection for a given metadata key.
  397. */
  398. export class KeySelector extends Tool {
  399. /**
  400. * Construct a new KeySelector.
  401. */
  402. constructor(options: KeySelector.IOptions) {
  403. super({ node: Private.createSelectorNode(options) });
  404. this.addClass(KEYSELECTOR_CLASS);
  405. this.key = options.key;
  406. this._validCellTypes = options.validCellTypes || [];
  407. this._getter = options.getter || this._getValue;
  408. this._setter = options.setter || this._setValue;
  409. }
  410. /**
  411. * The metadata key used by the selector.
  412. */
  413. readonly key: string;
  414. /**
  415. * The select node for the widget.
  416. */
  417. get selectNode(): HTMLSelectElement {
  418. return this.node.getElementsByTagName('select')[0] as HTMLSelectElement;
  419. }
  420. /**
  421. * Handle the DOM events for the widget.
  422. *
  423. * @param event - The DOM event sent to the widget.
  424. *
  425. * #### Notes
  426. * This method implements the DOM `EventListener` interface and is
  427. * called in response to events on the notebook panel's node. It should
  428. * not be called directly by user code.
  429. */
  430. handleEvent(event: Event): void {
  431. switch (event.type) {
  432. case 'change':
  433. this.onValueChanged();
  434. break;
  435. default:
  436. break;
  437. }
  438. }
  439. /**
  440. * Handle `after-attach` messages for the widget.
  441. */
  442. protected onAfterAttach(msg: Message): void {
  443. let node = this.selectNode;
  444. node.addEventListener('change', this);
  445. }
  446. /**
  447. * Handle `before-detach` messages for the widget.
  448. */
  449. protected onBeforeDetach(msg: Message): void {
  450. let node = this.selectNode;
  451. node.removeEventListener('change', this);
  452. }
  453. /**
  454. * Handle a change to the active cell.
  455. */
  456. protected onActiveCellChanged(msg: Message): void {
  457. let select = this.selectNode;
  458. let activeCell = this.parent.activeCell;
  459. if (!activeCell) {
  460. select.disabled = true;
  461. select.value = '';
  462. return;
  463. }
  464. let cellType = activeCell.model.type;
  465. if (
  466. this._validCellTypes.length &&
  467. this._validCellTypes.indexOf(cellType) === -1
  468. ) {
  469. select.disabled = true;
  470. return;
  471. }
  472. select.disabled = false;
  473. this._changeGuard = true;
  474. let getter = this._getter;
  475. select.value = JSON.stringify(getter(activeCell));
  476. this._changeGuard = false;
  477. }
  478. /**
  479. * Handle a change to the metadata of the active cell.
  480. */
  481. protected onMetadataChanged(msg: ObservableJSON.ChangeMessage) {
  482. if (this._changeGuard) {
  483. return;
  484. }
  485. let select = this.selectNode;
  486. let cell = this.parent.activeCell;
  487. if (msg.args.key === this.key && cell) {
  488. this._changeGuard = true;
  489. let getter = this._getter;
  490. select.value = JSON.stringify(getter(cell));
  491. this._changeGuard = false;
  492. }
  493. }
  494. /**
  495. * Handle a change to the value.
  496. */
  497. protected onValueChanged(): void {
  498. let activeCell = this.parent.activeCell;
  499. if (!activeCell || this._changeGuard) {
  500. return;
  501. }
  502. this._changeGuard = true;
  503. let select = this.selectNode;
  504. let setter = this._setter;
  505. setter(activeCell, JSON.parse(select.value));
  506. this._changeGuard = false;
  507. }
  508. /**
  509. * Get the value for the data.
  510. */
  511. private _getValue = (cell: Cell) => {
  512. return cell.model.metadata.get(this.key);
  513. };
  514. /**
  515. * Set the value for the data.
  516. */
  517. private _setValue = (cell: Cell, value: JSONValue) => {
  518. cell.model.metadata.set(this.key, value);
  519. };
  520. private _changeGuard = false;
  521. private _validCellTypes: string[];
  522. private _getter: (cell: Cell) => JSONValue;
  523. private _setter: (cell: Cell, value: JSONValue) => void;
  524. }
  525. /**
  526. * The namespace for `KeySelector` static data.
  527. */
  528. export namespace KeySelector {
  529. /**
  530. * The options used to initialize a keyselector.
  531. */
  532. export interface IOptions {
  533. /**
  534. * The metadata key of interest.
  535. */
  536. key: string;
  537. /**
  538. * The map of options to values.
  539. */
  540. optionsMap: { [key: string]: JSONValue };
  541. /**
  542. * The optional title of the selector - defaults to capitalized `key`.
  543. */
  544. title?: string;
  545. /**
  546. * The optional valid cell types - defaults to all valid types.
  547. */
  548. validCellTypes?: nbformat.CellType[];
  549. /**
  550. * An optional value getter for the selector.
  551. *
  552. * @param cell - The currently active cell.
  553. *
  554. * @returns The appropriate value for the selector.
  555. */
  556. getter?: (cell: Cell) => JSONValue;
  557. /**
  558. * An optional value setter for the selector.
  559. *
  560. * @param cell - The currently active cell.
  561. *
  562. * @param value - The value of the selector.
  563. *
  564. * #### Notes
  565. * The setter should set the appropriate metadata value
  566. * given the value of the selector.
  567. */
  568. setter?: (cell: Cell, value: JSONValue) => void;
  569. }
  570. }
  571. /**
  572. * Create a slideshow selector.
  573. */
  574. export function createSlideShowSelector(): KeySelector {
  575. let options: KeySelector.IOptions = {
  576. key: 'slideshow',
  577. title: 'Slide Type',
  578. optionsMap: {
  579. '-': '-',
  580. Slide: 'slide',
  581. 'Sub-Slide': 'subslide',
  582. Fragment: 'fragment',
  583. Skip: 'skip',
  584. Notes: 'notes'
  585. },
  586. getter: cell => {
  587. let value = cell.model.metadata.get('slideshow');
  588. return value && (value as JSONObject)['slide_type'];
  589. },
  590. setter: (cell, value) => {
  591. let data = cell.model.metadata.get('slideshow') || Object.create(null);
  592. data = { ...data, slide_type: value };
  593. cell.model.metadata.set('slideshow', data);
  594. }
  595. };
  596. return new KeySelector(options);
  597. }
  598. /**
  599. * Create an nbcovert selector.
  600. */
  601. export function createNBConvertSelector(optionsMap: {
  602. [key: string]: JSONValue;
  603. }): KeySelector {
  604. return new KeySelector({
  605. key: 'raw_mimetype',
  606. title: 'Raw NBConvert Format',
  607. optionsMap: optionsMap,
  608. validCellTypes: ['raw']
  609. });
  610. }
  611. }
  612. /**
  613. * A namespace for private data.
  614. */
  615. namespace Private {
  616. /**
  617. * An object which holds a widget and its sort rank.
  618. */
  619. export interface IRankItem {
  620. /**
  621. * The widget for the item.
  622. */
  623. tool: CellTools.Tool;
  624. /**
  625. * The sort rank of the menu.
  626. */
  627. rank: number;
  628. }
  629. /**
  630. * A comparator function for widget rank items.
  631. */
  632. export function itemCmp(first: IRankItem, second: IRankItem): number {
  633. return first.rank - second.rank;
  634. }
  635. /**
  636. * Create the node for a KeySelector.
  637. */
  638. export function createSelectorNode(
  639. options: CellTools.KeySelector.IOptions
  640. ): HTMLElement {
  641. let name = options.key;
  642. let title = options.title || name[0].toLocaleUpperCase() + name.slice(1);
  643. let optionNodes: VirtualNode[] = [];
  644. for (let label in options.optionsMap) {
  645. let value = JSON.stringify(options.optionsMap[label]);
  646. optionNodes.push(h.option({ value }, label));
  647. }
  648. let node = VirtualDOM.realize(
  649. h.div({}, h.label(title), h.select({}, optionNodes))
  650. );
  651. Styling.styleNode(node);
  652. return node;
  653. }
  654. }