widget.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. 'use strict';
  4. import {
  5. loadModeByMIME
  6. } from 'jupyter-js-ui/lib/codemirror';
  7. import {
  8. RenderMime
  9. } from 'jupyter-js-ui/lib/rendermime';
  10. import {
  11. Message
  12. } from 'phosphor-messaging';
  13. import {
  14. PanelLayout
  15. } from 'phosphor-panel';
  16. import {
  17. Widget
  18. } from 'phosphor-widget';
  19. import {
  20. MimeBundle
  21. } from '../notebook/nbformat';
  22. import {
  23. OutputAreaWidget, ObservableOutputs
  24. } from '../output-area';
  25. import {
  26. sanitize
  27. } from 'sanitizer';
  28. import {
  29. IMetadataCursor
  30. } from '../common/metadata';
  31. import {
  32. CellEditorWidget
  33. } from './editor';
  34. import {
  35. ICodeCellModel, ICellModel
  36. } from './model';
  37. /**
  38. * The class name added to cell widgets.
  39. */
  40. const CELL_CLASS = 'jp-Cell';
  41. /**
  42. * The class name added to input area widgets.
  43. */
  44. const INPUT_CLASS = 'jp-InputArea';
  45. /**
  46. * The class name added to the prompt area of cell.
  47. */
  48. const PROMPT_CLASS = 'jp-InputArea-prompt';
  49. /**
  50. * The class name added to the editor area of the cell.
  51. */
  52. const EDITOR_CLASS = 'jp-InputArea-editor';
  53. /**
  54. * The class name added to the cell when collapsed.
  55. */
  56. const COLLAPSED_CLASS = 'jp-mod-collapsed';
  57. /**
  58. * The class name added to the cell when readonly.
  59. */
  60. const READONLY_CLASS = 'jp-mod-readOnly';
  61. /**
  62. * The class name added to code cells.
  63. */
  64. const CODE_CELL_CLASS = 'jp-CodeCell';
  65. /**
  66. * The class name added to markdown cells.
  67. */
  68. const MARKDOWN_CELL_CLASS = 'jp-MarkdownCell';
  69. /**
  70. * The class name added to the markdown cell renderer widget.
  71. */
  72. const RENDERER_CLASS = 'jp-MarkdownCell-renderer';
  73. /**
  74. * The class name added to raw cells.
  75. */
  76. const RAW_CELL_CLASS = 'jp-RawCell';
  77. /**
  78. * The class name added to a rendered markdown cell.
  79. */
  80. const RENDERED_CLASS = 'jp-mod-rendered';
  81. /**
  82. * The text applied to an empty markdown cell.
  83. */
  84. const DEFAULT_MARKDOWN_TEXT = 'Type Markdown and LaTeX: $ α^2 $';
  85. /**
  86. * A base cell widget.
  87. */
  88. export
  89. class BaseCellWidget extends Widget {
  90. /**
  91. * Create a new cell editor widget.
  92. */
  93. static createCellEditor(model: ICellModel): CellEditorWidget {
  94. return new CellEditorWidget(model);
  95. }
  96. /**
  97. * Construct a new base cell widget.
  98. */
  99. constructor(model: ICellModel) {
  100. super();
  101. this.addClass(CELL_CLASS);
  102. this._model = model;
  103. let ctor = this.constructor as typeof BaseCellWidget;
  104. this._editor = ctor.createCellEditor(model);
  105. this._input = new InputAreaWidget(this._editor);
  106. this.layout = new PanelLayout();
  107. (this.layout as PanelLayout).addChild(this._input);
  108. model.contentChanged.connect(this.onModelChanged, this);
  109. this._trustedCursor = model.getMetadata('trusted');
  110. this._trusted = this._trustedCursor.getValue();
  111. }
  112. /**
  113. * Get the model used by the widget.
  114. *
  115. * #### Notes
  116. * This is a read-only property.
  117. */
  118. get model(): ICellModel {
  119. return this._model;
  120. }
  121. /**
  122. * Get the editor widget used by the cell.
  123. *
  124. * #### Notes
  125. * This is a ready-only property.
  126. */
  127. get editor(): CellEditorWidget {
  128. return this._editor;
  129. }
  130. /**
  131. * The mimetype used by the cell.
  132. */
  133. get mimetype(): string {
  134. return this._mimetype;
  135. }
  136. set mimetype(value: string) {
  137. if (this._mimetype === value) {
  138. return;
  139. }
  140. this._mimetype = value;
  141. loadModeByMIME(this.editor.editor, value);
  142. }
  143. /**
  144. * The read only state of the cell.
  145. */
  146. get readOnly(): boolean {
  147. return this._readOnly;
  148. }
  149. set readOnly(value: boolean) {
  150. if (value === this._readOnly) {
  151. return;
  152. }
  153. this._readOnly = value;
  154. this.update();
  155. }
  156. /**
  157. * The trusted state of the cell.
  158. */
  159. get trusted(): boolean {
  160. return this._trusted;
  161. }
  162. set trusted(value: boolean) {
  163. this._trustedCursor.setValue(value);
  164. }
  165. /**
  166. * Focus the widget.
  167. */
  168. focus(): void {
  169. this.editor.editor.focus();
  170. }
  171. /**
  172. * Set the prompt for the widget.
  173. */
  174. setPrompt(value: string): void {
  175. this._input.setPrompt(value);
  176. }
  177. /**
  178. * Toggle whether the input area is shown.
  179. */
  180. toggleInput(value: boolean): void {
  181. if (value) {
  182. this._input.show();
  183. this.focus();
  184. } else {
  185. this._input.hide();
  186. }
  187. }
  188. /**
  189. * Dispose of the resources held by the widget.
  190. */
  191. dispose() {
  192. // Do nothing if already disposed.
  193. if (this.isDisposed) {
  194. return;
  195. }
  196. this._model = null;
  197. this._input = null;
  198. this._editor = null;
  199. this._trustedCursor.dispose();
  200. this._trustedCursor = null;
  201. super.dispose();
  202. }
  203. /**
  204. * Handle `after-attach` messages.
  205. */
  206. protected onAfterAttach(msg: Message): void {
  207. this.update();
  208. }
  209. /**
  210. * Handle `update_request` messages.
  211. */
  212. protected onUpdateRequest(message: Message): void {
  213. // Handle read only state.
  214. let option = this._readOnly ? 'nocursor' : false;
  215. this.editor.editor.setOption('readOnly', option);
  216. this.toggleClass(READONLY_CLASS, this._readOnly);
  217. }
  218. /**
  219. * Handle changes in the model.
  220. */
  221. protected onModelChanged(model: ICellModel, change: string): void {
  222. switch (change) {
  223. case 'metadata':
  224. case 'metadata.trusted':
  225. this._trusted = this._trustedCursor.getValue();
  226. this.update();
  227. break;
  228. default:
  229. break;
  230. }
  231. }
  232. private _input: InputAreaWidget = null;
  233. private _editor: CellEditorWidget = null;
  234. private _model: ICellModel = null;
  235. private _mimetype = 'text/plain';
  236. private _readOnly = false;
  237. private _trustedCursor: IMetadataCursor = null;
  238. private _trusted = false;
  239. }
  240. /**
  241. * A widget for a code cell.
  242. */
  243. export
  244. class CodeCellWidget extends BaseCellWidget {
  245. /**
  246. * Create an output area widget.
  247. */
  248. static createOutput(outputs: ObservableOutputs, rendermime: RenderMime<Widget>): OutputAreaWidget {
  249. return new OutputAreaWidget(outputs, rendermime);
  250. }
  251. /**
  252. * Construct a code cell widget.
  253. */
  254. constructor(model: ICodeCellModel, rendermime: RenderMime<Widget>) {
  255. super(model);
  256. this._rendermime = rendermime;
  257. this.addClass(CODE_CELL_CLASS);
  258. let constructor = this.constructor as typeof CodeCellWidget;
  259. this._output = constructor.createOutput(model.outputs, rendermime);
  260. this._output.trusted = this.trusted;
  261. (this.layout as PanelLayout).addChild(this._output);
  262. this._collapsedCursor = model.getMetadata('collapsed');
  263. this._scrolledCursor = model.getMetadata('scrolled');
  264. this.setPrompt(String(model.executionCount));
  265. }
  266. /**
  267. * Dispose of the resources used by the widget.
  268. */
  269. dispose(): void {
  270. if (this.isDisposed) {
  271. return;
  272. }
  273. this._rendermime = null;
  274. this._collapsedCursor.dispose();
  275. this._collapsedCursor = null;
  276. this._scrolledCursor.dispose();
  277. this._scrolledCursor = null;
  278. this._output = null;
  279. super.dispose();
  280. }
  281. /**
  282. * Handle `update_request` messages.
  283. */
  284. protected onUpdateRequest(message: Message): void {
  285. this.toggleClass(COLLAPSED_CLASS, this._collapsedCursor.getValue());
  286. // TODO: handle scrolled state.
  287. this._output.trusted = this.trusted;
  288. super.onUpdateRequest(message);
  289. }
  290. /**
  291. * Handle changes in the model.
  292. */
  293. protected onModelChanged(model: ICodeCellModel, change: string): void {
  294. switch (change) {
  295. case 'metadata.collapsed':
  296. case 'metadata.scrolled':
  297. this.update();
  298. break;
  299. case 'executionCount':
  300. this.setPrompt(String(model.executionCount));
  301. break;
  302. default:
  303. break;
  304. }
  305. super.onModelChanged(model, change);
  306. }
  307. private _output: OutputAreaWidget = null;
  308. private _rendermime: RenderMime<Widget> = null;
  309. private _collapsedCursor: IMetadataCursor = null;
  310. private _scrolledCursor: IMetadataCursor = null;
  311. }
  312. /**
  313. * A widget for a Markdown cell.
  314. *
  315. * #### Notes
  316. * Things get complicated if we want the rendered text to update
  317. * any time the text changes, the text editor model changes,
  318. * or the input area model changes. We don't support automatically
  319. * updating the rendered text in all of these cases.
  320. */
  321. export
  322. class MarkdownCellWidget extends BaseCellWidget {
  323. /**
  324. * Construct a Markdown cell widget.
  325. */
  326. constructor(model: ICellModel, rendermime: RenderMime<Widget>) {
  327. super(model);
  328. this._rendermime = rendermime;
  329. this.addClass(MARKDOWN_CELL_CLASS);
  330. // Insist on the Github-flavored markdown mode.
  331. this.mimetype = 'text/x-ipythongfm';
  332. this._renderer = new Widget();
  333. this._renderer.addClass(RENDERER_CLASS);
  334. (this.layout as PanelLayout).addChild(this._renderer);
  335. }
  336. /**
  337. * Whether the cell is rendered.
  338. */
  339. get rendered(): boolean {
  340. return this._rendered;
  341. }
  342. set rendered(value: boolean) {
  343. if (value === this._rendered) {
  344. return;
  345. }
  346. this._rendered = value;
  347. this.update();
  348. }
  349. /**
  350. * Dispose of the resource held by the widget.
  351. */
  352. dispose(): void {
  353. if (this.isDisposed) {
  354. return;
  355. }
  356. this._renderer = null;
  357. this._rendermime = null;
  358. super.dispose();
  359. }
  360. /**
  361. * Handle `update_request` messages.
  362. */
  363. protected onUpdateRequest(message: Message): void {
  364. let model = this.model;
  365. if (this.rendered) {
  366. let text = model.source || DEFAULT_MARKDOWN_TEXT;
  367. // Do not re-render if the text has not changed.
  368. if (text !== this._prev) {
  369. text = sanitize(text);
  370. let bundle: MimeBundle = { 'text/markdown': text };
  371. this._renderer.dispose();
  372. this._renderer = this._rendermime.render(bundle) || new Widget();
  373. this._renderer.addClass(RENDERER_CLASS);
  374. (this.layout as PanelLayout).addChild(this._renderer);
  375. }
  376. this._prev = text;
  377. this._renderer.show();
  378. this.toggleInput(false);
  379. this.addClass(RENDERED_CLASS);
  380. } else {
  381. this._renderer.hide();
  382. this.toggleInput(true);
  383. this.removeClass(RENDERED_CLASS);
  384. }
  385. super.onUpdateRequest(message);
  386. }
  387. private _rendermime: RenderMime<Widget> = null;
  388. private _renderer: Widget = null;
  389. private _rendered = true;
  390. private _prev = '';
  391. }
  392. /**
  393. * A widget for a raw cell.
  394. */
  395. export
  396. class RawCellWidget extends BaseCellWidget {
  397. /**
  398. * Construct a raw cell widget.
  399. */
  400. constructor(model: ICellModel) {
  401. super(model);
  402. this.addClass(RAW_CELL_CLASS);
  403. }
  404. }
  405. /**
  406. * An input area widget, which hosts a prompt and an editor widget.
  407. */
  408. class InputAreaWidget extends Widget {
  409. /**
  410. * Construct an input area widget.
  411. */
  412. constructor(editor: CellEditorWidget) {
  413. super();
  414. this.addClass(INPUT_CLASS);
  415. editor.addClass(EDITOR_CLASS);
  416. this.layout = new PanelLayout();
  417. let prompt = new Widget();
  418. prompt.addClass(PROMPT_CLASS);
  419. let layout = this.layout as PanelLayout;
  420. layout.addChild(prompt);
  421. layout.addChild(editor);
  422. }
  423. /**
  424. * Set the prompt of the input area.
  425. */
  426. setPrompt(value: string): void {
  427. let prompt = (this.layout as PanelLayout).childAt(0);
  428. if (value === 'null') {
  429. value = ' ';
  430. }
  431. let text = `In [${value || ' '}]:`;
  432. prompt.node.textContent = text;
  433. }
  434. }