widget.ts 43 KB

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