##// END OF EJS Templates
enable keyboard manager when codemirror is focused...
Min RK -
Show More
@@ -1,645 +1,653 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3 /**
4 4 *
5 5 *
6 6 * @module codecell
7 7 * @namespace codecell
8 8 * @class CodeCell
9 9 */
10 10
11 11
12 12 define([
13 13 'base/js/namespace',
14 14 'jquery',
15 15 'base/js/utils',
16 16 'base/js/keyboard',
17 17 'services/config',
18 18 'notebook/js/cell',
19 19 'notebook/js/outputarea',
20 20 'notebook/js/completer',
21 21 'notebook/js/celltoolbar',
22 22 'codemirror/lib/codemirror',
23 23 'codemirror/mode/python/python',
24 24 'notebook/js/codemirror-ipython'
25 25 ], function(IPython,
26 26 $,
27 27 utils,
28 28 keyboard,
29 29 configmod,
30 30 cell,
31 31 outputarea,
32 32 completer,
33 33 celltoolbar,
34 34 CodeMirror,
35 35 cmpython,
36 36 cmip
37 37 ) {
38 38 "use strict";
39 39
40 40 var Cell = cell.Cell;
41 41
42 42 /* local util for codemirror */
43 43 var posEq = function(a, b) {return a.line === b.line && a.ch === b.ch;};
44 44
45 45 /**
46 46 *
47 47 * function to delete until previous non blanking space character
48 48 * or first multiple of 4 tabstop.
49 49 * @private
50 50 */
51 51 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
52 52 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
53 53 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
54 54 var cur = cm.getCursor(), line = cm.getLine(cur.line);
55 55 var tabsize = cm.getOption('tabSize');
56 56 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
57 57 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
58 58 var select = cm.getRange(from,cur);
59 59 if( select.match(/^\ +$/) !== null){
60 60 cm.replaceRange("",from,cur);
61 61 } else {
62 62 cm.deleteH(-1,"char");
63 63 }
64 64 };
65 65
66 66 var keycodes = keyboard.keycodes;
67 67
68 68 var CodeCell = function (kernel, options) {
69 69 /**
70 70 * Constructor
71 71 *
72 72 * A Cell conceived to write code.
73 73 *
74 74 * Parameters:
75 75 * kernel: Kernel instance
76 76 * The kernel doesn't have to be set at creation time, in that case
77 77 * it will be null and set_kernel has to be called later.
78 78 * options: dictionary
79 79 * Dictionary of keyword arguments.
80 80 * events: $(Events) instance
81 81 * config: dictionary
82 82 * keyboard_manager: KeyboardManager instance
83 83 * notebook: Notebook instance
84 84 * tooltip: Tooltip instance
85 85 */
86 86 this.kernel = kernel || null;
87 87 this.notebook = options.notebook;
88 88 this.collapsed = false;
89 89 this.events = options.events;
90 90 this.tooltip = options.tooltip;
91 91 this.config = options.config;
92 92 this.class_config = new configmod.ConfigWithDefaults(this.config,
93 93 CodeCell.config_defaults, 'CodeCell');
94 94
95 95 // create all attributed in constructor function
96 96 // even if null for V8 VM optimisation
97 97 this.input_prompt_number = null;
98 98 this.celltoolbar = null;
99 99 this.output_area = null;
100 100
101 101 this.last_msg_id = null;
102 102 this.completer = null;
103 103 this.widget_views = [];
104 104 this._widgets_live = true;
105 105
106 106 Cell.apply(this,[{
107 107 config: $.extend({}, CodeCell.options_default),
108 108 keyboard_manager: options.keyboard_manager,
109 109 events: this.events}]);
110 110
111 111 // Attributes we want to override in this subclass.
112 112 this.cell_type = "code";
113 113 var that = this;
114 114 this.element.focusout(
115 115 function() { that.auto_highlight(); }
116 116 );
117 117 };
118 118
119 119 CodeCell.options_default = {
120 120 cm_config : {
121 121 extraKeys: {
122 122 "Tab" : "indentMore",
123 123 "Shift-Tab" : "indentLess",
124 124 "Backspace" : "delSpaceToPrevTabStop",
125 125 "Cmd-/" : "toggleComment",
126 126 "Ctrl-/" : "toggleComment"
127 127 },
128 128 mode: 'ipython',
129 129 theme: 'ipython',
130 130 matchBrackets: true
131 131 }
132 132 };
133 133
134 134 CodeCell.config_defaults = {
135 135 highlight_modes : {
136 136 'magic_javascript' :{'reg':[/^%%javascript/]},
137 137 'magic_perl' :{'reg':[/^%%perl/]},
138 138 'magic_ruby' :{'reg':[/^%%ruby/]},
139 139 'magic_python' :{'reg':[/^%%python3?/]},
140 140 'magic_shell' :{'reg':[/^%%bash/]},
141 141 'magic_r' :{'reg':[/^%%R/]},
142 142 'magic_text/x-cython' :{'reg':[/^%%cython/]},
143 143 },
144 144 };
145 145
146 146 CodeCell.msg_cells = {};
147 147
148 148 CodeCell.prototype = Object.create(Cell.prototype);
149 149
150 150 /** @method create_element */
151 151 CodeCell.prototype.create_element = function () {
152 152 Cell.prototype.create_element.apply(this, arguments);
153 var that = this;
153 154
154 155 var cell = $('<div></div>').addClass('cell code_cell');
155 156 cell.attr('tabindex','2');
156 157
157 158 var input = $('<div></div>').addClass('input');
158 159 var prompt = $('<div/>').addClass('prompt input_prompt');
159 160 var inner_cell = $('<div/>').addClass('inner_cell');
160 161 this.celltoolbar = new celltoolbar.CellToolbar({
161 162 cell: this,
162 163 notebook: this.notebook});
163 164 inner_cell.append(this.celltoolbar.element);
164 165 var input_area = $('<div/>').addClass('input_area');
165 166 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
167 // In case of bugs that put the keyboard manager into an inconsistent state,
168 // ensure KM is enabled when CodeMirror is focused:
169 this.code_mirror.on('focus', function () {
170 if (that.keyboard_manager) {
171 that.keyboard_manager.enable();
172 }
173 });
166 174 this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this));
167 175 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
168 176 inner_cell.append(input_area);
169 177 input.append(prompt).append(inner_cell);
170 178
171 179 var widget_area = $('<div/>')
172 180 .addClass('widget-area')
173 181 .hide();
174 182 this.widget_area = widget_area;
175 183 var widget_prompt = $('<div/>')
176 184 .addClass('prompt')
177 185 .appendTo(widget_area);
178 186 var widget_subarea = $('<div/>')
179 187 .addClass('widget-subarea')
180 188 .appendTo(widget_area);
181 189 this.widget_subarea = widget_subarea;
182 190 var that = this;
183 191 var widget_clear_buton = $('<button />')
184 192 .addClass('close')
185 193 .html('&times;')
186 194 .click(function() {
187 195 widget_area.slideUp('', function(){
188 196 for (var i = 0; i < that.widget_views.length; i++) {
189 197 var view = that.widget_views[i];
190 198 view.remove();
191 199
192 200 // Remove widget live events.
193 201 view.off('comm:live', that._widget_live);
194 202 view.off('comm:dead', that._widget_dead);
195 203 }
196 204 that.widget_views = [];
197 205 widget_subarea.html('');
198 206 });
199 207 })
200 208 .appendTo(widget_prompt);
201 209
202 210 var output = $('<div></div>');
203 211 cell.append(input).append(widget_area).append(output);
204 212 this.element = cell;
205 213 this.output_area = new outputarea.OutputArea({
206 214 selector: output,
207 215 prompt_area: true,
208 216 events: this.events,
209 217 keyboard_manager: this.keyboard_manager});
210 218 this.completer = new completer.Completer(this, this.events);
211 219 };
212 220
213 221 /**
214 222 * Display a widget view in the cell.
215 223 */
216 224 CodeCell.prototype.display_widget_view = function(view_promise) {
217 225
218 226 // Display a dummy element
219 227 var dummy = $('<div/>');
220 228 this.widget_subarea.append(dummy);
221 229
222 230 // Display the view.
223 231 var that = this;
224 232 return view_promise.then(function(view) {
225 233 that.widget_area.show();
226 234 dummy.replaceWith(view.$el);
227 235 that.widget_views.push(view);
228 236
229 237 // Check the live state of the view's model.
230 238 if (view.model.comm_live) {
231 239 that._widget_live(view);
232 240 } else {
233 241 that._widget_dead(view);
234 242 }
235 243
236 244 // Listen to comm live events for the view.
237 245 view.on('comm:live', that._widget_live, that);
238 246 view.on('comm:dead', that._widget_dead, that);
239 247 return view;
240 248 });
241 249 };
242 250
243 251 /**
244 252 * Handles when a widget loses it's comm connection.
245 253 * @param {WidgetView} view
246 254 */
247 255 CodeCell.prototype._widget_dead = function(view) {
248 256 if (this._widgets_live) {
249 257 this._widgets_live = false;
250 258 this.widget_area.addClass('connection-problems');
251 259 }
252 260
253 261 };
254 262
255 263 /**
256 264 * Handles when a widget is connected to a live comm.
257 265 * @param {WidgetView} view
258 266 */
259 267 CodeCell.prototype._widget_live = function(view) {
260 268 if (!this._widgets_live) {
261 269 // Check that the other widgets are live too. O(N) operation.
262 270 // Abort the function at the first dead widget found.
263 271 for (var i = 0; i < this.widget_views.length; i++) {
264 272 if (!this.widget_views[i].model.comm_live) return;
265 273 }
266 274 this._widgets_live = true;
267 275 this.widget_area.removeClass('connection-problems');
268 276 }
269 277 };
270 278
271 279 /** @method bind_events */
272 280 CodeCell.prototype.bind_events = function () {
273 281 Cell.prototype.bind_events.apply(this);
274 282 var that = this;
275 283
276 284 this.element.focusout(
277 285 function() { that.auto_highlight(); }
278 286 );
279 287 };
280 288
281 289
282 290 /**
283 291 * This method gets called in CodeMirror's onKeyDown/onKeyPress
284 292 * handlers and is used to provide custom key handling. Its return
285 293 * value is used to determine if CodeMirror should ignore the event:
286 294 * true = ignore, false = don't ignore.
287 295 * @method handle_codemirror_keyevent
288 296 */
289 297
290 298 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
291 299
292 300 var that = this;
293 301 // whatever key is pressed, first, cancel the tooltip request before
294 302 // they are sent, and remove tooltip if any, except for tab again
295 303 var tooltip_closed = null;
296 304 if (event.type === 'keydown' && event.which !== keycodes.tab ) {
297 305 tooltip_closed = this.tooltip.remove_and_cancel_tooltip();
298 306 }
299 307
300 308 var cur = editor.getCursor();
301 309 if (event.keyCode === keycodes.enter){
302 310 this.auto_highlight();
303 311 }
304 312
305 313 if (event.which === keycodes.down && event.type === 'keypress' && this.tooltip.time_before_tooltip >= 0) {
306 314 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
307 315 // browser and keyboard layout !
308 316 // Pressing '(' , request tooltip, don't forget to reappend it
309 317 // The second argument says to hide the tooltip if the docstring
310 318 // is actually empty
311 319 this.tooltip.pending(that, true);
312 320 } else if ( tooltip_closed && event.which === keycodes.esc && event.type === 'keydown') {
313 321 // If tooltip is active, cancel it. The call to
314 322 // remove_and_cancel_tooltip above doesn't pass, force=true.
315 323 // Because of this it won't actually close the tooltip
316 324 // if it is in sticky mode. Thus, we have to check again if it is open
317 325 // and close it with force=true.
318 326 if (!this.tooltip._hidden) {
319 327 this.tooltip.remove_and_cancel_tooltip(true);
320 328 }
321 329 // If we closed the tooltip, don't let CM or the global handlers
322 330 // handle this event.
323 331 event.codemirrorIgnore = true;
324 332 event.preventDefault();
325 333 return true;
326 334 } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
327 335 if (editor.somethingSelected() || editor.getSelections().length !== 1){
328 336 var anchor = editor.getCursor("anchor");
329 337 var head = editor.getCursor("head");
330 338 if( anchor.line !== head.line){
331 339 return false;
332 340 }
333 341 }
334 342 this.tooltip.request(that);
335 343 event.codemirrorIgnore = true;
336 344 event.preventDefault();
337 345 return true;
338 346 } else if (event.keyCode === keycodes.tab && event.type === 'keydown') {
339 347 // Tab completion.
340 348 this.tooltip.remove_and_cancel_tooltip();
341 349
342 350 // completion does not work on multicursor, it might be possible though in some cases
343 351 if (editor.somethingSelected() || editor.getSelections().length > 1) {
344 352 return false;
345 353 }
346 354 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
347 355 if (pre_cursor.trim() === "") {
348 356 // Don't autocomplete if the part of the line before the cursor
349 357 // is empty. In this case, let CodeMirror handle indentation.
350 358 return false;
351 359 } else {
352 360 event.codemirrorIgnore = true;
353 361 event.preventDefault();
354 362 this.completer.startCompletion();
355 363 return true;
356 364 }
357 365 }
358 366
359 367 // keyboard event wasn't one of those unique to code cells, let's see
360 368 // if it's one of the generic ones (i.e. check edit mode shortcuts)
361 369 return Cell.prototype.handle_codemirror_keyevent.apply(this, [editor, event]);
362 370 };
363 371
364 372 // Kernel related calls.
365 373
366 374 CodeCell.prototype.set_kernel = function (kernel) {
367 375 this.kernel = kernel;
368 376 };
369 377
370 378 /**
371 379 * Execute current code cell to the kernel
372 380 * @method execute
373 381 */
374 382 CodeCell.prototype.execute = function (stop_on_error) {
375 383 if (!this.kernel || !this.kernel.is_connected()) {
376 384 console.log("Can't execute, kernel is not connected.");
377 385 return;
378 386 }
379 387
380 388 this.output_area.clear_output(false, true);
381 389
382 390 if (stop_on_error === undefined) {
383 391 stop_on_error = true;
384 392 }
385 393
386 394 // Clear widget area
387 395 for (var i = 0; i < this.widget_views.length; i++) {
388 396 var view = this.widget_views[i];
389 397 view.remove();
390 398
391 399 // Remove widget live events.
392 400 view.off('comm:live', this._widget_live);
393 401 view.off('comm:dead', this._widget_dead);
394 402 }
395 403 this.widget_views = [];
396 404 this.widget_subarea.html('');
397 405 this.widget_subarea.height('');
398 406 this.widget_area.height('');
399 407 this.widget_area.hide();
400 408
401 409 this.set_input_prompt('*');
402 410 this.element.addClass("running");
403 411 if (this.last_msg_id) {
404 412 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
405 413 }
406 414 var callbacks = this.get_callbacks();
407 415
408 416 var old_msg_id = this.last_msg_id;
409 417 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true,
410 418 stop_on_error : stop_on_error});
411 419 if (old_msg_id) {
412 420 delete CodeCell.msg_cells[old_msg_id];
413 421 }
414 422 CodeCell.msg_cells[this.last_msg_id] = this;
415 423 this.render();
416 424 this.events.trigger('execute.CodeCell', {cell: this});
417 425 };
418 426
419 427 /**
420 428 * Construct the default callbacks for
421 429 * @method get_callbacks
422 430 */
423 431 CodeCell.prototype.get_callbacks = function () {
424 432 var that = this;
425 433 return {
426 434 shell : {
427 435 reply : $.proxy(this._handle_execute_reply, this),
428 436 payload : {
429 437 set_next_input : $.proxy(this._handle_set_next_input, this),
430 438 page : $.proxy(this._open_with_pager, this)
431 439 }
432 440 },
433 441 iopub : {
434 442 output : function() {
435 443 that.output_area.handle_output.apply(that.output_area, arguments);
436 444 },
437 445 clear_output : function() {
438 446 that.output_area.handle_clear_output.apply(that.output_area, arguments);
439 447 },
440 448 },
441 449 input : $.proxy(this._handle_input_request, this)
442 450 };
443 451 };
444 452
445 453 CodeCell.prototype._open_with_pager = function (payload) {
446 454 this.events.trigger('open_with_text.Pager', payload);
447 455 };
448 456
449 457 /**
450 458 * @method _handle_execute_reply
451 459 * @private
452 460 */
453 461 CodeCell.prototype._handle_execute_reply = function (msg) {
454 462 this.set_input_prompt(msg.content.execution_count);
455 463 this.element.removeClass("running");
456 464 this.events.trigger('set_dirty.Notebook', {value: true});
457 465 };
458 466
459 467 /**
460 468 * @method _handle_set_next_input
461 469 * @private
462 470 */
463 471 CodeCell.prototype._handle_set_next_input = function (payload) {
464 472 var data = {'cell': this, 'text': payload.text, replace: payload.replace};
465 473 this.events.trigger('set_next_input.Notebook', data);
466 474 };
467 475
468 476 /**
469 477 * @method _handle_input_request
470 478 * @private
471 479 */
472 480 CodeCell.prototype._handle_input_request = function (msg) {
473 481 this.output_area.append_raw_input(msg);
474 482 };
475 483
476 484
477 485 // Basic cell manipulation.
478 486
479 487 CodeCell.prototype.select = function () {
480 488 var cont = Cell.prototype.select.apply(this);
481 489 if (cont) {
482 490 this.code_mirror.refresh();
483 491 this.auto_highlight();
484 492 }
485 493 return cont;
486 494 };
487 495
488 496 CodeCell.prototype.render = function () {
489 497 var cont = Cell.prototype.render.apply(this);
490 498 // Always execute, even if we are already in the rendered state
491 499 return cont;
492 500 };
493 501
494 502 CodeCell.prototype.select_all = function () {
495 503 var start = {line: 0, ch: 0};
496 504 var nlines = this.code_mirror.lineCount();
497 505 var last_line = this.code_mirror.getLine(nlines-1);
498 506 var end = {line: nlines-1, ch: last_line.length};
499 507 this.code_mirror.setSelection(start, end);
500 508 };
501 509
502 510
503 511 CodeCell.prototype.collapse_output = function () {
504 512 this.output_area.collapse();
505 513 };
506 514
507 515
508 516 CodeCell.prototype.expand_output = function () {
509 517 this.output_area.expand();
510 518 this.output_area.unscroll_area();
511 519 };
512 520
513 521 CodeCell.prototype.scroll_output = function () {
514 522 this.output_area.expand();
515 523 this.output_area.scroll_if_long();
516 524 };
517 525
518 526 CodeCell.prototype.toggle_output = function () {
519 527 this.output_area.toggle_output();
520 528 };
521 529
522 530 CodeCell.prototype.toggle_output_scroll = function () {
523 531 this.output_area.toggle_scroll();
524 532 };
525 533
526 534
527 535 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
528 536 var ns;
529 537 if (prompt_value === undefined || prompt_value === null) {
530 538 ns = "&nbsp;";
531 539 } else {
532 540 ns = encodeURIComponent(prompt_value);
533 541 }
534 542 return 'In&nbsp;[' + ns + ']:';
535 543 };
536 544
537 545 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
538 546 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
539 547 for(var i=1; i < lines_number; i++) {
540 548 html.push(['...:']);
541 549 }
542 550 return html.join('<br/>');
543 551 };
544 552
545 553 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
546 554
547 555
548 556 CodeCell.prototype.set_input_prompt = function (number) {
549 557 var nline = 1;
550 558 if (this.code_mirror !== undefined) {
551 559 nline = this.code_mirror.lineCount();
552 560 }
553 561 this.input_prompt_number = number;
554 562 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
555 563 // This HTML call is okay because the user contents are escaped.
556 564 this.element.find('div.input_prompt').html(prompt_html);
557 565 };
558 566
559 567
560 568 CodeCell.prototype.clear_input = function () {
561 569 this.code_mirror.setValue('');
562 570 };
563 571
564 572
565 573 CodeCell.prototype.get_text = function () {
566 574 return this.code_mirror.getValue();
567 575 };
568 576
569 577
570 578 CodeCell.prototype.set_text = function (code) {
571 579 return this.code_mirror.setValue(code);
572 580 };
573 581
574 582
575 583 CodeCell.prototype.clear_output = function (wait) {
576 584 this.output_area.clear_output(wait);
577 585 this.set_input_prompt();
578 586 };
579 587
580 588
581 589 // JSON serialization
582 590
583 591 CodeCell.prototype.fromJSON = function (data) {
584 592 Cell.prototype.fromJSON.apply(this, arguments);
585 593 if (data.cell_type === 'code') {
586 594 if (data.source !== undefined) {
587 595 this.set_text(data.source);
588 596 // make this value the starting point, so that we can only undo
589 597 // to this state, instead of a blank cell
590 598 this.code_mirror.clearHistory();
591 599 this.auto_highlight();
592 600 }
593 601 this.set_input_prompt(data.execution_count);
594 602 this.output_area.trusted = data.metadata.trusted || false;
595 603 this.output_area.fromJSON(data.outputs);
596 604 if (data.metadata.collapsed !== undefined) {
597 605 if (data.metadata.collapsed) {
598 606 this.collapse_output();
599 607 } else {
600 608 this.expand_output();
601 609 }
602 610 }
603 611 }
604 612 };
605 613
606 614
607 615 CodeCell.prototype.toJSON = function () {
608 616 var data = Cell.prototype.toJSON.apply(this);
609 617 data.source = this.get_text();
610 618 // is finite protect against undefined and '*' value
611 619 if (isFinite(this.input_prompt_number)) {
612 620 data.execution_count = this.input_prompt_number;
613 621 } else {
614 622 data.execution_count = null;
615 623 }
616 624 var outputs = this.output_area.toJSON();
617 625 data.outputs = outputs;
618 626 data.metadata.trusted = this.output_area.trusted;
619 627 data.metadata.collapsed = this.output_area.collapsed;
620 628 return data;
621 629 };
622 630
623 631 /**
624 632 * handle cell level logic when a cell is unselected
625 633 * @method unselect
626 634 * @return is the action being taken
627 635 */
628 636 CodeCell.prototype.unselect = function () {
629 637 var cont = Cell.prototype.unselect.apply(this);
630 638 if (cont) {
631 639 // When a code cell is usnelected, make sure that the corresponding
632 640 // tooltip and completer to that cell is closed.
633 641 this.tooltip.remove_and_cancel_tooltip(true);
634 642 if (this.completer !== null) {
635 643 this.completer.close();
636 644 }
637 645 }
638 646 return cont;
639 647 };
640 648
641 649 // Backwards compatability.
642 650 IPython.CodeCell = CodeCell;
643 651
644 652 return {'CodeCell': CodeCell};
645 653 });
@@ -1,367 +1,375 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'base/js/utils',
7 7 'jquery',
8 8 'notebook/js/cell',
9 9 'base/js/security',
10 10 'services/config',
11 11 'notebook/js/mathjaxutils',
12 12 'notebook/js/celltoolbar',
13 13 'components/marked/lib/marked',
14 14 'codemirror/lib/codemirror',
15 15 'codemirror/mode/gfm/gfm',
16 16 'notebook/js/codemirror-ipythongfm'
17 17 ], function(IPython,
18 18 utils,
19 19 $,
20 20 cell,
21 21 security,
22 22 configmod,
23 23 mathjaxutils,
24 24 celltoolbar,
25 25 marked,
26 26 CodeMirror,
27 27 gfm,
28 28 ipgfm
29 29 ) {
30 30 "use strict";
31 31 var Cell = cell.Cell;
32 32
33 33 var TextCell = function (options) {
34 34 /**
35 35 * Constructor
36 36 *
37 37 * Construct a new TextCell, codemirror mode is by default 'htmlmixed',
38 38 * and cell type is 'text' cell start as not redered.
39 39 *
40 40 * Parameters:
41 41 * options: dictionary
42 42 * Dictionary of keyword arguments.
43 43 * events: $(Events) instance
44 44 * config: dictionary
45 45 * keyboard_manager: KeyboardManager instance
46 46 * notebook: Notebook instance
47 47 */
48 48 options = options || {};
49 49
50 50 // in all TextCell/Cell subclasses
51 51 // do not assign most of members here, just pass it down
52 52 // in the options dict potentially overwriting what you wish.
53 53 // they will be assigned in the base class.
54 54 this.notebook = options.notebook;
55 55 this.events = options.events;
56 56 this.config = options.config;
57 57
58 58 // we cannot put this as a class key as it has handle to "this".
59 59 var config = utils.mergeopt(TextCell, this.config);
60 60 Cell.apply(this, [{
61 61 config: config,
62 62 keyboard_manager: options.keyboard_manager,
63 63 events: this.events}]);
64 64
65 65 this.cell_type = this.cell_type || 'text';
66 66 mathjaxutils = mathjaxutils;
67 67 this.rendered = false;
68 68 };
69 69
70 70 TextCell.prototype = Object.create(Cell.prototype);
71 71
72 72 TextCell.options_default = {
73 73 cm_config : {
74 74 extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
75 75 mode: 'htmlmixed',
76 76 lineWrapping : true,
77 77 }
78 78 };
79 79
80 80
81 81 /**
82 82 * Create the DOM element of the TextCell
83 83 * @method create_element
84 84 * @private
85 85 */
86 86 TextCell.prototype.create_element = function () {
87 87 Cell.prototype.create_element.apply(this, arguments);
88 var that = this;
88 89
89 90 var cell = $("<div>").addClass('cell text_cell');
90 91 cell.attr('tabindex','2');
91 92
92 93 var prompt = $('<div/>').addClass('prompt input_prompt');
93 94 cell.append(prompt);
94 95 var inner_cell = $('<div/>').addClass('inner_cell');
95 96 this.celltoolbar = new celltoolbar.CellToolbar({
96 97 cell: this,
97 98 notebook: this.notebook});
98 99 inner_cell.append(this.celltoolbar.element);
99 100 var input_area = $('<div/>').addClass('input_area');
100 101 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
102 // In case of bugs that put the keyboard manager into an inconsistent state,
103 // ensure KM is enabled when CodeMirror is focused:
104 this.code_mirror.on('focus', function () {
105 if (that.keyboard_manager) {
106 that.keyboard_manager.enable();
107 }
108 });
101 109 this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this))
102 110 // The tabindex=-1 makes this div focusable.
103 111 var render_area = $('<div/>').addClass('text_cell_render rendered_html')
104 112 .attr('tabindex','-1');
105 113 inner_cell.append(input_area).append(render_area);
106 114 cell.append(inner_cell);
107 115 this.element = cell;
108 116 };
109 117
110 118
111 119 // Cell level actions
112 120
113 121 TextCell.prototype.select = function () {
114 122 var cont = Cell.prototype.select.apply(this);
115 123 if (cont) {
116 124 if (this.mode === 'edit') {
117 125 this.code_mirror.refresh();
118 126 }
119 127 }
120 128 return cont;
121 129 };
122 130
123 131 TextCell.prototype.unrender = function () {
124 132 if (this.read_only) return;
125 133 var cont = Cell.prototype.unrender.apply(this);
126 134 if (cont) {
127 135 var text_cell = this.element;
128 136 if (this.get_text() === this.placeholder) {
129 137 this.set_text('');
130 138 }
131 139 this.refresh();
132 140 }
133 141 return cont;
134 142 };
135 143
136 144 TextCell.prototype.execute = function () {
137 145 this.render();
138 146 };
139 147
140 148 /**
141 149 * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
142 150 * @method get_text
143 151 * @retrun {string} CodeMirror current text value
144 152 */
145 153 TextCell.prototype.get_text = function() {
146 154 return this.code_mirror.getValue();
147 155 };
148 156
149 157 /**
150 158 * @param {string} text - Codemiror text value
151 159 * @see TextCell#get_text
152 160 * @method set_text
153 161 * */
154 162 TextCell.prototype.set_text = function(text) {
155 163 this.code_mirror.setValue(text);
156 164 this.unrender();
157 165 this.code_mirror.refresh();
158 166 };
159 167
160 168 /**
161 169 * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
162 170 * @method get_rendered
163 171 * */
164 172 TextCell.prototype.get_rendered = function() {
165 173 return this.element.find('div.text_cell_render').html();
166 174 };
167 175
168 176 /**
169 177 * @method set_rendered
170 178 */
171 179 TextCell.prototype.set_rendered = function(text) {
172 180 this.element.find('div.text_cell_render').html(text);
173 181 };
174 182
175 183
176 184 /**
177 185 * Create Text cell from JSON
178 186 * @param {json} data - JSON serialized text-cell
179 187 * @method fromJSON
180 188 */
181 189 TextCell.prototype.fromJSON = function (data) {
182 190 Cell.prototype.fromJSON.apply(this, arguments);
183 191 if (data.cell_type === this.cell_type) {
184 192 if (data.source !== undefined) {
185 193 this.set_text(data.source);
186 194 // make this value the starting point, so that we can only undo
187 195 // to this state, instead of a blank cell
188 196 this.code_mirror.clearHistory();
189 197 // TODO: This HTML needs to be treated as potentially dangerous
190 198 // user input and should be handled before set_rendered.
191 199 this.set_rendered(data.rendered || '');
192 200 this.rendered = false;
193 201 this.render();
194 202 }
195 203 }
196 204 };
197 205
198 206 /** Generate JSON from cell
199 207 * @return {object} cell data serialised to json
200 208 */
201 209 TextCell.prototype.toJSON = function () {
202 210 var data = Cell.prototype.toJSON.apply(this);
203 211 data.source = this.get_text();
204 212 if (data.source == this.placeholder) {
205 213 data.source = "";
206 214 }
207 215 return data;
208 216 };
209 217
210 218
211 219 var MarkdownCell = function (options) {
212 220 /**
213 221 * Constructor
214 222 *
215 223 * Parameters:
216 224 * options: dictionary
217 225 * Dictionary of keyword arguments.
218 226 * events: $(Events) instance
219 227 * config: ConfigSection instance
220 228 * keyboard_manager: KeyboardManager instance
221 229 * notebook: Notebook instance
222 230 */
223 231 options = options || {};
224 232 var config = utils.mergeopt(MarkdownCell, {});
225 233 TextCell.apply(this, [$.extend({}, options, {config: config})]);
226 234
227 235 this.class_config = new configmod.ConfigWithDefaults(options.config,
228 236 {}, 'MarkdownCell');
229 237 this.cell_type = 'markdown';
230 238 };
231 239
232 240 MarkdownCell.options_default = {
233 241 cm_config: {
234 242 mode: 'ipythongfm'
235 243 },
236 244 placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
237 245 };
238 246
239 247 MarkdownCell.prototype = Object.create(TextCell.prototype);
240 248
241 249 MarkdownCell.prototype.set_heading_level = function (level) {
242 250 /**
243 251 * make a markdown cell a heading
244 252 */
245 253 level = level || 1;
246 254 var source = this.get_text();
247 255 source = source.replace(/^(#*)\s?/,
248 256 new Array(level + 1).join('#') + ' ');
249 257 this.set_text(source);
250 258 this.refresh();
251 259 if (this.rendered) {
252 260 this.render();
253 261 }
254 262 };
255 263
256 264 /**
257 265 * @method render
258 266 */
259 267 MarkdownCell.prototype.render = function () {
260 268 var cont = TextCell.prototype.render.apply(this);
261 269 if (cont) {
262 270 var that = this;
263 271 var text = this.get_text();
264 272 var math = null;
265 273 if (text === "") { text = this.placeholder; }
266 274 var text_and_math = mathjaxutils.remove_math(text);
267 275 text = text_and_math[0];
268 276 math = text_and_math[1];
269 277 marked(text, function (err, html) {
270 278 html = mathjaxutils.replace_math(html, math);
271 279 html = security.sanitize_html(html);
272 280 html = $($.parseHTML(html));
273 281 // add anchors to headings
274 282 html.find(":header").addBack(":header").each(function (i, h) {
275 283 h = $(h);
276 284 var hash = h.text().replace(/ /g, '-');
277 285 h.attr('id', hash);
278 286 h.append(
279 287 $('<a/>')
280 288 .addClass('anchor-link')
281 289 .attr('href', '#' + hash)
282 290 .text('ΒΆ')
283 291 );
284 292 });
285 293 // links in markdown cells should open in new tabs
286 294 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
287 295 that.set_rendered(html);
288 296 that.typeset();
289 297 that.events.trigger("rendered.MarkdownCell", {cell: that});
290 298 });
291 299 }
292 300 return cont;
293 301 };
294 302
295 303
296 304 var RawCell = function (options) {
297 305 /**
298 306 * Constructor
299 307 *
300 308 * Parameters:
301 309 * options: dictionary
302 310 * Dictionary of keyword arguments.
303 311 * events: $(Events) instance
304 312 * config: ConfigSection instance
305 313 * keyboard_manager: KeyboardManager instance
306 314 * notebook: Notebook instance
307 315 */
308 316 options = options || {};
309 317 var config = utils.mergeopt(RawCell, {});
310 318 TextCell.apply(this, [$.extend({}, options, {config: config})]);
311 319
312 320 this.class_config = new configmod.ConfigWithDefaults(options.config,
313 321 RawCell.config_defaults, 'RawCell');
314 322 this.cell_type = 'raw';
315 323 };
316 324
317 325 RawCell.options_default = {
318 326 placeholder : "Write raw LaTeX or other formats here, for use with nbconvert. " +
319 327 "It will not be rendered in the notebook. " +
320 328 "When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
321 329 };
322 330
323 331 RawCell.config_defaults = {
324 332 highlight_modes : {
325 333 'diff' :{'reg':[/^diff/]}
326 334 },
327 335 };
328 336
329 337 RawCell.prototype = Object.create(TextCell.prototype);
330 338
331 339 /** @method bind_events **/
332 340 RawCell.prototype.bind_events = function () {
333 341 TextCell.prototype.bind_events.apply(this);
334 342 var that = this;
335 343 this.element.focusout(function() {
336 344 that.auto_highlight();
337 345 that.render();
338 346 });
339 347
340 348 this.code_mirror.on('focus', function() { that.unrender(); });
341 349 };
342 350
343 351 /** @method render **/
344 352 RawCell.prototype.render = function () {
345 353 var cont = TextCell.prototype.render.apply(this);
346 354 if (cont){
347 355 var text = this.get_text();
348 356 if (text === "") { text = this.placeholder; }
349 357 this.set_text(text);
350 358 this.element.removeClass('rendered');
351 359 this.auto_highlight();
352 360 }
353 361 return cont;
354 362 };
355 363
356 364 // Backwards compatability.
357 365 IPython.TextCell = TextCell;
358 366 IPython.MarkdownCell = MarkdownCell;
359 367 IPython.RawCell = RawCell;
360 368
361 369 var textcell = {
362 370 TextCell: TextCell,
363 371 MarkdownCell: MarkdownCell,
364 372 RawCell: RawCell
365 373 };
366 374 return textcell;
367 375 });
General Comments 0
You need to be logged in to leave comments. Login now