##// END OF EJS Templates
Always refresh the CM editor upon TextCell unrender.
Brian E. Granger -
Show More
@@ -1,565 +1,564
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2012 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // TextCell
10 10 //============================================================================
11 11
12 12
13 13
14 14 /**
15 15 A module that allow to create different type of Text Cell
16 16 @module IPython
17 17 @namespace IPython
18 18 */
19 19 var IPython = (function (IPython) {
20 20 "use strict";
21 21
22 22 // TextCell base class
23 23 var key = IPython.utils.keycodes;
24 24
25 25 /**
26 26 * Construct a new TextCell, codemirror mode is by default 'htmlmixed', and cell type is 'text'
27 27 * cell start as not redered.
28 28 *
29 29 * @class TextCell
30 30 * @constructor TextCell
31 31 * @extend IPython.Cell
32 32 * @param {object|undefined} [options]
33 33 * @param [options.cm_config] {object} config to pass to CodeMirror, will extend/overwrite default config
34 34 * @param [options.placeholder] {string} default string to use when souce in empty for rendering (only use in some TextCell subclass)
35 35 */
36 36 var TextCell = function (options) {
37 37 // in all TextCell/Cell subclasses
38 38 // do not assign most of members here, just pass it down
39 39 // in the options dict potentially overwriting what you wish.
40 40 // they will be assigned in the base class.
41 41
42 42 // we cannot put this as a class key as it has handle to "this".
43 43 var cm_overwrite_options = {
44 44 onKeyEvent: $.proxy(this.handle_keyevent,this)
45 45 };
46 46
47 47 options = this.mergeopt(TextCell,options,{cm_config:cm_overwrite_options});
48 48
49 49 this.cell_type = this.cell_type || 'text';
50 50
51 51 IPython.Cell.apply(this, [options]);
52 52
53 53 this.rendered = false;
54 54 };
55 55
56 56 TextCell.prototype = new IPython.Cell();
57 57
58 58 TextCell.options_default = {
59 59 cm_config : {
60 60 extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
61 61 mode: 'htmlmixed',
62 62 lineWrapping : true,
63 63 }
64 64 };
65 65
66 66
67 67 /**
68 68 * Create the DOM element of the TextCell
69 69 * @method create_element
70 70 * @private
71 71 */
72 72 TextCell.prototype.create_element = function () {
73 73 IPython.Cell.prototype.create_element.apply(this, arguments);
74 74
75 75 var cell = $("<div>").addClass('cell text_cell border-box-sizing');
76 76 cell.attr('tabindex','2');
77 77
78 78 var prompt = $('<div/>').addClass('prompt input_prompt');
79 79 cell.append(prompt);
80 80 var inner_cell = $('<div/>').addClass('inner_cell');
81 81 this.celltoolbar = new IPython.CellToolbar(this);
82 82 inner_cell.append(this.celltoolbar.element);
83 83 var input_area = $('<div/>').addClass('text_cell_input border-box-sizing');
84 84 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
85 85 // The tabindex=-1 makes this div focusable.
86 86 var render_area = $('<div/>').addClass('text_cell_render border-box-sizing').
87 87 addClass('rendered_html').attr('tabindex','-1');
88 88 inner_cell.append(input_area).append(render_area);
89 89 cell.append(inner_cell);
90 90 this.element = cell;
91 91 };
92 92
93 93
94 94 /**
95 95 * Bind the DOM evet to cell actions
96 96 * Need to be called after TextCell.create_element
97 97 * @private
98 98 * @method bind_event
99 99 */
100 100 TextCell.prototype.bind_events = function () {
101 101 IPython.Cell.prototype.bind_events.apply(this);
102 102 var that = this;
103 103
104 104 this.element.dblclick(function () {
105 105 if (that.selected === false) {
106 106 $([IPython.events]).trigger('select.Cell', {'cell':that});
107 107 }
108 108 $([IPython.events]).trigger('edit_mode.Cell', {cell: that});
109 109 });
110 110 };
111 111
112 112 TextCell.prototype.handle_keyevent = function (editor, event) {
113 113
114 114 // console.log('CM', this.mode, event.which, event.type)
115 115
116 116 if (this.mode === 'command') {
117 117 return true;
118 118 } else if (this.mode === 'edit') {
119 119 return this.handle_codemirror_keyevent(editor, event);
120 120 }
121 121 };
122 122
123 123 /**
124 124 * This method gets called in CodeMirror's onKeyDown/onKeyPress
125 125 * handlers and is used to provide custom key handling.
126 126 *
127 127 * Subclass should override this method to have custom handeling
128 128 *
129 129 * @method handle_codemirror_keyevent
130 130 * @param {CodeMirror} editor - The codemirror instance bound to the cell
131 131 * @param {event} event -
132 132 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
133 133 */
134 134 TextCell.prototype.handle_codemirror_keyevent = function (editor, event) {
135 135 var that = this;
136 136
137 137 if (event.keyCode === 13 && (event.shiftKey || event.ctrlKey || event.altKey)) {
138 138 // Always ignore shift-enter in CodeMirror as we handle it.
139 139 return true;
140 140 } else if (event.which === key.UPARROW && event.type === 'keydown') {
141 141 // If we are not at the top, let CM handle the up arrow and
142 142 // prevent the global keydown handler from handling it.
143 143 if (!that.at_top()) {
144 144 event.stop();
145 145 return false;
146 146 } else {
147 147 return true;
148 148 }
149 149 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
150 150 // If we are not at the bottom, let CM handle the down arrow and
151 151 // prevent the global keydown handler from handling it.
152 152 if (!that.at_bottom()) {
153 153 event.stop();
154 154 return false;
155 155 } else {
156 156 return true;
157 157 }
158 158 } else if (event.which === key.ESC && event.type === 'keydown') {
159 159 if (that.code_mirror.options.keyMap === "vim-insert") {
160 160 // vim keyMap is active and in insert mode. In this case we leave vim
161 161 // insert mode, but remain in notebook edit mode.
162 162 // Let' CM handle this event and prevent global handling.
163 163 event.stop();
164 164 return false;
165 165 } else {
166 166 // vim keyMap is not active. Leave notebook edit mode.
167 167 // Don't let CM handle the event, defer to global handling.
168 168 return true;
169 169 }
170 170 }
171 171 return false;
172 172 };
173 173
174 174 // Cell level actions
175 175
176 176 TextCell.prototype.select = function () {
177 177 var cont = IPython.Cell.prototype.select.apply(this);
178 178 if (cont) {
179 179 if (this.mode === 'edit') {
180 180 this.code_mirror.refresh();
181 181 }
182 182 }
183 183 return cont;
184 184 };
185 185
186 186 TextCell.prototype.unrender = function () {
187 187 if (this.read_only) return;
188 188 var cont = IPython.Cell.prototype.unrender.apply(this);
189 189 if (cont) {
190 190 var text_cell = this.element;
191 191 var output = text_cell.find("div.text_cell_render");
192 192 output.hide();
193 193 text_cell.find('div.text_cell_input').show();
194 194 if (this.get_text() === this.placeholder) {
195 195 this.set_text('');
196 this.refresh();
197 196 }
198
197 this.refresh();
199 198 }
200 199 return cont;
201 200 };
202 201
203 202 TextCell.prototype.execute = function () {
204 203 this.render();
205 204 };
206 205
207 206 TextCell.prototype.edit_mode = function (focus_editor) {
208 207 var cont = IPython.Cell.prototype.edit_mode.apply(this);
209 208 if (cont) {
210 209 cont = this.unrender();
211 210 // Focus the editor if codemirror was just added to the page or the
212 211 // caller explicitly wants to focus the editor (usally when the
213 212 // edit_mode was triggered by something other than a mouse click).
214 213 if (cont || focus_editor) {
215 214 this.focus_editor();
216 215 }
217 216 }
218 217 return cont;
219 218 };
220 219
221 220 /**
222 221 * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
223 222 * @method get_text
224 223 * @retrun {string} CodeMirror current text value
225 224 */
226 225 TextCell.prototype.get_text = function() {
227 226 return this.code_mirror.getValue();
228 227 };
229 228
230 229 /**
231 230 * @param {string} text - Codemiror text value
232 231 * @see TextCell#get_text
233 232 * @method set_text
234 233 * */
235 234 TextCell.prototype.set_text = function(text) {
236 235 this.code_mirror.setValue(text);
237 236 this.code_mirror.refresh();
238 237 };
239 238
240 239 /**
241 240 * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
242 241 * @method get_rendered
243 242 * @return {html} html of rendered element
244 243 * */
245 244 TextCell.prototype.get_rendered = function() {
246 245 return this.element.find('div.text_cell_render').html();
247 246 };
248 247
249 248 /**
250 249 * @method set_rendered
251 250 */
252 251 TextCell.prototype.set_rendered = function(text) {
253 252 this.element.find('div.text_cell_render').html(text);
254 253 };
255 254
256 255 /**
257 256 * @method at_top
258 257 * @return {Boolean}
259 258 */
260 259 TextCell.prototype.at_top = function () {
261 260 if (this.rendered) {
262 261 return true;
263 262 } else {
264 263 var cursor = this.code_mirror.getCursor();
265 264 if (cursor.line === 0 && cursor.ch === 0) {
266 265 return true;
267 266 } else {
268 267 return false;
269 268 }
270 269 }
271 270 };
272 271
273 272 /**
274 273 * @method at_bottom
275 274 * @return {Boolean}
276 275 * */
277 276 TextCell.prototype.at_bottom = function () {
278 277 if (this.rendered) {
279 278 return true;
280 279 } else {
281 280 var cursor = this.code_mirror.getCursor();
282 281 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
283 282 return true;
284 283 } else {
285 284 return false;
286 285 }
287 286 }
288 287 };
289 288
290 289 /**
291 290 * Create Text cell from JSON
292 291 * @param {json} data - JSON serialized text-cell
293 292 * @method fromJSON
294 293 */
295 294 TextCell.prototype.fromJSON = function (data) {
296 295 IPython.Cell.prototype.fromJSON.apply(this, arguments);
297 296 if (data.cell_type === this.cell_type) {
298 297 if (data.source !== undefined) {
299 298 this.set_text(data.source);
300 299 // make this value the starting point, so that we can only undo
301 300 // to this state, instead of a blank cell
302 301 this.code_mirror.clearHistory();
303 302 this.set_rendered(data.rendered || '');
304 303 this.rendered = false;
305 304 this.render();
306 305 }
307 306 }
308 307 };
309 308
310 309 /** Generate JSON from cell
311 310 * @return {object} cell data serialised to json
312 311 */
313 312 TextCell.prototype.toJSON = function () {
314 313 var data = IPython.Cell.prototype.toJSON.apply(this);
315 314 data.source = this.get_text();
316 315 if (data.source == this.placeholder) {
317 316 data.source = "";
318 317 }
319 318 return data;
320 319 };
321 320
322 321
323 322 /**
324 323 * @class MarkdownCell
325 324 * @constructor MarkdownCell
326 325 * @extends IPython.HTMLCell
327 326 */
328 327 var MarkdownCell = function (options) {
329 328 options = this.mergeopt(MarkdownCell, options);
330 329
331 330 this.cell_type = 'markdown';
332 331 TextCell.apply(this, [options]);
333 332 };
334 333
335 334 MarkdownCell.options_default = {
336 335 cm_config: {
337 336 mode: 'gfm'
338 337 },
339 338 placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
340 339 };
341 340
342 341 MarkdownCell.prototype = new TextCell();
343 342
344 343 /**
345 344 * @method render
346 345 */
347 346 MarkdownCell.prototype.render = function () {
348 347 var cont = IPython.TextCell.prototype.render.apply(this);
349 348 if (cont) {
350 349 var text = this.get_text();
351 350 var math = null;
352 351 if (text === "") { text = this.placeholder; }
353 352 var text_and_math = IPython.mathjaxutils.remove_math(text);
354 353 text = text_and_math[0];
355 354 math = text_and_math[1];
356 355 var html = marked.parser(marked.lexer(text));
357 356 html = $(IPython.mathjaxutils.replace_math(html, math));
358 357 // links in markdown cells should open in new tabs
359 358 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
360 359 try {
361 360 this.set_rendered(html);
362 361 } catch (e) {
363 362 console.log("Error running Javascript in Markdown:");
364 363 console.log(e);
365 364 this.set_rendered($("<div/>").addClass("js-error").html(
366 365 "Error rendering Markdown!<br/>" + e.toString())
367 366 );
368 367 }
369 368 this.element.find('div.text_cell_input').hide();
370 369 this.element.find("div.text_cell_render").show();
371 370 this.typeset();
372 371 }
373 372 return cont;
374 373 };
375 374
376 375
377 376 // RawCell
378 377
379 378 /**
380 379 * @class RawCell
381 380 * @constructor RawCell
382 381 * @extends IPython.TextCell
383 382 */
384 383 var RawCell = function (options) {
385 384
386 385 options = this.mergeopt(RawCell,options);
387 386 TextCell.apply(this, [options]);
388 387 this.cell_type = 'raw';
389 388 // RawCell should always hide its rendered div
390 389 this.element.find('div.text_cell_render').hide();
391 390 };
392 391
393 392 RawCell.options_default = {
394 393 placeholder : "Write raw LaTeX or other formats here, for use with nbconvert.\n" +
395 394 "It will not be rendered in the notebook.\n" +
396 395 "When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
397 396 };
398 397
399 398 RawCell.prototype = new TextCell();
400 399
401 400 /** @method bind_events **/
402 401 RawCell.prototype.bind_events = function () {
403 402 TextCell.prototype.bind_events.apply(this);
404 403 var that = this;
405 404 this.element.focusout(function() {
406 405 that.auto_highlight();
407 406 });
408 407 };
409 408
410 409 /**
411 410 * Trigger autodetection of highlight scheme for current cell
412 411 * @method auto_highlight
413 412 */
414 413 RawCell.prototype.auto_highlight = function () {
415 414 this._auto_highlight(IPython.config.raw_cell_highlight);
416 415 };
417 416
418 417 /** @method render **/
419 418 RawCell.prototype.render = function () {
420 419 // Make sure that this cell type can never be rendered
421 420 if (this.rendered) {
422 421 this.unrender();
423 422 }
424 423 var text = this.get_text();
425 424 if (text === "") { text = this.placeholder; }
426 425 this.set_text(text);
427 426 };
428 427
429 428
430 429 /**
431 430 * @class HeadingCell
432 431 * @extends IPython.TextCell
433 432 */
434 433
435 434 /**
436 435 * @constructor HeadingCell
437 436 * @extends IPython.TextCell
438 437 */
439 438 var HeadingCell = function (options) {
440 439 options = this.mergeopt(HeadingCell, options);
441 440
442 441 this.level = 1;
443 442 this.cell_type = 'heading';
444 443 TextCell.apply(this, [options]);
445 444
446 445 /**
447 446 * heading level of the cell, use getter and setter to access
448 447 * @property level
449 448 */
450 449 };
451 450
452 451 HeadingCell.options_default = {
453 452 placeholder: "Type Heading Here"
454 453 };
455 454
456 455 HeadingCell.prototype = new TextCell();
457 456
458 457 /** @method fromJSON */
459 458 HeadingCell.prototype.fromJSON = function (data) {
460 459 if (data.level !== undefined){
461 460 this.level = data.level;
462 461 }
463 462 TextCell.prototype.fromJSON.apply(this, arguments);
464 463 };
465 464
466 465
467 466 /** @method toJSON */
468 467 HeadingCell.prototype.toJSON = function () {
469 468 var data = TextCell.prototype.toJSON.apply(this);
470 469 data.level = this.get_level();
471 470 return data;
472 471 };
473 472
474 473 /**
475 474 * can the cell be split into two cells
476 475 * @method is_splittable
477 476 **/
478 477 HeadingCell.prototype.is_splittable = function () {
479 478 return false;
480 479 };
481 480
482 481
483 482 /**
484 483 * can the cell be merged with other cells
485 484 * @method is_mergeable
486 485 **/
487 486 HeadingCell.prototype.is_mergeable = function () {
488 487 return false;
489 488 };
490 489
491 490 /**
492 491 * Change heading level of cell, and re-render
493 492 * @method set_level
494 493 */
495 494 HeadingCell.prototype.set_level = function (level) {
496 495 this.level = level;
497 496 if (this.rendered) {
498 497 this.rendered = false;
499 498 this.render();
500 499 }
501 500 };
502 501
503 502 /** The depth of header cell, based on html (h1 to h6)
504 503 * @method get_level
505 504 * @return {integer} level - for 1 to 6
506 505 */
507 506 HeadingCell.prototype.get_level = function () {
508 507 return this.level;
509 508 };
510 509
511 510
512 511 HeadingCell.prototype.set_rendered = function (html) {
513 512 this.element.find("div.text_cell_render").html(html);
514 513 };
515 514
516 515
517 516 HeadingCell.prototype.get_rendered = function () {
518 517 var r = this.element.find("div.text_cell_render");
519 518 return r.children().first().html();
520 519 };
521 520
522 521
523 522 HeadingCell.prototype.render = function () {
524 523 var cont = IPython.TextCell.prototype.render.apply(this);
525 524 if (cont) {
526 525 var text = this.get_text();
527 526 var math = null;
528 527 // Markdown headings must be a single line
529 528 text = text.replace(/\n/g, ' ');
530 529 if (text === "") { text = this.placeholder; }
531 530 text = Array(this.level + 1).join("#") + " " + text;
532 531 var text_and_math = IPython.mathjaxutils.remove_math(text);
533 532 text = text_and_math[0];
534 533 math = text_and_math[1];
535 534 var html = marked.parser(marked.lexer(text));
536 535 var h = $(IPython.mathjaxutils.replace_math(html, math));
537 536 // add id and linkback anchor
538 537 var hash = h.text().replace(/ /g, '-');
539 538 h.attr('id', hash);
540 539 h.append(
541 540 $('<a/>')
542 541 .addClass('anchor-link')
543 542 .attr('href', '#' + hash)
544 543 .text('¶')
545 544 );
546 545
547 546 this.set_rendered(h);
548 547 this.typeset();
549 548 this.element.find('div.text_cell_input').hide();
550 549 this.element.find("div.text_cell_render").show();
551 550
552 551 }
553 552 return cont;
554 553 };
555 554
556 555 IPython.TextCell = TextCell;
557 556 IPython.MarkdownCell = MarkdownCell;
558 557 IPython.RawCell = RawCell;
559 558 IPython.HeadingCell = HeadingCell;
560 559
561 560
562 561 return IPython;
563 562
564 563 }(IPython));
565 564
General Comments 0
You need to be logged in to leave comments. Login now