widget.ts 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. ArrayExt, each
  5. } from '@phosphor/algorithm';
  6. import {
  7. JSONValue
  8. } from '@phosphor/coreutils';
  9. import {
  10. Message
  11. } from '@phosphor/messaging';
  12. import {
  13. MimeData
  14. } from '@phosphor/coreutils';
  15. import {
  16. AttachedProperty
  17. } from '@phosphor/properties';
  18. import {
  19. ISignal, Signal
  20. } from '@phosphor/signaling';
  21. import {
  22. Drag, IDragEvent
  23. } from '@phosphor/dragdrop';
  24. import {
  25. PanelLayout, Widget
  26. } from '@phosphor/widgets';
  27. import {
  28. h, VirtualDOM
  29. } from '@phosphor/virtualdom';
  30. import {
  31. ICellModel, Cell, IMarkdownCellModel,
  32. CodeCell, MarkdownCell,
  33. ICodeCellModel, RawCell, IRawCellModel
  34. } from '@jupyterlab/cells';
  35. import {
  36. IEditorMimeTypeService, CodeEditor
  37. } from '@jupyterlab/codeeditor';
  38. import {
  39. IChangedArgs, nbformat
  40. } from '@jupyterlab/coreutils';
  41. import {
  42. IObservableMap, IObservableList
  43. } from '@jupyterlab/observables';
  44. import {
  45. RenderMimeRegistry
  46. } from '@jupyterlab/rendermime';
  47. import {
  48. INotebookModel
  49. } from './model';
  50. /**
  51. * The data attribute added to a widget that has an active kernel.
  52. */
  53. const KERNEL_USER = 'jpKernelUser';
  54. /**
  55. * The data attribute added to a widget that can run code.
  56. */
  57. const CODE_RUNNER = 'jpCodeRunner';
  58. /**
  59. * The data attribute added to a widget that can undo.
  60. */
  61. const UNDOER = 'jpUndoer';
  62. /**
  63. * The class name added to notebook widgets.
  64. */
  65. const NB_CLASS = 'jp-Notebook';
  66. /**
  67. * The class name added to notebook widget cells.
  68. */
  69. const NB_CELL_CLASS = 'jp-Notebook-cell';
  70. /**
  71. * The class name added to a notebook in edit mode.
  72. */
  73. const EDIT_CLASS = 'jp-mod-editMode';
  74. /**
  75. * The class name added to a notebook in command mode.
  76. */
  77. const COMMAND_CLASS = 'jp-mod-commandMode';
  78. /**
  79. * The class name added to the active cell.
  80. */
  81. const ACTIVE_CLASS = 'jp-mod-active';
  82. /**
  83. * The class name added to selected cells.
  84. */
  85. const SELECTED_CLASS = 'jp-mod-selected';
  86. /**
  87. * The class name added to an active cell when there are other selected cells.
  88. */
  89. const OTHER_SELECTED_CLASS = 'jp-mod-multiSelected';
  90. /**
  91. * The class name added to unconfined images.
  92. */
  93. const UNCONFINED_CLASS = 'jp-mod-unconfined';
  94. /**
  95. * The class name added to a drop target.
  96. */
  97. const DROP_TARGET_CLASS = 'jp-mod-dropTarget';
  98. /**
  99. * The class name added to a drop source.
  100. */
  101. const DROP_SOURCE_CLASS = 'jp-mod-dropSource';
  102. /**
  103. * The class name added to drag images.
  104. */
  105. const DRAG_IMAGE_CLASS = 'jp-dragImage';
  106. /**
  107. * The class name added to singular drag images
  108. */
  109. const SINGLE_DRAG_IMAGE_CLASS = 'jp-dragImage-singlePrompt';
  110. /**
  111. * The class name added to the drag image cell content.
  112. */
  113. const CELL_DRAG_CONTENT_CLASS = 'jp-dragImage-content';
  114. /**
  115. * The class name added to the drag image cell content.
  116. */
  117. const CELL_DRAG_PROMPT_CLASS = 'jp-dragImage-prompt';
  118. /**
  119. * The class name added to the drag image cell content.
  120. */
  121. const CELL_DRAG_MULTIPLE_BACK = 'jp-dragImage-multipleBack';
  122. /**
  123. * The mimetype used for Jupyter cell data.
  124. */
  125. const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
  126. /**
  127. * The threshold in pixels to start a drag event.
  128. */
  129. const DRAG_THRESHOLD = 5;
  130. /**
  131. * The interactivity modes for the notebook.
  132. */
  133. export
  134. type NotebookMode = 'command' | 'edit';
  135. /**
  136. * A widget which renders static non-interactive notebooks.
  137. *
  138. * #### Notes
  139. * The widget model must be set separately and can be changed
  140. * at any time. Consumers of the widget must account for a
  141. * `null` model, and may want to listen to the `modelChanged`
  142. * signal.
  143. */
  144. export
  145. class StaticNotebook extends Widget {
  146. /**
  147. * Construct a notebook widget.
  148. */
  149. constructor(options: StaticNotebook.IOptions) {
  150. super();
  151. this.addClass(NB_CLASS);
  152. this.node.dataset[KERNEL_USER] = 'true';
  153. this.node.dataset[CODE_RUNNER] = 'true';
  154. this.node.dataset[UNDOER] = 'true';
  155. this.rendermime = options.rendermime;
  156. this.layout = new Private.NotebookPanelLayout();
  157. this.contentFactory = (
  158. options.contentFactory || StaticNotebook.defaultContentFactory
  159. );
  160. this.editorConfig = options.editorConfig ||
  161. StaticNotebook.defaultEditorConfig;
  162. this._mimetypeService = options.mimeTypeService;
  163. }
  164. /**
  165. * A signal emitted when the model of the notebook changes.
  166. */
  167. get modelChanged(): ISignal<this, void> {
  168. return this._modelChanged;
  169. }
  170. /**
  171. * A signal emitted when the model content changes.
  172. *
  173. * #### Notes
  174. * This is a convenience signal that follows the current model.
  175. */
  176. get modelContentChanged(): ISignal<this, void> {
  177. return this._modelContentChanged;
  178. }
  179. /**
  180. * The cell factory used by the widget.
  181. */
  182. readonly contentFactory: StaticNotebook.IContentFactory;
  183. /**
  184. * The Rendermime instance used by the widget.
  185. */
  186. readonly rendermime: RenderMimeRegistry;
  187. /**
  188. * The model for the widget.
  189. */
  190. get model(): INotebookModel {
  191. return this._model;
  192. }
  193. set model(newValue: INotebookModel) {
  194. newValue = newValue || null;
  195. if (this._model === newValue) {
  196. return;
  197. }
  198. let oldValue = this._model;
  199. this._model = newValue;
  200. if (oldValue && oldValue.modelDB.isCollaborative) {
  201. oldValue.modelDB.connected.then(() => {
  202. oldValue.modelDB.collaborators.changed.disconnect(
  203. this._onCollaboratorsChanged, this);
  204. });
  205. }
  206. if (newValue && newValue.modelDB.isCollaborative) {
  207. newValue.modelDB.connected.then(() => {
  208. newValue.modelDB.collaborators.changed.connect(
  209. this._onCollaboratorsChanged, this);
  210. });
  211. }
  212. // Trigger private, protected, and public changes.
  213. this._onModelChanged(oldValue, newValue);
  214. this.onModelChanged(oldValue, newValue);
  215. this._modelChanged.emit(void 0);
  216. }
  217. /**
  218. * Get the mimetype for code cells.
  219. */
  220. get codeMimetype(): string {
  221. return this._mimetype;
  222. }
  223. /**
  224. * A read-only sequence of the widgets in the notebook.
  225. */
  226. get widgets(): ReadonlyArray<Cell> {
  227. return (this.layout as PanelLayout).widgets as ReadonlyArray<Cell>;
  228. }
  229. /**
  230. * A configuration object for cell editor settings.
  231. */
  232. get editorConfig(): StaticNotebook.IEditorConfig {
  233. return this._editorConfig;
  234. }
  235. set editorConfig(value: StaticNotebook.IEditorConfig) {
  236. this._editorConfig = value;
  237. this._updateEditorConfig();
  238. }
  239. /**
  240. * Dispose of the resources held by the widget.
  241. */
  242. dispose() {
  243. // Do nothing if already disposed.
  244. if (this.isDisposed) {
  245. return;
  246. }
  247. this._model = null;
  248. super.dispose();
  249. }
  250. /**
  251. * Handle a new model.
  252. *
  253. * #### Notes
  254. * This method is called after the model change has been handled
  255. * internally and before the `modelChanged` signal is emitted.
  256. * The default implementation is a no-op.
  257. */
  258. protected onModelChanged(oldValue: INotebookModel, newValue: INotebookModel): void {
  259. // No-op.
  260. }
  261. /**
  262. * Handle changes to the notebook model content.
  263. *
  264. * #### Notes
  265. * The default implementation emits the `modelContentChanged` signal.
  266. */
  267. protected onModelContentChanged(model: INotebookModel, args: void): void {
  268. this._modelContentChanged.emit(void 0);
  269. }
  270. /**
  271. * Handle changes to the notebook model metadata.
  272. *
  273. * #### Notes
  274. * The default implementation updates the mimetypes of the code cells
  275. * when the `language_info` metadata changes.
  276. */
  277. protected onMetadataChanged(sender: IObservableMap<JSONValue>, args: IObservableMap.IChangedArgs<JSONValue>): void {
  278. switch (args.key) {
  279. case 'language_info':
  280. this._updateMimetype();
  281. break;
  282. default:
  283. break;
  284. }
  285. }
  286. /**
  287. * Handle a cell being inserted.
  288. *
  289. * The default implementation is a no-op
  290. */
  291. protected onCellInserted(index: number, cell: Cell): void {
  292. // This is a no-op.
  293. }
  294. /**
  295. * Handle a cell being moved.
  296. *
  297. * The default implementation is a no-op
  298. */
  299. protected onCellMoved(fromIndex: number, toIndex: number): void {
  300. // This is a no-op.
  301. }
  302. /**
  303. * Handle a cell being removed.
  304. *
  305. * The default implementation is a no-op
  306. */
  307. protected onCellRemoved(index: number, cell: Cell): void {
  308. // This is a no-op.
  309. }
  310. /**
  311. * Handle a new model on the widget.
  312. */
  313. private _onModelChanged(oldValue: INotebookModel, newValue: INotebookModel): void {
  314. let layout = this.layout as PanelLayout;
  315. if (oldValue) {
  316. oldValue.cells.changed.disconnect(this._onCellsChanged, this);
  317. oldValue.metadata.changed.disconnect(this.onMetadataChanged, this);
  318. oldValue.contentChanged.disconnect(this.onModelContentChanged, this);
  319. // TODO: reuse existing cell widgets if possible.
  320. while (layout.widgets.length) {
  321. this._removeCell(0);
  322. }
  323. }
  324. if (!newValue) {
  325. this._mimetype = 'text/plain';
  326. return;
  327. }
  328. this._updateMimetype();
  329. let cells = newValue.cells;
  330. each(cells, (cell: ICellModel, i: number) => {
  331. this._insertCell(i, cell);
  332. });
  333. cells.changed.connect(this._onCellsChanged, this);
  334. newValue.contentChanged.connect(this.onModelContentChanged, this);
  335. newValue.metadata.changed.connect(this.onMetadataChanged, this);
  336. }
  337. /**
  338. * Handle a change cells event.
  339. */
  340. private _onCellsChanged(sender: IObservableList<ICellModel>, args: IObservableList.IChangedArgs<ICellModel>) {
  341. let index = 0;
  342. switch (args.type) {
  343. case 'add':
  344. index = args.newIndex;
  345. each(args.newValues, value => {
  346. this._insertCell(index++, value);
  347. });
  348. break;
  349. case 'move':
  350. this._moveCell(args.oldIndex, args.newIndex);
  351. break;
  352. case 'remove':
  353. each(args.oldValues, value => {
  354. this._removeCell(args.oldIndex);
  355. });
  356. break;
  357. case 'set':
  358. // TODO: reuse existing widgets if possible.
  359. index = args.newIndex;
  360. each(args.newValues, value => {
  361. // Note: this ordering (insert then remove)
  362. // is important for getting the active cell
  363. // index for the editable notebook correct.
  364. this._insertCell(index, value);
  365. this._removeCell(index + 1);
  366. index++;
  367. });
  368. break;
  369. default:
  370. return;
  371. }
  372. }
  373. /**
  374. * Create a cell widget and insert into the notebook.
  375. */
  376. private _insertCell(index: number, cell: ICellModel): void {
  377. let widget: Cell;
  378. switch (cell.type) {
  379. case 'code':
  380. widget = this._createCodeCell(cell as ICodeCellModel);
  381. widget.model.mimeType = this._mimetype;
  382. break;
  383. case 'markdown':
  384. widget = this._createMarkdownCell(cell as IMarkdownCellModel);
  385. break;
  386. default:
  387. widget = this._createRawCell(cell as IRawCellModel);
  388. }
  389. widget.addClass(NB_CELL_CLASS);
  390. let layout = this.layout as PanelLayout;
  391. layout.insertWidget(index, widget);
  392. this.onCellInserted(index, widget);
  393. }
  394. /**
  395. * Create a code cell widget from a code cell model.
  396. */
  397. private _createCodeCell(model: ICodeCellModel): CodeCell {
  398. let rendermime = this.rendermime;
  399. let contentFactory = this.contentFactory;
  400. const editorConfig = this.editorConfig.code;
  401. let options = { editorConfig, model, rendermime, contentFactory };
  402. return this.contentFactory.createCodeCell(options, this);
  403. }
  404. /**
  405. * Create a markdown cell widget from a markdown cell model.
  406. */
  407. private _createMarkdownCell(model: IMarkdownCellModel): MarkdownCell {
  408. let rendermime = this.rendermime;
  409. let contentFactory = this.contentFactory;
  410. const editorConfig = this.editorConfig.markdown;
  411. let options = { editorConfig, model, rendermime, contentFactory };
  412. return this.contentFactory.createMarkdownCell(options, this);
  413. }
  414. /**
  415. * Create a raw cell widget from a raw cell model.
  416. */
  417. private _createRawCell(model: IRawCellModel): RawCell {
  418. let contentFactory = this.contentFactory;
  419. const editorConfig = this.editorConfig.raw;
  420. let options = { editorConfig, model, contentFactory };
  421. return this.contentFactory.createRawCell(options, this);
  422. }
  423. /**
  424. * Move a cell widget.
  425. */
  426. private _moveCell(fromIndex: number, toIndex: number): void {
  427. let layout = this.layout as PanelLayout;
  428. layout.insertWidget(toIndex, layout.widgets[fromIndex]);
  429. this.onCellMoved(fromIndex, toIndex);
  430. }
  431. /**
  432. * Remove a cell widget.
  433. */
  434. private _removeCell(index: number): void {
  435. let layout = this.layout as PanelLayout;
  436. let widget = layout.widgets[index] as Cell;
  437. widget.parent = null;
  438. this.onCellRemoved(index, widget);
  439. widget.dispose();
  440. }
  441. /**
  442. * Update the mimetype of the notebook.
  443. */
  444. private _updateMimetype(): void {
  445. let info = this._model.metadata.get('language_info') as nbformat.ILanguageInfoMetadata;
  446. if (!info) {
  447. return;
  448. }
  449. this._mimetype = this._mimetypeService.getMimeTypeByLanguage(info);
  450. each(this.widgets, widget => {
  451. if (widget.model.type === 'code') {
  452. widget.model.mimeType = this._mimetype;
  453. }
  454. });
  455. }
  456. /**
  457. * Handle an update to the collaborators.
  458. */
  459. private _onCollaboratorsChanged(): void {
  460. // If there are selections corresponding to non-collaborators,
  461. // they are stale and should be removed.
  462. for (let i = 0; i < this.widgets.length; i++) {
  463. let cell = this.widgets[i];
  464. for (let key of cell.model.selections.keys()) {
  465. if (!this._model.modelDB.collaborators.has(key)) {
  466. cell.model.selections.delete(key);
  467. }
  468. }
  469. }
  470. }
  471. /**
  472. * Update editor settings for notebook cells.
  473. */
  474. private _updateEditorConfig() {
  475. for (let i = 0; i < this.widgets.length; i++) {
  476. const cell = this.widgets[i];
  477. let config: Partial<CodeEditor.IConfig>;
  478. switch (cell.model.type) {
  479. case 'code':
  480. config = this._editorConfig.code;
  481. break;
  482. case 'markdown':
  483. config = this._editorConfig.markdown;
  484. break;
  485. default:
  486. config = this._editorConfig.raw;
  487. break;
  488. }
  489. Object.keys(config).forEach((key: keyof CodeEditor.IConfig) => {
  490. cell.editor.setOption(key, config[key]);
  491. });
  492. }
  493. }
  494. private _editorConfig = StaticNotebook.defaultEditorConfig;
  495. private _mimetype = 'text/plain';
  496. private _model: INotebookModel = null;
  497. private _mimetypeService: IEditorMimeTypeService;
  498. private _modelChanged = new Signal<this, void>(this);
  499. private _modelContentChanged = new Signal<this, void>(this);
  500. }
  501. /**
  502. * The namespace for the `StaticNotebook` class statics.
  503. */
  504. export
  505. namespace StaticNotebook {
  506. /**
  507. * An options object for initializing a static notebook.
  508. */
  509. export
  510. interface IOptions {
  511. /**
  512. * The rendermime instance used by the widget.
  513. */
  514. rendermime: RenderMimeRegistry;
  515. /**
  516. * The language preference for the model.
  517. */
  518. languagePreference?: string;
  519. /**
  520. * A factory for creating content.
  521. */
  522. contentFactory?: IContentFactory;
  523. /**
  524. * A configuration object for the cell editor settings.
  525. */
  526. editorConfig?: IEditorConfig;
  527. /**
  528. * The service used to look up mime types.
  529. */
  530. mimeTypeService: IEditorMimeTypeService;
  531. }
  532. /**
  533. * A factory for creating notebook content.
  534. *
  535. * #### Notes
  536. * This extends the content factory of the cell itself, which extends the content
  537. * factory of the output area and input area. The result is that there is a single
  538. * factory for creating all child content of a notebook.
  539. */
  540. export
  541. interface IContentFactory extends Cell.IContentFactory {
  542. /**
  543. * Create a new code cell widget.
  544. */
  545. createCodeCell(options: CodeCell.IOptions, parent: StaticNotebook): CodeCell;
  546. /**
  547. * Create a new markdown cell widget.
  548. */
  549. createMarkdownCell(options: MarkdownCell.IOptions, parent: StaticNotebook): MarkdownCell;
  550. /**
  551. * Create a new raw cell widget.
  552. */
  553. createRawCell(options: RawCell.IOptions, parent: StaticNotebook): RawCell;
  554. }
  555. /**
  556. * A config object for the cell editors.
  557. */
  558. export
  559. interface IEditorConfig {
  560. /**
  561. * Config options for code cells.
  562. */
  563. readonly code: Partial<CodeEditor.IConfig>;
  564. /**
  565. * Config options for markdown cells.
  566. */
  567. readonly markdown: Partial<CodeEditor.IConfig>;
  568. /**
  569. * Config options for raw cells.
  570. */
  571. readonly raw: Partial<CodeEditor.IConfig>;
  572. }
  573. /**
  574. * Default configuration options for cell editors.
  575. */
  576. export
  577. const defaultEditorConfig: IEditorConfig = {
  578. code: {
  579. ...CodeEditor.defaultConfig,
  580. lineWrap: false,
  581. matchBrackets: true,
  582. autoClosingBrackets: true
  583. },
  584. markdown: {
  585. ...CodeEditor.defaultConfig,
  586. lineWrap: true,
  587. matchBrackets: false,
  588. autoClosingBrackets: false
  589. },
  590. raw: {
  591. ...CodeEditor.defaultConfig,
  592. lineWrap: true,
  593. matchBrackets: false,
  594. autoClosingBrackets: false
  595. }
  596. };
  597. /**
  598. * The default implementation of an `IContentFactory`.
  599. */
  600. export
  601. class ContentFactory extends Cell.ContentFactory implements IContentFactory {
  602. /**
  603. * Create a new code cell widget.
  604. *
  605. * #### Notes
  606. * If no cell content factory is passed in with the options, the one on the
  607. * notebook content factory is used.
  608. */
  609. createCodeCell(options: CodeCell.IOptions, parent: StaticNotebook): CodeCell {
  610. if (!options.contentFactory) {
  611. options.contentFactory = this;
  612. }
  613. return new CodeCell(options);
  614. }
  615. /**
  616. * Create a new markdown cell widget.
  617. *
  618. * #### Notes
  619. * If no cell content factory is passed in with the options, the one on the
  620. * notebook content factory is used.
  621. */
  622. createMarkdownCell(options: MarkdownCell.IOptions, parent: StaticNotebook): MarkdownCell {
  623. if (!options.contentFactory) {
  624. options.contentFactory = this;
  625. }
  626. return new MarkdownCell(options);
  627. }
  628. /**
  629. * Create a new raw cell widget.
  630. *
  631. * #### Notes
  632. * If no cell content factory is passed in with the options, the one on the
  633. * notebook content factory is used.
  634. */
  635. createRawCell(options: RawCell.IOptions, parent: StaticNotebook): RawCell {
  636. if (!options.contentFactory) {
  637. options.contentFactory = this;
  638. }
  639. return new RawCell(options);
  640. }
  641. }
  642. /**
  643. * A namespace for the staic notebook content factory.
  644. */
  645. export
  646. namespace ContentFactory {
  647. /**
  648. * Options for the content factory.
  649. */
  650. export
  651. interface IOptions extends Cell.ContentFactory.IOptions { }
  652. }
  653. /**
  654. * Default content factory for the static notebook widget.
  655. */
  656. export
  657. const defaultContentFactory: IContentFactory = new ContentFactory();
  658. }
  659. /**
  660. * A notebook widget that supports interactivity.
  661. */
  662. export
  663. class Notebook extends StaticNotebook {
  664. /**
  665. * Construct a notebook widget.
  666. */
  667. constructor(options: Notebook.IOptions) {
  668. super( Private.processNotebookOptions(options) );
  669. this.node.tabIndex = -1; // Allow the widget to take focus.
  670. // Allow the node to scroll while dragging items.
  671. this.node.setAttribute('data-p-dragscroll', 'true');
  672. }
  673. /**
  674. * A signal emitted when the active cell changes.
  675. *
  676. * #### Notes
  677. * This can be due to the active index changing or the
  678. * cell at the active index changing.
  679. */
  680. get activeCellChanged(): ISignal<this, Cell> {
  681. return this._activeCellChanged;
  682. }
  683. /**
  684. * A signal emitted when the state of the notebook changes.
  685. */
  686. get stateChanged(): ISignal<this, IChangedArgs<any>> {
  687. return this._stateChanged;
  688. }
  689. /**
  690. * A signal emitted when the selection state of the notebook changes.
  691. */
  692. get selectionChanged(): ISignal<this, void> {
  693. return this._selectionChanged;
  694. }
  695. /**
  696. * The interactivity mode of the notebook.
  697. */
  698. get mode(): NotebookMode {
  699. return this._mode;
  700. }
  701. set mode(newValue: NotebookMode) {
  702. let activeCell = this.activeCell;
  703. if (!activeCell) {
  704. newValue = 'command';
  705. }
  706. if (newValue === this._mode) {
  707. this._ensureFocus();
  708. return;
  709. }
  710. // Post an update request.
  711. this.update();
  712. let oldValue = this._mode;
  713. this._mode = newValue;
  714. if (newValue === 'edit') {
  715. // Edit mode deselects all cells.
  716. each(this.widgets, widget => { this.deselect(widget); });
  717. // Edit mode unrenders an active markdown widget.
  718. if (activeCell instanceof MarkdownCell) {
  719. activeCell.rendered = false;
  720. }
  721. activeCell.inputHidden = false;
  722. }
  723. this._stateChanged.emit({ name: 'mode', oldValue, newValue });
  724. this._ensureFocus();
  725. }
  726. /**
  727. * The active cell index of the notebook.
  728. *
  729. * #### Notes
  730. * The index will be clamped to the bounds of the notebook cells.
  731. */
  732. get activeCellIndex(): number {
  733. if (!this.model) {
  734. return -1;
  735. }
  736. return this.model.cells.length ? this._activeCellIndex : -1;
  737. }
  738. set activeCellIndex(newValue: number) {
  739. let oldValue = this._activeCellIndex;
  740. if (!this.model || !this.model.cells.length) {
  741. newValue = -1;
  742. } else {
  743. newValue = Math.max(newValue, 0);
  744. newValue = Math.min(newValue, this.model.cells.length - 1);
  745. }
  746. this._activeCellIndex = newValue;
  747. let cell = this.widgets[newValue];
  748. if (cell !== this._activeCell) {
  749. // Post an update request.
  750. this.update();
  751. this._activeCell = cell;
  752. this._activeCellChanged.emit(cell);
  753. }
  754. if (this.mode === 'edit' && cell instanceof MarkdownCell) {
  755. cell.rendered = false;
  756. }
  757. this._ensureFocus();
  758. if (newValue === oldValue) {
  759. return;
  760. }
  761. this._trimSelections();
  762. this._stateChanged.emit({ name: 'activeCellIndex', oldValue, newValue });
  763. }
  764. /**
  765. * Get the active cell widget.
  766. */
  767. get activeCell(): Cell {
  768. return this._activeCell;
  769. }
  770. /**
  771. * Dispose of the resources held by the widget.
  772. */
  773. dispose(): void {
  774. if (this._activeCell === null) {
  775. return;
  776. }
  777. this._activeCell = null;
  778. super.dispose();
  779. }
  780. /**
  781. * Select a cell widget.
  782. *
  783. * #### Notes
  784. * It is a no-op if the value does not change.
  785. * It will emit the `selectionChanged` signal.
  786. */
  787. select(widget: Cell): void {
  788. if (Private.selectedProperty.get(widget)) {
  789. return;
  790. }
  791. Private.selectedProperty.set(widget, true);
  792. this._selectionChanged.emit(void 0);
  793. this.update();
  794. }
  795. /**
  796. * Deselect a cell widget.
  797. *
  798. * #### Notes
  799. * It is a no-op if the value does not change.
  800. * It will emit the `selectionChanged` signal.
  801. */
  802. deselect(widget: Cell): void {
  803. if (!Private.selectedProperty.get(widget)) {
  804. return;
  805. }
  806. Private.selectedProperty.set(widget, false);
  807. this._selectionChanged.emit(void 0);
  808. this.update();
  809. }
  810. /**
  811. * Whether a cell is selected.
  812. */
  813. isSelected(widget: Cell): boolean {
  814. return Private.selectedProperty.get(widget);
  815. }
  816. /**
  817. * Whether a cell is selected or is the active cell.
  818. */
  819. isSelectedOrActive(widget: Cell): boolean {
  820. if (widget === this._activeCell) {
  821. return true;
  822. }
  823. return Private.selectedProperty.get(widget);
  824. }
  825. /**
  826. * Deselect all of the cells.
  827. */
  828. deselectAll(): void {
  829. let changed = false;
  830. each(this.widgets, widget => {
  831. if (Private.selectedProperty.get(widget)) {
  832. changed = true;
  833. }
  834. Private.selectedProperty.set(widget, false);
  835. });
  836. if (changed) {
  837. this._selectionChanged.emit(void 0);
  838. }
  839. // Make sure we have a valid active cell.
  840. this.activeCellIndex = this.activeCellIndex;
  841. this.update();
  842. }
  843. /**
  844. * Move the head of an existing contiguous selection to extend the selection.
  845. *
  846. * @param index - The new head of the existing selection.
  847. *
  848. * #### Notes
  849. * If there is no existing selection, the active cell is considered an
  850. * existing one-cell selection.
  851. *
  852. * If the new selection is a single cell, that cell becomes the active cell
  853. * and all cells are deselected.
  854. *
  855. * There is no change if there are no cells (i.e., activeCellIndex is -1).
  856. */
  857. extendContiguousSelectionTo(index: number): void {
  858. let {head, anchor} = this.getContiguousSelection();
  859. let i: number;
  860. // Handle the case of no current selection.
  861. if (anchor === null) {
  862. if (index === this.activeCellIndex) {
  863. // Already collapsed selection, nothing more to do.
  864. return;
  865. }
  866. // We will start a new selection below.
  867. head = this.activeCellIndex;
  868. anchor = this.activeCellIndex;
  869. }
  870. // Move the active cell. We do this before the collapsing shortcut below.
  871. this.activeCellIndex = index;
  872. // Make sure the index is valid, according to the rules for setting and clipping the
  873. // active cell index. This may change the index.
  874. index = this.activeCellIndex;
  875. // Collapse the selection if it is only the active cell.
  876. if (index === anchor) {
  877. this.deselectAll();
  878. return;
  879. }
  880. let selectionChanged = false;
  881. if (head < index) {
  882. if (head < anchor) {
  883. Private.selectedProperty.set(this.widgets[head], false);
  884. selectionChanged = true;
  885. }
  886. // Toggle everything strictly between head and index except anchor.
  887. for (i = head + 1; i < index; i++) {
  888. if (i !== anchor) {
  889. Private.selectedProperty.set(this.widgets[i], !Private.selectedProperty.get(this.widgets[i]));
  890. selectionChanged = true;
  891. }
  892. }
  893. } else if (index < head) {
  894. if (anchor < head) {
  895. Private.selectedProperty.set(this.widgets[head], false);
  896. selectionChanged = true;
  897. }
  898. // Toggle everything strictly between index and head except anchor.
  899. for (i = index + 1; i < head; i++) {
  900. if (i !== anchor) {
  901. Private.selectedProperty.set(this.widgets[i], !Private.selectedProperty.get(this.widgets[i]));
  902. selectionChanged = true;
  903. }
  904. }
  905. }
  906. // Anchor and index should *always* be selected.
  907. if (!Private.selectedProperty.get(this.widgets[anchor])) {
  908. selectionChanged = true;
  909. }
  910. Private.selectedProperty.set(this.widgets[anchor], true);
  911. if (!Private.selectedProperty.get(this.widgets[index])) {
  912. selectionChanged = true;
  913. }
  914. Private.selectedProperty.set(this.widgets[index], true);
  915. if (selectionChanged) {
  916. this._selectionChanged.emit(void 0);
  917. }
  918. }
  919. /**
  920. * Get the head and anchor of a contiguous cell selection.
  921. *
  922. * The head of a contiguous selection is always the active cell.
  923. *
  924. * If there are no cells selected, `{head: null, anchor: null}` is returned.
  925. *
  926. * Throws an error if the currently selected cells do not form a contiguous
  927. * selection.
  928. */
  929. getContiguousSelection(): {head: number | null, anchor: number | null} {
  930. let cells = this.widgets;
  931. let first = ArrayExt.findFirstIndex(cells, c => this.isSelected(c));
  932. // Return early if no cells are selected.
  933. if (first === -1) {
  934. return {head: null, anchor: null};
  935. }
  936. let last = ArrayExt.findLastIndex(cells, c => this.isSelected(c), -1, first);
  937. // Check that the selection is contiguous.
  938. for (let i = first; i <= last; i++) {
  939. if (!this.isSelected(cells[i])) {
  940. throw new Error('Selection not contiguous');
  941. }
  942. }
  943. // Check that the active cell is one of the endpoints of the selection.
  944. let activeIndex = this.activeCellIndex;
  945. if (first !== activeIndex && last !== activeIndex) {
  946. throw new Error('Active cell not at endpoint of selection');
  947. }
  948. // Determine the head and anchor of the selection.
  949. if (first === activeIndex) {
  950. return {head: first, anchor: last};
  951. } else {
  952. return {head: last, anchor: first};
  953. }
  954. }
  955. /**
  956. * Scroll so that the given position is visible.
  957. *
  958. * @param position - The vertical position in the notebook widget.
  959. *
  960. * @param threshold - An optional threshold for the scroll. Defaults to 25
  961. * percent of the widget height.
  962. */
  963. scrollToPosition(position: number, threshold=25): void {
  964. let node = this.node;
  965. let ar = node.getBoundingClientRect();
  966. let delta = position - ar.top - ar.height / 2;
  967. if (Math.abs(delta) > ar.height * threshold / 100) {
  968. node.scrollTop += delta;
  969. }
  970. }
  971. /**
  972. * Handle the DOM events for the widget.
  973. *
  974. * @param event - The DOM event sent to the widget.
  975. *
  976. * #### Notes
  977. * This method implements the DOM `EventListener` interface and is
  978. * called in response to events on the notebook panel's node. It should
  979. * not be called directly by user code.
  980. */
  981. handleEvent(event: Event): void {
  982. if (!this.model) {
  983. return;
  984. }
  985. switch (event.type) {
  986. case 'mousedown':
  987. this._evtMouseDown(event as MouseEvent);
  988. break;
  989. case 'mouseup':
  990. if (event.currentTarget === document) {
  991. this._evtDocumentMouseup(event as MouseEvent);
  992. }
  993. break;
  994. case 'mousemove':
  995. if (event.currentTarget === document) {
  996. this._evtDocumentMousemove(event as MouseEvent);
  997. }
  998. break;
  999. case 'keydown':
  1000. this._ensureFocus(true);
  1001. break;
  1002. case 'dblclick':
  1003. this._evtDblClick(event as MouseEvent);
  1004. break;
  1005. case 'focus':
  1006. this._evtFocus(event as MouseEvent);
  1007. break;
  1008. case 'blur':
  1009. this._evtBlur(event as MouseEvent);
  1010. break;
  1011. case 'p-dragenter':
  1012. this._evtDragEnter(event as IDragEvent);
  1013. break;
  1014. case 'p-dragleave':
  1015. this._evtDragLeave(event as IDragEvent);
  1016. break;
  1017. case 'p-dragover':
  1018. this._evtDragOver(event as IDragEvent);
  1019. break;
  1020. case 'p-drop':
  1021. this._evtDrop(event as IDragEvent);
  1022. break;
  1023. default:
  1024. break;
  1025. }
  1026. }
  1027. /**
  1028. * Handle `after-attach` messages for the widget.
  1029. */
  1030. protected onAfterAttach(msg: Message): void {
  1031. super.onAfterAttach(msg);
  1032. let node = this.node;
  1033. node.addEventListener('mousedown', this);
  1034. node.addEventListener('keydown', this);
  1035. node.addEventListener('dblclick', this);
  1036. node.addEventListener('focus', this, true);
  1037. node.addEventListener('blur', this, true);
  1038. node.addEventListener('p-dragenter', this);
  1039. node.addEventListener('p-dragleave', this);
  1040. node.addEventListener('p-dragover', this);
  1041. node.addEventListener('p-drop', this);
  1042. }
  1043. /**
  1044. * Handle `before-detach` messages for the widget.
  1045. */
  1046. protected onBeforeDetach(msg: Message): void {
  1047. let node = this.node;
  1048. node.removeEventListener('mousedown', this);
  1049. node.removeEventListener('keydown', this);
  1050. node.removeEventListener('dblclick', this);
  1051. node.removeEventListener('focus', this, true);
  1052. node.removeEventListener('blur', this, true);
  1053. node.removeEventListener('p-dragenter', this);
  1054. node.removeEventListener('p-dragleave', this);
  1055. node.removeEventListener('p-dragover', this);
  1056. node.removeEventListener('p-drop', this);
  1057. document.removeEventListener('mousemove', this, true);
  1058. document.removeEventListener('mouseup', this, true);
  1059. }
  1060. /**
  1061. * Handle `'activate-request'` messages.
  1062. */
  1063. protected onActivateRequest(msg: Message): void {
  1064. this._ensureFocus(true);
  1065. }
  1066. /**
  1067. * Handle `update-request` messages sent to the widget.
  1068. */
  1069. protected onUpdateRequest(msg: Message): void {
  1070. let activeCell = this.activeCell;
  1071. // Set the appropriate classes on the cells.
  1072. if (this.mode === 'edit') {
  1073. this.addClass(EDIT_CLASS);
  1074. this.removeClass(COMMAND_CLASS);
  1075. } else {
  1076. this.addClass(COMMAND_CLASS);
  1077. this.removeClass(EDIT_CLASS);
  1078. }
  1079. if (activeCell) {
  1080. activeCell.addClass(ACTIVE_CLASS);
  1081. }
  1082. let count = 0;
  1083. each(this.widgets, widget => {
  1084. if (widget !== activeCell) {
  1085. widget.removeClass(ACTIVE_CLASS);
  1086. }
  1087. widget.removeClass(OTHER_SELECTED_CLASS);
  1088. if (this.isSelectedOrActive(widget)) {
  1089. widget.addClass(SELECTED_CLASS);
  1090. count++;
  1091. } else {
  1092. widget.removeClass(SELECTED_CLASS);
  1093. }
  1094. });
  1095. if (count > 1) {
  1096. activeCell.addClass(OTHER_SELECTED_CLASS);
  1097. }
  1098. }
  1099. /**
  1100. * Handle a cell being inserted.
  1101. */
  1102. protected onCellInserted(index: number, cell: Cell): void {
  1103. if (this.model && this.model.modelDB.isCollaborative) {
  1104. let modelDB = this.model.modelDB;
  1105. modelDB.connected.then(() => {
  1106. if (!cell.isDisposed) {
  1107. // Setup the selection style for collaborators.
  1108. let localCollaborator = modelDB.collaborators.localCollaborator;
  1109. cell.editor.uuid = localCollaborator.sessionId;
  1110. cell.editor.selectionStyle = {
  1111. ...CodeEditor.defaultSelectionStyle,
  1112. color: localCollaborator.color
  1113. };
  1114. }
  1115. });
  1116. }
  1117. cell.editor.edgeRequested.connect(this._onEdgeRequest, this);
  1118. // If the insertion happened above, increment the active cell
  1119. // index, otherwise it stays the same.
  1120. this.activeCellIndex = index <= this.activeCellIndex ?
  1121. this.activeCellIndex + 1 : this.activeCellIndex ;
  1122. }
  1123. /**
  1124. * Handle a cell being moved.
  1125. */
  1126. protected onCellMoved(fromIndex: number, toIndex: number): void {
  1127. let i = this.activeCellIndex;
  1128. if (fromIndex === i) {
  1129. this.activeCellIndex = toIndex;
  1130. } else if (fromIndex < i && i <= toIndex) {
  1131. this.activeCellIndex--;
  1132. } else if (toIndex <= i && i < fromIndex) {
  1133. this.activeCellIndex++;
  1134. }
  1135. }
  1136. /**
  1137. * Handle a cell being removed.
  1138. */
  1139. protected onCellRemoved(index: number, cell: Cell): void {
  1140. // If the removal happened above, decrement the active
  1141. // cell index, otherwise it stays the same.
  1142. this.activeCellIndex = index <= this.activeCellIndex ?
  1143. this.activeCellIndex - 1 : this.activeCellIndex ;
  1144. if (this.isSelected(cell)) {
  1145. this._selectionChanged.emit(void 0);
  1146. }
  1147. }
  1148. /**
  1149. * Handle a new model.
  1150. */
  1151. protected onModelChanged(oldValue: INotebookModel, newValue: INotebookModel): void {
  1152. // Try to set the active cell index to 0.
  1153. // It will be set to `-1` if there is no new model or the model is empty.
  1154. this.activeCellIndex = 0;
  1155. }
  1156. /**
  1157. * Handle edge request signals from cells.
  1158. */
  1159. private _onEdgeRequest(editor: CodeEditor.IEditor, location: CodeEditor.EdgeLocation): void {
  1160. let prev = this.activeCellIndex;
  1161. if (location === 'top') {
  1162. this.activeCellIndex--;
  1163. // Move the cursor to the first position on the last line.
  1164. if (this.activeCellIndex < prev) {
  1165. let editor = this.activeCell.editor;
  1166. let lastLine = editor.lineCount - 1;
  1167. editor.setCursorPosition({ line: lastLine, column: 0 });
  1168. }
  1169. } else {
  1170. this.activeCellIndex++;
  1171. // Move the cursor to the first character.
  1172. if (this.activeCellIndex > prev) {
  1173. let editor = this.activeCell.editor;
  1174. editor.setCursorPosition({ line: 0, column: 0 });
  1175. }
  1176. }
  1177. }
  1178. /**
  1179. * Ensure that the notebook has proper focus.
  1180. */
  1181. private _ensureFocus(force=false): void {
  1182. let activeCell = this.activeCell;
  1183. if (this.mode === 'edit' && activeCell) {
  1184. activeCell.editor.focus();
  1185. } else if (activeCell) {
  1186. activeCell.editor.blur();
  1187. }
  1188. if (force && !this.node.contains(document.activeElement)) {
  1189. this.node.focus();
  1190. }
  1191. }
  1192. /**
  1193. * Find the cell index containing the target html element.
  1194. *
  1195. * #### Notes
  1196. * Returns -1 if the cell is not found.
  1197. */
  1198. private _findCell(node: HTMLElement): number {
  1199. // Trace up the DOM hierarchy to find the root cell node.
  1200. // Then find the corresponding child and select it.
  1201. while (node && node !== this.node) {
  1202. if (node.classList.contains(NB_CELL_CLASS)) {
  1203. let i = ArrayExt.findFirstIndex(this.widgets, widget => widget.node === node);
  1204. if (i !== -1) {
  1205. return i;
  1206. }
  1207. break;
  1208. }
  1209. node = node.parentElement;
  1210. }
  1211. return -1;
  1212. }
  1213. /**
  1214. * Handle `mousedown` events for the widget.
  1215. */
  1216. private _evtMouseDown(event: MouseEvent): void {
  1217. // Mouse click should always ensure the notebook is focused.
  1218. this._ensureFocus(true);
  1219. // We only handle main or secondary button actions.
  1220. if (!(event.button === 0 || event.button === 2)) {
  1221. return;
  1222. }
  1223. // Try to find the cell associated with the event.
  1224. let target = event.target as HTMLElement;
  1225. let index = this._findCell(target);
  1226. let widget = this.widgets[index];
  1227. // Switch to command mode if the click is not in an editor.
  1228. if (index === -1 || !widget.editorWidget.node.contains(target)) {
  1229. this.mode = 'command';
  1230. }
  1231. // Secondary click deselects cells and possibly changes the active cell.
  1232. if (event.button === 2) {
  1233. this.deselectAll();
  1234. if (index !== -1) {
  1235. this.activeCellIndex = index;
  1236. }
  1237. return;
  1238. }
  1239. if (index !== -1) {
  1240. if (event.shiftKey) {
  1241. // Prevent text select behavior.
  1242. event.preventDefault();
  1243. event.stopPropagation();
  1244. try {
  1245. this.extendContiguousSelectionTo(index);
  1246. } catch (e) {
  1247. console.error(e);
  1248. this.deselectAll();
  1249. return;
  1250. }
  1251. // Enter selecting mode
  1252. this._mouseMode = 'select';
  1253. document.addEventListener('mouseup', this, true);
  1254. document.addEventListener('mousemove', this, true);
  1255. } else {
  1256. // Check if we need to change the active cell.
  1257. if (!this.isSelectedOrActive(widget)) {
  1258. this.deselectAll();
  1259. this.activeCellIndex = index;
  1260. }
  1261. // Prepare to start a drag if we are on the drag region.
  1262. if (widget.promptNode.contains(target)) {
  1263. // Prepare for a drag start
  1264. this._dragData = { pressX: event.clientX, pressY: event.clientY, index: index};
  1265. // Enter possible drag mode
  1266. this._mouseMode = 'couldDrag';
  1267. document.addEventListener('mouseup', this, true);
  1268. document.addEventListener('mousemove', this, true);
  1269. event.preventDefault();
  1270. }
  1271. }
  1272. }
  1273. }
  1274. /**
  1275. * Handle the `'mouseup'` event on the document.
  1276. */
  1277. private _evtDocumentMouseup(event: MouseEvent): void {
  1278. // Remove the event listeners we put on the document
  1279. document.removeEventListener('mousemove', this, true);
  1280. document.removeEventListener('mouseup', this, true);
  1281. this._mouseMode = null;
  1282. event.preventDefault();
  1283. event.stopPropagation();
  1284. }
  1285. /**
  1286. * Handle the `'mousemove'` event for the widget.
  1287. */
  1288. private _evtDocumentMousemove(event: MouseEvent): void {
  1289. event.preventDefault();
  1290. event.stopPropagation();
  1291. // If in select mode, update the selection
  1292. switch (this._mouseMode) {
  1293. case 'select':
  1294. let target = event.target as HTMLElement;
  1295. let index = this._findCell(target);
  1296. if (index !== -1) {
  1297. this.extendContiguousSelectionTo(index);
  1298. }
  1299. break;
  1300. case 'couldDrag':
  1301. // Check for a drag initialization.
  1302. let data = this._dragData;
  1303. let dx = Math.abs(event.clientX - data.pressX);
  1304. let dy = Math.abs(event.clientY - data.pressY);
  1305. if (dx >= DRAG_THRESHOLD || dy >= DRAG_THRESHOLD) {
  1306. this._mouseMode = null;
  1307. this._startDrag(data.index, event.clientX, event.clientY);
  1308. }
  1309. break;
  1310. default:
  1311. break;
  1312. }
  1313. }
  1314. /**
  1315. * Handle the `'p-dragenter'` event for the widget.
  1316. */
  1317. private _evtDragEnter(event: IDragEvent): void {
  1318. if (!event.mimeData.hasData(JUPYTER_CELL_MIME)) {
  1319. return;
  1320. }
  1321. event.preventDefault();
  1322. event.stopPropagation();
  1323. let target = event.target as HTMLElement;
  1324. let index = this._findCell(target);
  1325. if (index === -1) {
  1326. return;
  1327. }
  1328. let widget = (this.layout as PanelLayout).widgets[index];
  1329. widget.node.classList.add(DROP_TARGET_CLASS);
  1330. }
  1331. /**
  1332. * Handle the `'p-dragleave'` event for the widget.
  1333. */
  1334. private _evtDragLeave(event: IDragEvent): void {
  1335. if (!event.mimeData.hasData(JUPYTER_CELL_MIME)) {
  1336. return;
  1337. }
  1338. event.preventDefault();
  1339. event.stopPropagation();
  1340. let elements = this.node.getElementsByClassName(DROP_TARGET_CLASS);
  1341. if (elements.length) {
  1342. (elements[0] as HTMLElement).classList.remove(DROP_TARGET_CLASS);
  1343. }
  1344. }
  1345. /**
  1346. * Handle the `'p-dragover'` event for the widget.
  1347. */
  1348. private _evtDragOver(event: IDragEvent): void {
  1349. if (!event.mimeData.hasData(JUPYTER_CELL_MIME)) {
  1350. return;
  1351. }
  1352. event.preventDefault();
  1353. event.stopPropagation();
  1354. event.dropAction = event.proposedAction;
  1355. let elements = this.node.getElementsByClassName(DROP_TARGET_CLASS);
  1356. if (elements.length) {
  1357. (elements[0] as HTMLElement).classList.remove(DROP_TARGET_CLASS);
  1358. }
  1359. let target = event.target as HTMLElement;
  1360. let index = this._findCell(target);
  1361. if (index === -1) {
  1362. return;
  1363. }
  1364. let widget = (this.layout as PanelLayout).widgets[index];
  1365. widget.node.classList.add(DROP_TARGET_CLASS);
  1366. }
  1367. /**
  1368. * Handle the `'p-drop'` event for the widget.
  1369. */
  1370. private _evtDrop(event: IDragEvent): void {
  1371. if (!event.mimeData.hasData(JUPYTER_CELL_MIME)) {
  1372. return;
  1373. }
  1374. event.preventDefault();
  1375. event.stopPropagation();
  1376. if (event.proposedAction === 'none') {
  1377. event.dropAction = 'none';
  1378. return;
  1379. }
  1380. let target = event.target as HTMLElement;
  1381. while (target && target.parentElement) {
  1382. if (target.classList.contains(DROP_TARGET_CLASS)) {
  1383. target.classList.remove(DROP_TARGET_CLASS);
  1384. break;
  1385. }
  1386. target = target.parentElement;
  1387. }
  1388. let source: Notebook = event.source;
  1389. if (source === this) {
  1390. // Handle the case where we are moving cells within
  1391. // the same notebook.
  1392. event.dropAction = 'move';
  1393. let toMove: Cell[] = event.mimeData.getData('internal:cells');
  1394. // Compute the to/from indices for the move.
  1395. let fromIndex = ArrayExt.firstIndexOf(this.widgets, toMove[0]);
  1396. let toIndex = this._findCell(target);
  1397. // This check is needed for consistency with the view.
  1398. if (toIndex !== -1 && toIndex > fromIndex) {
  1399. toIndex -= 1;
  1400. } else if (toIndex === -1) {
  1401. // If the drop is within the notebook but not on any cell,
  1402. // most often this means it is past the cell areas, so
  1403. // set it to move the cells to the end of the notebook.
  1404. toIndex = this.widgets.length - 1;
  1405. }
  1406. // Don't move if we are within the block of selected cells.
  1407. if (toIndex >= fromIndex && toIndex < fromIndex + toMove.length) {
  1408. return;
  1409. }
  1410. // Move the cells one by one
  1411. this.model.cells.beginCompoundOperation();
  1412. if (fromIndex < toIndex) {
  1413. each(toMove, (cellWidget) => {
  1414. this.model.cells.move(fromIndex, toIndex);
  1415. });
  1416. } else if (fromIndex > toIndex) {
  1417. each(toMove, (cellWidget) => {
  1418. this.model.cells.move(fromIndex++, toIndex++);
  1419. });
  1420. }
  1421. this.model.cells.endCompoundOperation();
  1422. } else {
  1423. // Handle the case where we are copying cells between
  1424. // notebooks.
  1425. event.dropAction = 'copy';
  1426. // Find the target cell and insert the copied cells.
  1427. let index = this._findCell(target);
  1428. if (index === -1) {
  1429. index = this.widgets.length;
  1430. }
  1431. let start = index;
  1432. let model = this.model;
  1433. let values = event.mimeData.getData(JUPYTER_CELL_MIME);
  1434. let factory = model.contentFactory;
  1435. // Insert the copies of the original cells.
  1436. model.cells.beginCompoundOperation();
  1437. each(values, (cell: nbformat.ICell) => {
  1438. let value: ICellModel;
  1439. switch (cell.cell_type) {
  1440. case 'code':
  1441. value = factory.createCodeCell({ cell });
  1442. break;
  1443. case 'markdown':
  1444. value = factory.createMarkdownCell({ cell });
  1445. break;
  1446. default:
  1447. value = factory.createRawCell({ cell });
  1448. break;
  1449. }
  1450. model.cells.insert(index++, value);
  1451. });
  1452. model.cells.endCompoundOperation();
  1453. // Select the inserted cells.
  1454. this.deselectAll();
  1455. this.activeCellIndex = start;
  1456. this.extendContiguousSelectionTo(index - 1);
  1457. }
  1458. }
  1459. /**
  1460. * Start a drag event.
  1461. */
  1462. private _startDrag(index: number, clientX: number, clientY: number): void {
  1463. let cells = this.model.cells;
  1464. let selected: nbformat.ICell[] = [];
  1465. let toMove: Cell[] = [];
  1466. each(this.widgets, (widget, i) => {
  1467. let cell = cells.get(i);
  1468. if (this.isSelectedOrActive(widget)) {
  1469. widget.addClass(DROP_SOURCE_CLASS);
  1470. selected.push(cell.toJSON());
  1471. toMove.push(widget);
  1472. }
  1473. });
  1474. let activeCell = this.activeCell;
  1475. let dragImage: HTMLElement = null;
  1476. let countString: string;
  1477. if (activeCell.model.type === 'code') {
  1478. let executionCount = (activeCell.model as ICodeCellModel).executionCount;
  1479. countString = ' ';
  1480. if (executionCount) {
  1481. countString = executionCount.toString();
  1482. }
  1483. } else {
  1484. countString = '';
  1485. }
  1486. // Create the drag image.
  1487. dragImage = Private.createDragImage(selected.length, countString, activeCell.model.value.text.split('\n')[0].slice(0, 26));
  1488. // Set up the drag event.
  1489. this._drag = new Drag({
  1490. mimeData: new MimeData(),
  1491. dragImage,
  1492. supportedActions: 'copy-move',
  1493. proposedAction: 'copy',
  1494. source: this
  1495. });
  1496. this._drag.mimeData.setData(JUPYTER_CELL_MIME, selected);
  1497. // Add mimeData for the fully reified cell widgets, for the
  1498. // case where the target is in the same notebook and we
  1499. // can just move the cells.
  1500. this._drag.mimeData.setData('internal:cells', toMove);
  1501. // Remove mousemove and mouseup listeners and start the drag.
  1502. document.removeEventListener('mousemove', this, true);
  1503. document.removeEventListener('mouseup', this, true);
  1504. this._mouseMode = null;
  1505. this._drag.start(clientX, clientY).then(action => {
  1506. if (this.isDisposed) {
  1507. return;
  1508. }
  1509. this._drag = null;
  1510. each(toMove, widget => { widget.removeClass(DROP_SOURCE_CLASS); });
  1511. });
  1512. }
  1513. /**
  1514. * Handle `focus` events for the widget.
  1515. */
  1516. private _evtFocus(event: MouseEvent): void {
  1517. let target = event.target as HTMLElement;
  1518. let i = this._findCell(target);
  1519. if (i !== -1) {
  1520. let widget = this.widgets[i];
  1521. // If the editor itself does not have focus, ensure command mode.
  1522. if (!widget.editorWidget.node.contains(target)) {
  1523. this.mode = 'command';
  1524. }
  1525. this.activeCellIndex = i;
  1526. // If the editor has focus, ensure edit mode.
  1527. let node = widget.editorWidget.node;
  1528. if (node.contains(target)) {
  1529. this.mode = 'edit';
  1530. }
  1531. } else {
  1532. // No cell has focus, ensure command mode.
  1533. this.mode = 'command';
  1534. }
  1535. }
  1536. /**
  1537. * Handle `blur` events for the notebook.
  1538. */
  1539. private _evtBlur(event: MouseEvent): void {
  1540. let relatedTarget = event.relatedTarget as HTMLElement;
  1541. // Bail if focus is leaving the notebook.
  1542. if (!this.node.contains(relatedTarget)) {
  1543. return;
  1544. }
  1545. this.mode = 'command';
  1546. }
  1547. /**
  1548. * Handle `dblclick` events for the widget.
  1549. */
  1550. private _evtDblClick(event: MouseEvent): void {
  1551. let model = this.model;
  1552. if (!model) {
  1553. return;
  1554. }
  1555. let target = event.target as HTMLElement;
  1556. let i = this._findCell(target);
  1557. if (i === -1) {
  1558. return;
  1559. }
  1560. this.activeCellIndex = i;
  1561. if (model.cells.get(i).type === 'markdown') {
  1562. let widget = this.widgets[i] as MarkdownCell;
  1563. widget.rendered = false;
  1564. } else if (target.localName === 'img') {
  1565. target.classList.toggle(UNCONFINED_CLASS);
  1566. }
  1567. }
  1568. /**
  1569. * Remove selections from inactive cells to avoid
  1570. * spurious cursors.
  1571. */
  1572. private _trimSelections(): void {
  1573. for (let i = 0; i < this.widgets.length; i++) {
  1574. if (i !== this._activeCellIndex) {
  1575. let cell = this.widgets[i];
  1576. cell.model.selections.delete(cell.editor.uuid);
  1577. }
  1578. }
  1579. }
  1580. private _activeCellIndex = -1;
  1581. private _activeCell: Cell = null;
  1582. private _mode: NotebookMode = 'command';
  1583. private _drag: Drag = null;
  1584. private _dragData: { pressX: number, pressY: number, index: number } = null;
  1585. private _mouseMode: 'select' | 'couldDrag' | null = null;
  1586. private _activeCellChanged = new Signal<this, Cell>(this);
  1587. private _stateChanged = new Signal<this, IChangedArgs<any>>(this);
  1588. private _selectionChanged = new Signal<this, void>(this);
  1589. }
  1590. /**
  1591. * The namespace for the `Notebook` class statics.
  1592. */
  1593. export
  1594. namespace Notebook {
  1595. /**
  1596. * An options object for initializing a notebook widget.
  1597. */
  1598. export
  1599. interface IOptions extends StaticNotebook.IOptions { }
  1600. /**
  1601. * The content factory for the notebook widget.
  1602. */
  1603. export
  1604. interface IContentFactory extends StaticNotebook.IContentFactory { }
  1605. /**
  1606. * The default implementation of a notebook content factory..
  1607. *
  1608. * #### Notes
  1609. * Override methods on this class to customize the default notebook factory
  1610. * methods that create notebook content.
  1611. */
  1612. export
  1613. class ContentFactory extends StaticNotebook.ContentFactory { }
  1614. /**
  1615. * A namespace for the notebook content factory.
  1616. */
  1617. export
  1618. namespace ContentFactory {
  1619. /**
  1620. * An options object for initializing a notebook content factory.
  1621. */
  1622. export
  1623. interface IOptions extends StaticNotebook.ContentFactory.IOptions { }
  1624. }
  1625. export
  1626. const defaultContentFactory: IContentFactory = new ContentFactory();
  1627. }
  1628. /**
  1629. * A namespace for private data.
  1630. */
  1631. namespace Private {
  1632. /**
  1633. * An attached property for the selected state of a cell.
  1634. */
  1635. export
  1636. const selectedProperty = new AttachedProperty<Cell, boolean>({
  1637. name: 'selected',
  1638. create: () => false
  1639. });
  1640. /**
  1641. * A custom panel layout for the notebook.
  1642. */
  1643. export
  1644. class NotebookPanelLayout extends PanelLayout {
  1645. /**
  1646. * A message handler invoked on an `'update-request'` message.
  1647. *
  1648. * #### Notes
  1649. * This is a reimplementation of the base class method,
  1650. * and is a no-op.
  1651. */
  1652. protected onUpdateRequest(msg: Message): void {
  1653. // This is a no-op.
  1654. }
  1655. }
  1656. /**
  1657. * Create a cell drag image.
  1658. */
  1659. export
  1660. function createDragImage(count: number, promptNumber: string, cellContent: string): HTMLElement {
  1661. if (count > 1) {
  1662. if (promptNumber !== '') {
  1663. return VirtualDOM.realize(
  1664. h.div(
  1665. h.div({className: DRAG_IMAGE_CLASS},
  1666. h.span({className: CELL_DRAG_PROMPT_CLASS}, 'In [' + promptNumber + ']:'),
  1667. h.span({className: CELL_DRAG_CONTENT_CLASS}, cellContent)),
  1668. h.div({className: CELL_DRAG_MULTIPLE_BACK}, '')
  1669. )
  1670. );
  1671. } else {
  1672. return VirtualDOM.realize(
  1673. h.div(
  1674. h.div({className: DRAG_IMAGE_CLASS},
  1675. h.span({className: CELL_DRAG_PROMPT_CLASS}),
  1676. h.span({className: CELL_DRAG_CONTENT_CLASS}, cellContent)),
  1677. h.div({className: CELL_DRAG_MULTIPLE_BACK}, '')
  1678. )
  1679. );
  1680. }
  1681. } else {
  1682. if (promptNumber !== '') {
  1683. return VirtualDOM.realize(
  1684. h.div(
  1685. h.div({className: `${DRAG_IMAGE_CLASS} ${SINGLE_DRAG_IMAGE_CLASS}`},
  1686. h.span({className: CELL_DRAG_PROMPT_CLASS}, 'In [' + promptNumber + ']:'),
  1687. h.span({className: CELL_DRAG_CONTENT_CLASS}, cellContent)
  1688. )
  1689. )
  1690. );
  1691. } else {
  1692. return VirtualDOM.realize(
  1693. h.div(
  1694. h.div({className: `${DRAG_IMAGE_CLASS} ${SINGLE_DRAG_IMAGE_CLASS}`},
  1695. h.span({className: CELL_DRAG_PROMPT_CLASS}),
  1696. h.span({className: CELL_DRAG_CONTENT_CLASS}, cellContent)
  1697. )
  1698. )
  1699. );
  1700. }
  1701. }
  1702. }
  1703. /**
  1704. * Process the `IOptions` passed to the notebook widget.
  1705. *
  1706. * #### Notes
  1707. * This defaults the content factory to that in the `Notebook` namespace.
  1708. */
  1709. export
  1710. function processNotebookOptions(options: Notebook.IOptions) {
  1711. if (options.contentFactory) {
  1712. return options;
  1713. } else {
  1714. return {
  1715. rendermime: options.rendermime,
  1716. languagePreference: options.languagePreference,
  1717. contentFactory: Notebook.defaultContentFactory,
  1718. mimeTypeService: options.mimeTypeService
  1719. };
  1720. }
  1721. }
  1722. }