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