widget.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. 'use strict';
  4. import {
  5. IChangedArgs
  6. } from 'phosphor-properties';
  7. import {
  8. IObservableList, ObservableList, IListChangedArgs, ListChangeType
  9. } from 'phosphor-observablelist';
  10. import {
  11. Widget
  12. } from 'phosphor-widget';
  13. import {
  14. Panel
  15. } from 'phosphor-panel';
  16. import {
  17. NotebookModel, INotebookModel
  18. } from './model';
  19. import {
  20. ICellModel, CellType,
  21. CodeCellWidget, MarkdownCellWidget,
  22. CodeCellModel, MarkdownCellModel, isMarkdownCellModel
  23. } from '../cells';
  24. import {
  25. OutputAreaWidget, IOutputAreaModel
  26. } from '../output-area';
  27. import {
  28. DisposableDelegate, IDisposable
  29. } from 'phosphor-disposable';
  30. import './codemirror-ipython';
  31. import './codemirror-ipythongfm';
  32. /**
  33. * The class name added to notebook widgets.
  34. */
  35. const NB_CLASS = 'jp-Notebook';
  36. /**
  37. * The class name added to notebook widget cells.
  38. */
  39. const NB_CELL_CLASS = 'jp-Notebook-cell';
  40. /**
  41. * The class name added to notebook selected cells.
  42. */
  43. const NB_SELECTED_CLASS = 'jp-mod-selected';
  44. /**
  45. * A widget for a notebook.
  46. */
  47. export
  48. class NotebookWidget extends Panel {
  49. /**
  50. * Construct a code cell widget.
  51. */
  52. constructor(model: INotebookModel) {
  53. super();
  54. this.addClass(NB_CLASS);
  55. this._model = model;
  56. this._listdispose = follow<ICellModel>(model.cells, this, (c: ICellModel) => {
  57. let w: Widget;
  58. switch(c.type) {
  59. case "code":
  60. w = new CodeCellWidget(c as CodeCellModel);
  61. break;
  62. case "markdown":
  63. w = new MarkdownCellWidget(c as MarkdownCellModel);
  64. break;
  65. default:
  66. // if there are any issues, just return a blank placeholder
  67. // widget so the lists stay in sync
  68. w = new Widget();
  69. }
  70. w.addClass(NB_CELL_CLASS);
  71. return w;
  72. })
  73. this.updateSelectedCell(model.selectedCellIndex);
  74. // bind events that can select the cell
  75. // see https://github.com/jupyter/notebook/blob/203ccd3d4496cc22e6a1c5e6ece9f5a7d791472a/notebook/static/notebook/js/cell.js#L178
  76. this.node.addEventListener('click', (ev: MouseEvent) => {
  77. if (!this._model.readOnly) {
  78. this._model.selectedCellIndex = this.findCell(ev.target as HTMLElement);
  79. }
  80. })
  81. this.node.addEventListener('dblclick', (ev: MouseEvent) => {
  82. if (this._model.readOnly) {
  83. return;
  84. }
  85. let i = this.findCell(ev.target as HTMLElement);
  86. if (i === void 0) {
  87. return;
  88. }
  89. let cell = this._model.cells.get(i);
  90. if (isMarkdownCellModel(cell) && cell.rendered) {
  91. cell.rendered = false;
  92. cell.input.textEditor.select();
  93. }
  94. })
  95. model.stateChanged.connect(this.modelStateChanged, this);
  96. model.cells.changed.connect(this.cellsChanged, this);
  97. }
  98. /**
  99. * Find the cell index containing the target html element.
  100. *
  101. * #### Notes
  102. * Returns -1 if the cell is not found.
  103. */
  104. findCell(node: HTMLElement): number {
  105. // Trace up the DOM hierarchy to find the root cell node
  106. // then find the corresponding child and select it
  107. while (node && node !== this.node) {
  108. if (node.classList.contains(NB_CELL_CLASS)) {
  109. for (let i=0; i<this.childCount(); i++) {
  110. if (this.childAt(i).node === node) {
  111. return i;
  112. }
  113. }
  114. break;
  115. }
  116. node = node.parentElement;
  117. }
  118. return void 0;
  119. }
  120. /**
  121. * Handle a change cells event.
  122. */
  123. protected cellsChanged(sender: IObservableList<ICellModel>,
  124. args: IListChangedArgs<ICellModel>) {
  125. console.log(args);
  126. }
  127. /**
  128. * Handle a selection change event.
  129. */
  130. updateSelectedCell(newIndex: number, oldIndex?: number) {
  131. if (oldIndex !== void 0) {
  132. this.childAt(oldIndex).removeClass(NB_SELECTED_CLASS);
  133. }
  134. if (newIndex !== void 0) {
  135. let newCell = this.childAt(newIndex);
  136. newCell.addClass(NB_SELECTED_CLASS);
  137. scrollIfNeeded(this.node, newCell.node);
  138. }
  139. }
  140. /**
  141. * Change handler for model updates.
  142. */
  143. protected modelStateChanged(sender: INotebookModel, args: IChangedArgs<any>) {
  144. switch(args.name) {
  145. case 'defaultMimetype': break;
  146. case 'mode': break;
  147. case 'selectedCellIndex':
  148. this.updateSelectedCell(args.newValue, args.oldValue)
  149. }
  150. }
  151. /**
  152. * Dispose this model.
  153. */
  154. dispose() {
  155. this._listdispose.dispose();
  156. super.dispose();
  157. }
  158. /**
  159. * Get the model for the widget
  160. */
  161. get model(): INotebookModel {
  162. return this._model;
  163. }
  164. private _model: INotebookModel;
  165. private _listdispose: IDisposable;
  166. }
  167. /**
  168. * Make a panel mirror changes to an observable list.
  169. *
  170. * @param source - The observable list.
  171. * @param sink - The Panel.
  172. * @param factory - A function which takes an item from the list and constructs a widget.
  173. */
  174. function follow<T>(source: IObservableList<T>,
  175. sink: Panel,
  176. factory: (arg: T)=> Widget): IDisposable {
  177. for (let i = sink.childCount()-1; i>=0; i--) {
  178. sink.childAt(i).dispose();
  179. }
  180. for (let i=0; i<source.length; i++) {
  181. sink.addChild(factory(source.get(i)))
  182. }
  183. function callback(sender: ObservableList<T>, args: IListChangedArgs<T>) {
  184. switch(args.type) {
  185. case ListChangeType.Add:
  186. sink.insertChild(args.newIndex, factory(args.newValue as T))
  187. break;
  188. case ListChangeType.Move:
  189. sink.insertChild(args.newIndex, sink.childAt(args.oldIndex));
  190. break;
  191. case ListChangeType.Remove:
  192. sink.childAt(args.oldIndex).dispose();
  193. break;
  194. case ListChangeType.Replace:
  195. for (let i = (args.oldValue as T[]).length; i>0; i--) {
  196. sink.childAt(args.oldIndex).dispose();
  197. }
  198. for (let i = (args.newValue as T[]).length; i>0; i--) {
  199. sink.insertChild(args.newIndex, factory((args.newValue as T[])[i]))
  200. }
  201. break;
  202. case ListChangeType.Set:
  203. sink.childAt(args.newIndex).dispose();
  204. sink.insertChild(args.newIndex, factory(args.newValue as T))
  205. break;
  206. }
  207. }
  208. source.changed.connect(callback);
  209. return new DisposableDelegate(() => {
  210. source.changed.disconnect(callback);
  211. })
  212. }
  213. /**
  214. * Scroll an element into view if needed.
  215. *
  216. * @param area - The scroll area element.
  217. *
  218. * @param elem - The element of interest.
  219. */
  220. export
  221. function scrollIfNeeded(area: HTMLElement, elem: HTMLElement): void {
  222. let ar = area.getBoundingClientRect();
  223. let er = elem.getBoundingClientRect();
  224. if (er.top < ar.top) {
  225. area.scrollTop -= ar.top - er.top;
  226. } else if (er.bottom > ar.bottom) {
  227. area.scrollTop += er.bottom - ar.bottom;
  228. }
  229. }