##// END OF EJS Templates
Don't always call focus_cell in Cell.command_mode....
Brian E. Granger -
Show More
@@ -1,533 +1,525
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 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 // CodeCell
10 10 //============================================================================
11 11 /**
12 12 * An extendable module that provide base functionnality to create cell for notebook.
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule CodeCell
16 16 */
17 17
18 18
19 19 /* local util for codemirror */
20 20 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;};
21 21
22 22 /**
23 23 *
24 24 * function to delete until previous non blanking space character
25 25 * or first multiple of 4 tabstop.
26 26 * @private
27 27 */
28 28 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
29 29 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
30 30 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
31 31 var cur = cm.getCursor(), line = cm.getLine(cur.line);
32 32 var tabsize = cm.getOption('tabSize');
33 33 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
34 34 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
35 35 var select = cm.getRange(from,cur);
36 36 if( select.match(/^\ +$/) !== null){
37 37 cm.replaceRange("",from,cur);
38 38 } else {
39 39 cm.deleteH(-1,"char");
40 40 }
41 41 };
42 42
43 43
44 44 var IPython = (function (IPython) {
45 45 "use strict";
46 46
47 47 var utils = IPython.utils;
48 48 var key = IPython.utils.keycodes;
49 49
50 50 /**
51 51 * A Cell conceived to write code.
52 52 *
53 53 * The kernel doesn't have to be set at creation time, in that case
54 54 * it will be null and set_kernel has to be called later.
55 55 * @class CodeCell
56 56 * @extends IPython.Cell
57 57 *
58 58 * @constructor
59 59 * @param {Object|null} kernel
60 60 * @param {object|undefined} [options]
61 61 * @param [options.cm_config] {object} config to pass to CodeMirror
62 62 */
63 63 var CodeCell = function (kernel, options) {
64 64 this.kernel = kernel || null;
65 65 this.collapsed = false;
66 66
67 67 // create all attributed in constructor function
68 68 // even if null for V8 VM optimisation
69 69 this.input_prompt_number = null;
70 70 this.celltoolbar = null;
71 71 this.output_area = null;
72 72 this.last_msg_id = null;
73 73 this.completer = null;
74 74
75 75
76 76 var cm_overwrite_options = {
77 77 onKeyEvent: $.proxy(this.handle_keyevent,this)
78 78 };
79 79
80 80 options = this.mergeopt(CodeCell, options, {cm_config:cm_overwrite_options});
81 81
82 82 IPython.Cell.apply(this,[options]);
83 83
84 84 // Attributes we want to override in this subclass.
85 85 this.cell_type = "code";
86 86
87 87 var that = this;
88 88 this.element.focusout(
89 89 function() { that.auto_highlight(); }
90 90 );
91 91 };
92 92
93 93 CodeCell.options_default = {
94 94 cm_config : {
95 95 extraKeys: {
96 96 "Tab" : "indentMore",
97 97 "Shift-Tab" : "indentLess",
98 98 "Backspace" : "delSpaceToPrevTabStop",
99 99 "Cmd-/" : "toggleComment",
100 100 "Ctrl-/" : "toggleComment"
101 101 },
102 102 mode: 'ipython',
103 103 theme: 'ipython',
104 104 matchBrackets: true
105 105 }
106 106 };
107 107
108 108
109 109 CodeCell.prototype = new IPython.Cell();
110 110
111 111 /**
112 112 * @method auto_highlight
113 113 */
114 114 CodeCell.prototype.auto_highlight = function () {
115 115 this._auto_highlight(IPython.config.cell_magic_highlight);
116 116 };
117 117
118 118 /** @method create_element */
119 119 CodeCell.prototype.create_element = function () {
120 120 IPython.Cell.prototype.create_element.apply(this, arguments);
121 121
122 122 var cell = $('<div></div>').addClass('cell border-box-sizing code_cell');
123 123 cell.attr('tabindex','2');
124 124
125 125 var input = $('<div></div>').addClass('input');
126 126 var prompt = $('<div/>').addClass('prompt input_prompt');
127 127 var inner_cell = $('<div/>').addClass('inner_cell');
128 128 this.celltoolbar = new IPython.CellToolbar(this);
129 129 inner_cell.append(this.celltoolbar.element);
130 130 var input_area = $('<div/>').addClass('input_area');
131 131 this.code_mirror = CodeMirror(input_area.get(0), this.cm_config);
132 132 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
133 133 inner_cell.append(input_area);
134 134 input.append(prompt).append(inner_cell);
135 135 var output = $('<div></div>');
136 136 cell.append(input).append(output);
137 137 this.element = cell;
138 138 this.output_area = new IPython.OutputArea(output, true);
139 139 this.completer = new IPython.Completer(this);
140 140 };
141 141
142 142 /** @method bind_events */
143 143 CodeCell.prototype.bind_events = function () {
144 144 IPython.Cell.prototype.bind_events.apply(this);
145 145 var that = this;
146 146
147 147 this.element.focusout(
148 148 function() { that.auto_highlight(); }
149 149 );
150 150 };
151 151
152 152 CodeCell.prototype.handle_keyevent = function (editor, event) {
153 153
154 154 console.log('CM', this.mode, event.which, event.type)
155 155
156 156 if (this.mode === 'command') {
157 157 return true;
158 158 } else if (this.mode === 'edit') {
159 159 return this.handle_codemirror_keyevent(editor, event);
160 160 }
161 161 };
162 162
163 163 /**
164 164 * This method gets called in CodeMirror's onKeyDown/onKeyPress
165 165 * handlers and is used to provide custom key handling. Its return
166 166 * value is used to determine if CodeMirror should ignore the event:
167 167 * true = ignore, false = don't ignore.
168 168 * @method handle_codemirror_keyevent
169 169 */
170 170 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
171 171
172 172 var that = this;
173 173 // whatever key is pressed, first, cancel the tooltip request before
174 174 // they are sent, and remove tooltip if any, except for tab again
175 175 var tooltip_closed = null;
176 176 if (event.type === 'keydown' && event.which != key.TAB ) {
177 177 tooltip_closed = IPython.tooltip.remove_and_cancel_tooltip();
178 178 }
179 179
180 180 var cur = editor.getCursor();
181 181 if (event.keyCode === key.ENTER){
182 182 this.auto_highlight();
183 183 }
184 184
185 185 if (event.keyCode === key.ENTER && (event.shiftKey || event.ctrlKey)) {
186 186 // Always ignore shift-enter in CodeMirror as we handle it.
187 187 return true;
188 188 } else if (event.which === 40 && event.type === 'keypress' && IPython.tooltip.time_before_tooltip >= 0) {
189 189 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
190 190 // browser and keyboard layout !
191 191 // Pressing '(' , request tooltip, don't forget to reappend it
192 192 // The second argument says to hide the tooltip if the docstring
193 193 // is actually empty
194 194 IPython.tooltip.pending(that, true);
195 195 } else if (event.which === key.UPARROW && event.type === 'keydown') {
196 196 // If we are not at the top, let CM handle the up arrow and
197 197 // prevent the global keydown handler from handling it.
198 198 if (!that.at_top()) {
199 199 event.stop();
200 200 return false;
201 201 } else {
202 202 return true;
203 203 }
204 204 } else if (event.which === key.ESC && event.type === 'keydown') {
205 205 // First see if the tooltip is active and if so cancel it.
206 206 if (tooltip_closed) {
207 207 // The call to remove_and_cancel_tooltip above in L177 doesn't pass
208 208 // force=true. Because of this it won't actually close the tooltip
209 209 // if it is in sticky mode. Thus, we have to check again if it is open
210 210 // and close it with force=true.
211 211 if (!IPython.tooltip._hidden) {
212 212 IPython.tooltip.remove_and_cancel_tooltip(true);
213 213 }
214 214 // If we closed the tooltip, don't let CM or the global handlers
215 215 // handle this event.
216 216 event.stop();
217 217 return true;
218 218 }
219 219 if (that.code_mirror.options.keyMap === "vim-insert") {
220 220 // vim keyMap is active and in insert mode. In this case we leave vim
221 221 // insert mode, but remain in notebook edit mode.
222 222 // Let' CM handle this event and prevent global handling.
223 223 event.stop();
224 224 return false;
225 225 } else {
226 226 // vim keyMap is not active. Leave notebook edit mode.
227 227 // Don't let CM handle the event, defer to global handling.
228 228 return true;
229 229 }
230 230 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
231 231 // If we are not at the bottom, let CM handle the down arrow and
232 232 // prevent the global keydown handler from handling it.
233 233 if (!that.at_bottom()) {
234 234 event.stop();
235 235 return false;
236 236 } else {
237 237 return true;
238 238 }
239 239 } else if (event.keyCode === key.TAB && event.type === 'keydown' && event.shiftKey) {
240 240 if (editor.somethingSelected()){
241 241 var anchor = editor.getCursor("anchor");
242 242 var head = editor.getCursor("head");
243 243 if( anchor.line != head.line){
244 244 return false;
245 245 }
246 246 }
247 247 IPython.tooltip.request(that);
248 248 event.stop();
249 249 return true;
250 250 } else if (event.keyCode === key.TAB && event.type == 'keydown') {
251 251 // Tab completion.
252 252 IPython.tooltip.remove_and_cancel_tooltip();
253 253 if (editor.somethingSelected()) {
254 254 return false;
255 255 }
256 256 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
257 257 if (pre_cursor.trim() === "") {
258 258 // Don't autocomplete if the part of the line before the cursor
259 259 // is empty. In this case, let CodeMirror handle indentation.
260 260 return false;
261 261 } else {
262 262 event.stop();
263 263 this.completer.startCompletion();
264 264 return true;
265 265 }
266 266 } else {
267 267 // keypress/keyup also trigger on TAB press, and we don't want to
268 268 // use those to disable tab completion.
269 269 return false;
270 270 }
271 271 return false;
272 272 };
273 273
274 274 // Kernel related calls.
275 275
276 276 CodeCell.prototype.set_kernel = function (kernel) {
277 277 this.kernel = kernel;
278 278 };
279 279
280 280 /**
281 281 * Execute current code cell to the kernel
282 282 * @method execute
283 283 */
284 284 CodeCell.prototype.execute = function () {
285 285 this.output_area.clear_output();
286 286 this.set_input_prompt('*');
287 287 this.element.addClass("running");
288 288 if (this.last_msg_id) {
289 289 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
290 290 }
291 291 var callbacks = this.get_callbacks();
292 292
293 293 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true});
294 294 };
295 295
296 296 /**
297 297 * Construct the default callbacks for
298 298 * @method get_callbacks
299 299 */
300 300 CodeCell.prototype.get_callbacks = function () {
301 301 return {
302 302 shell : {
303 303 reply : $.proxy(this._handle_execute_reply, this),
304 304 payload : {
305 305 set_next_input : $.proxy(this._handle_set_next_input, this),
306 306 page : $.proxy(this._open_with_pager, this)
307 307 }
308 308 },
309 309 iopub : {
310 310 output : $.proxy(this.output_area.handle_output, this.output_area),
311 311 clear_output : $.proxy(this.output_area.handle_clear_output, this.output_area),
312 312 },
313 313 input : $.proxy(this._handle_input_request, this)
314 314 };
315 315 };
316 316
317 317 CodeCell.prototype._open_with_pager = function (payload) {
318 318 $([IPython.events]).trigger('open_with_text.Pager', payload);
319 319 };
320 320
321 321 /**
322 322 * @method _handle_execute_reply
323 323 * @private
324 324 */
325 325 CodeCell.prototype._handle_execute_reply = function (msg) {
326 326 this.set_input_prompt(msg.content.execution_count);
327 327 this.element.removeClass("running");
328 328 $([IPython.events]).trigger('set_dirty.Notebook', {value: true});
329 329 };
330 330
331 331 /**
332 332 * @method _handle_set_next_input
333 333 * @private
334 334 */
335 335 CodeCell.prototype._handle_set_next_input = function (payload) {
336 336 var data = {'cell': this, 'text': payload.text};
337 337 $([IPython.events]).trigger('set_next_input.Notebook', data);
338 338 };
339 339
340 340 /**
341 341 * @method _handle_input_request
342 342 * @private
343 343 */
344 344 CodeCell.prototype._handle_input_request = function (msg) {
345 345 this.output_area.append_raw_input(msg);
346 346 };
347 347
348 348
349 349 // Basic cell manipulation.
350 350
351 351 CodeCell.prototype.select = function () {
352 352 var cont = IPython.Cell.prototype.select.apply(this);
353 353 if (cont) {
354 354 this.code_mirror.refresh();
355 355 this.auto_highlight();
356 356 };
357 357 return cont;
358 358 };
359 359
360 360 CodeCell.prototype.render = function () {
361 361 var cont = IPython.Cell.prototype.render.apply(this);
362 362 // Always execute, even if we are already in the rendered state
363 363 return cont;
364 364 };
365 365
366 366 CodeCell.prototype.unrender = function () {
367 367 // CodeCell is always rendered
368 368 return false;
369 369 };
370 370
371 CodeCell.prototype.command_mode = function () {
372 var cont = IPython.Cell.prototype.command_mode.apply(this);
373 if (cont) {
374 this.focus_cell();
375 };
376 return cont;
377 }
378
379 371 CodeCell.prototype.edit_mode = function () {
380 372 var cont = IPython.Cell.prototype.edit_mode.apply(this);
381 373 if (cont) {
382 374 this.focus_editor();
383 375 };
384 376 return cont;
385 377 }
386 378
387 379 CodeCell.prototype.select_all = function () {
388 380 var start = {line: 0, ch: 0};
389 381 var nlines = this.code_mirror.lineCount();
390 382 var last_line = this.code_mirror.getLine(nlines-1);
391 383 var end = {line: nlines-1, ch: last_line.length};
392 384 this.code_mirror.setSelection(start, end);
393 385 };
394 386
395 387
396 388 CodeCell.prototype.collapse = function () {
397 389 this.collapsed = true;
398 390 this.output_area.collapse();
399 391 };
400 392
401 393
402 394 CodeCell.prototype.expand = function () {
403 395 this.collapsed = false;
404 396 this.output_area.expand();
405 397 };
406 398
407 399
408 400 CodeCell.prototype.toggle_output = function () {
409 401 this.collapsed = Boolean(1 - this.collapsed);
410 402 this.output_area.toggle_output();
411 403 };
412 404
413 405
414 406 CodeCell.prototype.toggle_output_scroll = function () {
415 407 this.output_area.toggle_scroll();
416 408 };
417 409
418 410
419 411 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
420 412 var ns = prompt_value || "&nbsp;";
421 413 return 'In&nbsp;[' + ns + ']:';
422 414 };
423 415
424 416 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
425 417 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
426 418 for(var i=1; i < lines_number; i++) {
427 419 html.push(['...:']);
428 420 }
429 421 return html.join('<br/>');
430 422 };
431 423
432 424 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
433 425
434 426
435 427 CodeCell.prototype.set_input_prompt = function (number) {
436 428 var nline = 1;
437 429 if (this.code_mirror !== undefined) {
438 430 nline = this.code_mirror.lineCount();
439 431 }
440 432 this.input_prompt_number = number;
441 433 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
442 434 this.element.find('div.input_prompt').html(prompt_html);
443 435 };
444 436
445 437
446 438 CodeCell.prototype.clear_input = function () {
447 439 this.code_mirror.setValue('');
448 440 };
449 441
450 442
451 443 CodeCell.prototype.get_text = function () {
452 444 return this.code_mirror.getValue();
453 445 };
454 446
455 447
456 448 CodeCell.prototype.set_text = function (code) {
457 449 return this.code_mirror.setValue(code);
458 450 };
459 451
460 452
461 453 CodeCell.prototype.at_top = function () {
462 454 var cursor = this.code_mirror.getCursor();
463 455 if (cursor.line === 0 && cursor.ch === 0) {
464 456 return true;
465 457 } else {
466 458 return false;
467 459 }
468 460 };
469 461
470 462
471 463 CodeCell.prototype.at_bottom = function () {
472 464 var cursor = this.code_mirror.getCursor();
473 465 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
474 466 return true;
475 467 } else {
476 468 return false;
477 469 }
478 470 };
479 471
480 472
481 473 CodeCell.prototype.clear_output = function (wait) {
482 474 this.output_area.clear_output(wait);
483 475 };
484 476
485 477
486 478 // JSON serialization
487 479
488 480 CodeCell.prototype.fromJSON = function (data) {
489 481 IPython.Cell.prototype.fromJSON.apply(this, arguments);
490 482 if (data.cell_type === 'code') {
491 483 if (data.input !== undefined) {
492 484 this.set_text(data.input);
493 485 // make this value the starting point, so that we can only undo
494 486 // to this state, instead of a blank cell
495 487 this.code_mirror.clearHistory();
496 488 this.auto_highlight();
497 489 }
498 490 if (data.prompt_number !== undefined) {
499 491 this.set_input_prompt(data.prompt_number);
500 492 } else {
501 493 this.set_input_prompt();
502 494 }
503 495 this.output_area.fromJSON(data.outputs);
504 496 if (data.collapsed !== undefined) {
505 497 if (data.collapsed) {
506 498 this.collapse();
507 499 } else {
508 500 this.expand();
509 501 }
510 502 }
511 503 }
512 504 };
513 505
514 506
515 507 CodeCell.prototype.toJSON = function () {
516 508 var data = IPython.Cell.prototype.toJSON.apply(this);
517 509 data.input = this.get_text();
518 510 // is finite protect against undefined and '*' value
519 511 if (isFinite(this.input_prompt_number)) {
520 512 data.prompt_number = this.input_prompt_number;
521 513 }
522 514 var outputs = this.output_area.toJSON();
523 515 data.outputs = outputs;
524 516 data.language = 'python';
525 517 data.collapsed = this.collapsed;
526 518 return data;
527 519 };
528 520
529 521
530 522 IPython.CodeCell = CodeCell;
531 523
532 524 return IPython;
533 525 }(IPython));
@@ -1,619 +1,621
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2011 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 // Keyboard management
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 // Setup global keycodes and inverse keycodes.
16 16
17 17 // See http://unixpapa.com/js/key.html for a complete description. The short of
18 18 // it is that there are different keycode sets. Firefox uses the "Mozilla keycodes"
19 19 // and Webkit/IE use the "IE keycodes". These keycode sets are mostly the same
20 20 // but have minor differences.
21 21
22 22 // These apply to Firefox, (Webkit and IE)
23 23 var _keycodes = {
24 24 'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73,
25 25 'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81, 'r': 82,
26 26 's': 83, 't': 84, 'u': 85, 'v': 86, 'w': 87, 'x': 88, 'y': 89, 'z': 90,
27 27 '1 !': 49, '2 @': 50, '3 #': 51, '4 $': 52, '5 %': 53, '6 ^': 54,
28 28 '7 &': 55, '8 *': 56, '9 (': 57, '0 )': 48,
29 29 '[ {': 219, '] }': 221, '` ~': 192, ', <': 188, '. >': 190, '/ ?': 191,
30 30 '\\ |': 220, '\' "': 222,
31 31 'numpad0': 96, 'numpad1': 97, 'numpad2': 98, 'numpad3': 99, 'numpad4': 100,
32 32 'numpad5': 101, 'numpad6': 102, 'numpad7': 103, 'numpad8': 104, 'numpad9': 105,
33 33 'multiply': 106, 'add': 107, 'subtract': 109, 'decimal': 110, 'divide': 111,
34 34 'f1': 112, 'f2': 113, 'f3': 114, 'f4': 115, 'f5': 116, 'f6': 117, 'f7': 118,
35 35 'f8': 119, 'f9': 120, 'f11': 122, 'f12': 123, 'f13': 124, 'f14': 125, 'f15': 126,
36 36 'backspace': 8, 'tab': 9, 'enter': 13, 'shift': 16, 'ctrl': 17, 'alt': 18,
37 37 'meta': 91, 'capslock': 20, 'esc': 27, 'space': 32, 'pageup': 33, 'pagedown': 34,
38 38 'end': 35, 'home': 36, 'left': 37, 'up': 38, 'right': 39, 'down': 40,
39 39 'insert': 45, 'delete': 46, 'numlock': 144,
40 40 };
41 41
42 42 // These apply to Firefox and Opera
43 43 var _mozilla_keycodes = {
44 44 '; :': 59, '= +': 61, '- _': 109,
45 45 }
46 46
47 47 // This apply to Webkit and IE
48 48 var _ie_keycodes = {
49 49 '; :': 186, '= +': 187, '- _': 189,
50 50 }
51 51
52 52 var browser = IPython.utils.browser[0];
53 53
54 54 if (browser === 'Firefox' || browser === 'Opera') {
55 55 $.extend(_keycodes, _mozilla_keycodes);
56 56 } else if (browser === 'Safari' || browser === 'Chrome' || browser === 'MSIE') {
57 57 $.extend(_keycodes, _ie_keycodes);
58 58 }
59 59
60 60 var keycodes = {};
61 61 var inv_keycodes = {};
62 62 for (var name in _keycodes) {
63 63 var names = name.split(' ');
64 64 if (names.length === 1) {
65 65 var n = names[0]
66 66 keycodes[n] = _keycodes[n]
67 67 inv_keycodes[_keycodes[n]] = n
68 68 } else {
69 69 var primary = names[0];
70 70 var secondary = names[1];
71 71 keycodes[primary] = _keycodes[name]
72 72 keycodes[secondary] = _keycodes[name]
73 73 inv_keycodes[_keycodes[name]] = primary
74 74 }
75 75 }
76 76
77 77
78 78 // Default keyboard shortcuts
79 79
80 80 var default_common_shortcuts = {
81 81 'meta+s' : {
82 82 help : 'save notebook',
83 83 handler : function (event) {
84 84 IPython.notebook.save_checkpoint();
85 85 event.preventDefault();
86 86 return false;
87 87 }
88 88 },
89 89 'ctrl+s' : {
90 90 help : 'save notebook',
91 91 handler : function (event) {
92 92 IPython.notebook.save_checkpoint();
93 93 event.preventDefault();
94 94 return false;
95 95 }
96 96 },
97 97 'shift' : {
98 98 help : '',
99 99 handler : function (event) {
100 100 // ignore shift keydown
101 101 return true;
102 102 }
103 103 },
104 104 'shift+enter' : {
105 105 help : 'run cell',
106 106 handler : function (event) {
107 107 IPython.notebook.execute_selected_cell('shift');
108 108 return false;
109 109 }
110 110 },
111 111 'alt+enter' : {
112 112 help : 'run cell, insert below',
113 113 handler : function (event) {
114 114 IPython.notebook.execute_selected_cell('alt');
115 115 return false;
116 116 }
117 117 },
118 118 'ctrl+enter' : {
119 119 help : 'run cell, select below',
120 120 handler : function (event) {
121 121 IPython.notebook.execute_selected_cell('ctrl');
122 122 return false;
123 123 }
124 124 }
125 125 }
126 126
127 127 // Edit mode defaults
128 128
129 129 var default_edit_shortcuts = {
130 130 'esc' : {
131 131 help : 'command mode',
132 132 handler : function (event) {
133 133 IPython.notebook.command_mode();
134 IPython.notebook.focus_cell();
134 135 return false;
135 136 }
136 137 },
137 138 'ctrl+m' : {
138 139 help : 'command mode',
139 140 handler : function (event) {
140 141 IPython.notebook.command_mode();
142 IPython.notebook.focus_cell();
141 143 return false;
142 144 }
143 145 },
144 146 'up' : {
145 147 help : 'select previous cell',
146 148 handler : function (event) {
147 149 var cell = IPython.notebook.get_selected_cell();
148 150 if (cell && cell.at_top()) {
149 151 event.preventDefault();
150 152 IPython.notebook.command_mode()
151 153 IPython.notebook.select_prev();
152 154 IPython.notebook.edit_mode();
153 155 return false;
154 156 };
155 157 }
156 158 },
157 159 'down' : {
158 160 help : 'select next cell',
159 161 handler : function (event) {
160 162 var cell = IPython.notebook.get_selected_cell();
161 163 if (cell && cell.at_bottom()) {
162 164 event.preventDefault();
163 165 IPython.notebook.command_mode()
164 166 IPython.notebook.select_next();
165 167 IPython.notebook.edit_mode();
166 168 return false;
167 169 };
168 170 }
169 171 },
170 172 'alt+-' : {
171 173 help : 'split cell',
172 174 handler : function (event) {
173 175 IPython.notebook.split_cell();
174 176 return false;
175 177 }
176 178 },
177 179 }
178 180
179 181 // Command mode defaults
180 182
181 183 var default_command_shortcuts = {
182 184 'enter' : {
183 185 help : 'edit mode',
184 186 handler : function (event) {
185 187 IPython.notebook.edit_mode();
186 188 return false;
187 189 }
188 190 },
189 191 'up' : {
190 192 help : 'select previous cell',
191 193 handler : function (event) {
192 194 var index = IPython.notebook.get_selected_index();
193 195 if (index !== 0 && index !== null) {
194 196 IPython.notebook.select_prev();
195 197 var cell = IPython.notebook.get_selected_cell();
196 198 cell.focus_cell();
197 199 };
198 200 return false;
199 201 }
200 202 },
201 203 'down' : {
202 204 help : 'select next cell',
203 205 handler : function (event) {
204 206 var index = IPython.notebook.get_selected_index();
205 207 if (index !== (IPython.notebook.ncells()-1) && index !== null) {
206 208 IPython.notebook.select_next();
207 209 var cell = IPython.notebook.get_selected_cell();
208 210 cell.focus_cell();
209 211 };
210 212 return false;
211 213 }
212 214 },
213 215 'k' : {
214 216 help : 'select previous cell',
215 217 handler : function (event) {
216 218 var index = IPython.notebook.get_selected_index();
217 219 if (index !== 0 && index !== null) {
218 220 IPython.notebook.select_prev();
219 221 var cell = IPython.notebook.get_selected_cell();
220 222 cell.focus_cell();
221 223 };
222 224 return false;
223 225 }
224 226 },
225 227 'j' : {
226 228 help : 'select next cell',
227 229 handler : function (event) {
228 230 var index = IPython.notebook.get_selected_index();
229 231 if (index !== (IPython.notebook.ncells()-1) && index !== null) {
230 232 IPython.notebook.select_next();
231 233 var cell = IPython.notebook.get_selected_cell();
232 234 cell.focus_cell();
233 235 };
234 236 return false;
235 237 }
236 238 },
237 239 'x' : {
238 240 help : 'cut cell',
239 241 handler : function (event) {
240 242 IPython.notebook.cut_cell();
241 243 return false;
242 244 }
243 245 },
244 246 'c' : {
245 247 help : 'copy cell',
246 248 handler : function (event) {
247 249 IPython.notebook.copy_cell();
248 250 return false;
249 251 }
250 252 },
251 253 'v' : {
252 254 help : 'paste cell below',
253 255 handler : function (event) {
254 256 IPython.notebook.paste_cell_below();
255 257 return false;
256 258 }
257 259 },
258 260 'd' : {
259 261 help : 'delete cell (press twice)',
260 262 handler : function (event) {
261 263 var dc = IPython.delete_count;
262 264 if (dc === undefined) {
263 265 IPython.delete_count = 1;
264 266 } else if (dc === 0) {
265 267 IPython.delete_count = 1;
266 268 setTimeout(function () {
267 269 IPython.delete_count = 0;
268 270 }, 800);
269 271 } else if (dc === 1) {
270 272 IPython.notebook.delete_cell();
271 273 IPython.delete_count = 0;
272 274 }
273 275 return false;
274 276 }
275 277 },
276 278 'a' : {
277 279 help : 'insert cell above',
278 280 handler : function (event) {
279 281 IPython.notebook.insert_cell_above('code');
280 282 IPython.notebook.select_prev();
281 283 return false;
282 284 }
283 285 },
284 286 'b' : {
285 287 help : 'insert cell below',
286 288 handler : function (event) {
287 289 IPython.notebook.insert_cell_below('code');
288 290 IPython.notebook.select_next();
289 291 return false;
290 292 }
291 293 },
292 294 'y' : {
293 295 help : 'to code',
294 296 handler : function (event) {
295 297 IPython.notebook.to_code();
296 298 return false;
297 299 }
298 300 },
299 301 'm' : {
300 302 help : 'to markdown',
301 303 handler : function (event) {
302 304 IPython.notebook.to_markdown();
303 305 return false;
304 306 }
305 307 },
306 308 't' : {
307 309 help : 'to raw',
308 310 handler : function (event) {
309 311 IPython.notebook.to_raw();
310 312 return false;
311 313 }
312 314 },
313 315 '1' : {
314 316 help : 'to heading 1',
315 317 handler : function (event) {
316 318 IPython.notebook.to_heading(undefined, 1);
317 319 return false;
318 320 }
319 321 },
320 322 '2' : {
321 323 help : 'to heading 2',
322 324 handler : function (event) {
323 325 IPython.notebook.to_heading(undefined, 2);
324 326 return false;
325 327 }
326 328 },
327 329 '3' : {
328 330 help : 'to heading 3',
329 331 handler : function (event) {
330 332 IPython.notebook.to_heading(undefined, 3);
331 333 return false;
332 334 }
333 335 },
334 336 '4' : {
335 337 help : 'to heading 4',
336 338 handler : function (event) {
337 339 IPython.notebook.to_heading(undefined, 4);
338 340 return false;
339 341 }
340 342 },
341 343 '5' : {
342 344 help : 'to heading 5',
343 345 handler : function (event) {
344 346 IPython.notebook.to_heading(undefined, 5);
345 347 return false;
346 348 }
347 349 },
348 350 '6' : {
349 351 help : 'to heading 6',
350 352 handler : function (event) {
351 353 IPython.notebook.to_heading(undefined, 6);
352 354 return false;
353 355 }
354 356 },
355 357 'o' : {
356 358 help : 'toggle output',
357 359 handler : function (event) {
358 360 IPython.notebook.toggle_output();
359 361 return false;
360 362 }
361 363 },
362 364 'shift+o' : {
363 365 help : 'toggle output',
364 366 handler : function (event) {
365 367 IPython.notebook.toggle_output_scroll();
366 368 return false;
367 369 }
368 370 },
369 371 's' : {
370 372 help : 'save notebook',
371 373 handler : function (event) {
372 374 IPython.notebook.save_checkpoint();
373 375 return false;
374 376 }
375 377 },
376 378 'ctrl+j' : {
377 379 help : 'move cell down',
378 380 handler : function (event) {
379 381 IPython.notebook.move_cell_down();
380 382 return false;
381 383 }
382 384 },
383 385 'ctrl+k' : {
384 386 help : 'move cell up',
385 387 handler : function (event) {
386 388 IPython.notebook.move_cell_up();
387 389 return false;
388 390 }
389 391 },
390 392 'l' : {
391 393 help : 'toggle line numbers',
392 394 handler : function (event) {
393 395 IPython.notebook.cell_toggle_line_numbers();
394 396 return false;
395 397 }
396 398 },
397 399 'i' : {
398 400 help : 'interrupt kernel',
399 401 handler : function (event) {
400 402 IPython.notebook.kernel.interrupt();
401 403 return false;
402 404 }
403 405 },
404 406 '.' : {
405 407 help : 'restart kernel',
406 408 handler : function (event) {
407 409 IPython.notebook.restart_kernel();
408 410 return false;
409 411 }
410 412 },
411 413 'h' : {
412 414 help : 'keyboard shortcuts',
413 415 handler : function (event) {
414 416 IPython.quick_help.show_keyboard_shortcuts();
415 417 return false;
416 418 }
417 419 },
418 420 'z' : {
419 421 help : 'undo last delete',
420 422 handler : function (event) {
421 423 IPython.notebook.undelete_cell();
422 424 return false;
423 425 }
424 426 },
425 427 '-' : {
426 428 help : 'split cell',
427 429 handler : function (event) {
428 430 IPython.notebook.split_cell();
429 431 return false;
430 432 }
431 433 },
432 434 'shift+=' : {
433 435 help : 'merge cell below',
434 436 handler : function (event) {
435 437 IPython.notebook.merge_cell_below();
436 438 return false;
437 439 }
438 440 },
439 441 }
440 442
441 443
442 444 // Shortcut manager class
443 445
444 446 var ShortcutManager = function () {
445 447 this._shortcuts = {}
446 448 }
447 449
448 450 ShortcutManager.prototype.help = function () {
449 451 var help = [];
450 452 for (var shortcut in this._shortcuts) {
451 453 help.push({shortcut: shortcut, help: this._shortcuts[shortcut]['help']});
452 454 }
453 455 return help;
454 456 }
455 457
456 458 ShortcutManager.prototype.canonicalize_key = function (key) {
457 459 return inv_keycodes[keycodes[key]];
458 460 }
459 461
460 462 ShortcutManager.prototype.canonicalize_shortcut = function (shortcut) {
461 463 // Sort a sequence of + separated modifiers into the order alt+ctrl+meta+shift
462 464 var values = shortcut.split("+");
463 465 if (values.length === 1) {
464 466 return this.canonicalize_key(values[0])
465 467 } else {
466 468 var modifiers = values.slice(0,-1);
467 469 var key = this.canonicalize_key(values[values.length-1]);
468 470 modifiers.sort();
469 471 return modifiers.join('+') + '+' + key;
470 472 }
471 473 }
472 474
473 475 ShortcutManager.prototype.event_to_shortcut = function (event) {
474 476 // Convert a jQuery keyboard event to a strong based keyboard shortcut
475 477 var shortcut = '';
476 478 var key = inv_keycodes[event.which]
477 479 if (event.altKey && key !== 'alt') {shortcut += 'alt+';}
478 480 if (event.ctrlKey && key !== 'ctrl') {shortcut += 'ctrl+';}
479 481 if (event.metaKey && key !== 'meta') {shortcut += 'meta+';}
480 482 if (event.shiftKey && key !== 'shift') {shortcut += 'shift+';}
481 483 shortcut += key;
482 484 return shortcut
483 485 }
484 486
485 487 ShortcutManager.prototype.clear_shortcuts = function () {
486 488 this._shortcuts = {};
487 489 }
488 490
489 491 ShortcutManager.prototype.add_shortcut = function (shortcut, data) {
490 492 shortcut = this.canonicalize_shortcut(shortcut);
491 493 this._shortcuts[shortcut] = data;
492 494 }
493 495
494 496 ShortcutManager.prototype.add_shortcuts = function (data) {
495 497 for (var shortcut in data) {
496 498 this.add_shortcut(shortcut, data[shortcut]);
497 499 }
498 500 }
499 501
500 502 ShortcutManager.prototype.remove_shortcut = function (shortcut) {
501 503 shortcut = this.canonicalize_shortcut(shortcut);
502 504 delete this._shortcuts[shortcut];
503 505 }
504 506
505 507 ShortcutManager.prototype.call_handler = function (event) {
506 508 var shortcut = this.event_to_shortcut(event);
507 509 var data = this._shortcuts[shortcut];
508 510 if (data !== undefined) {
509 511 var handler = data['handler'];
510 512 if (handler !== undefined) {
511 513 return handler(event);
512 514 }
513 515 }
514 516 return true;
515 517 }
516 518
517 519
518 520
519 521 // Main keyboard manager for the notebook
520 522
521 523 var KeyboardManager = function () {
522 524 this.mode = 'command';
523 525 this.enabled = true;
524 526 this.delete_count = 0;
525 527 this.bind_events();
526 528 this.command_shortcuts = new ShortcutManager();
527 529 this.command_shortcuts.add_shortcuts(default_common_shortcuts);
528 530 this.command_shortcuts.add_shortcuts(default_command_shortcuts);
529 531 this.edit_shortcuts = new ShortcutManager();
530 532 this.edit_shortcuts.add_shortcuts(default_common_shortcuts);
531 533 this.edit_shortcuts.add_shortcuts(default_edit_shortcuts);
532 534 };
533 535
534 536 KeyboardManager.prototype.bind_events = function () {
535 537 var that = this;
536 538 $(document).keydown(function (event) {
537 539 return that.handle_keydown(event);
538 540 });
539 541 };
540 542
541 543 KeyboardManager.prototype.handle_keydown = function (event) {
542 544 var notebook = IPython.notebook;
543 545
544 546 console.log('keyboard_manager', this.mode, event.keyCode);
545 547
546 548 if (event.which === keycodes['esc']) {
547 549 // Intercept escape at highest level to avoid closing
548 550 // websocket connection with firefox
549 551 event.preventDefault();
550 552 }
551 553
552 554 if (!this.enabled) {
553 555 if (event.which === keycodes['esc']) {
554 556 // ESC
555 557 notebook.command_mode();
556 558 return false;
557 559 }
558 560 return true;
559 561 }
560 562
561 563 if (this.mode === 'edit') {
562 564 return this.edit_shortcuts.call_handler(event);
563 565 } else if (this.mode === 'command') {
564 566 return this.command_shortcuts.call_handler(event);
565 567 }
566 568 return true;
567 569 }
568 570
569 571 KeyboardManager.prototype.edit_mode = function () {
570 572 console.log('KeyboardManager', 'changing to edit mode');
571 573 this.last_mode = this.mode;
572 574 this.mode = 'edit';
573 575 }
574 576
575 577 KeyboardManager.prototype.command_mode = function () {
576 578 console.log('KeyboardManager', 'changing to command mode');
577 579 this.last_mode = this.mode;
578 580 this.mode = 'command';
579 581 }
580 582
581 583 KeyboardManager.prototype.enable = function () {
582 584 this.enabled = true;
583 585 }
584 586
585 587 KeyboardManager.prototype.disable = function () {
586 588 this.enabled = false;
587 589 }
588 590
589 591 KeyboardManager.prototype.register_events = function (e) {
590 592 var that = this;
591 593 e.on('focusin', function () {
592 594 that.command_mode();
593 595 that.disable();
594 596 });
595 597 e.on('focusout', function () {
596 598 that.command_mode();
597 599 that.enable();
598 600 });
599 601 // There are times (raw_input) where we remove the element from the DOM before
600 602 // focusout is called. In this case we bind to the remove event of jQueryUI,
601 603 // which gets triggered upon removal.
602 604 e.on('remove', function () {
603 605 that.command_mode();
604 606 that.enable();
605 607 });
606 608 }
607 609
608 610
609 611 IPython.keycodes = keycodes;
610 612 IPython.inv_keycodes = inv_keycodes;
611 613 IPython.default_common_shortcuts = default_common_shortcuts;
612 614 IPython.default_edit_shortcuts = default_edit_shortcuts;
613 615 IPython.default_command_shortcuts = default_command_shortcuts;
614 616 IPython.ShortcutManager = ShortcutManager;
615 617 IPython.KeyboardManager = KeyboardManager;
616 618
617 619 return IPython;
618 620
619 621 }(IPython));
@@ -1,2180 +1,2185
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2011 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 // Notebook
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 var utils = IPython.utils;
16 16
17 17 /**
18 18 * A notebook contains and manages cells.
19 19 *
20 20 * @class Notebook
21 21 * @constructor
22 22 * @param {String} selector A jQuery selector for the notebook's DOM element
23 23 * @param {Object} [options] A config object
24 24 */
25 25 var Notebook = function (selector, options) {
26 26 var options = options || {};
27 27 this._baseProjectUrl = options.baseProjectUrl;
28 28 this.notebook_path = options.notebookPath;
29 29 this.notebook_name = options.notebookName;
30 30 this.element = $(selector);
31 31 this.element.scroll();
32 32 this.element.data("notebook", this);
33 33 this.next_prompt_number = 1;
34 34 this.session = null;
35 35 this.kernel = null;
36 36 this.clipboard = null;
37 37 this.undelete_backup = null;
38 38 this.undelete_index = null;
39 39 this.undelete_below = false;
40 40 this.paste_enabled = false;
41 41 // It is important to start out in command mode to match the intial mode
42 42 // of the KeyboardManager.
43 43 this.mode = 'command';
44 44 this.set_dirty(false);
45 45 this.metadata = {};
46 46 this._checkpoint_after_save = false;
47 47 this.last_checkpoint = null;
48 48 this.checkpoints = [];
49 49 this.autosave_interval = 0;
50 50 this.autosave_timer = null;
51 51 // autosave *at most* every two minutes
52 52 this.minimum_autosave_interval = 120000;
53 53 // single worksheet for now
54 54 this.worksheet_metadata = {};
55 55 this.notebook_name_blacklist_re = /[\/\\:]/;
56 56 this.nbformat = 3 // Increment this when changing the nbformat
57 57 this.nbformat_minor = 0 // Increment this when changing the nbformat
58 58 this.style();
59 59 this.create_elements();
60 60 this.bind_events();
61 61 };
62 62
63 63 /**
64 64 * Tweak the notebook's CSS style.
65 65 *
66 66 * @method style
67 67 */
68 68 Notebook.prototype.style = function () {
69 69 $('div#notebook').addClass('border-box-sizing');
70 70 };
71 71
72 72 /**
73 73 * Get the root URL of the notebook server.
74 74 *
75 75 * @method baseProjectUrl
76 76 * @return {String} The base project URL
77 77 */
78 78 Notebook.prototype.baseProjectUrl = function() {
79 79 return this._baseProjectUrl || $('body').data('baseProjectUrl');
80 80 };
81 81
82 82 Notebook.prototype.notebookName = function() {
83 83 return $('body').data('notebookName');
84 84 };
85 85
86 86 Notebook.prototype.notebookPath = function() {
87 87 return $('body').data('notebookPath');
88 88 };
89 89
90 90 /**
91 91 * Create an HTML and CSS representation of the notebook.
92 92 *
93 93 * @method create_elements
94 94 */
95 95 Notebook.prototype.create_elements = function () {
96 96 var that = this;
97 97 this.element.attr('tabindex','-1');
98 98 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
99 99 // We add this end_space div to the end of the notebook div to:
100 100 // i) provide a margin between the last cell and the end of the notebook
101 101 // ii) to prevent the div from scrolling up when the last cell is being
102 102 // edited, but is too low on the page, which browsers will do automatically.
103 103 var end_space = $('<div/>').addClass('end_space');
104 104 end_space.dblclick(function (e) {
105 105 var ncells = that.ncells();
106 106 that.insert_cell_below('code',ncells-1);
107 107 });
108 108 this.element.append(this.container);
109 109 this.container.append(end_space);
110 110 };
111 111
112 112 /**
113 113 * Bind JavaScript events: key presses and custom IPython events.
114 114 *
115 115 * @method bind_events
116 116 */
117 117 Notebook.prototype.bind_events = function () {
118 118 var that = this;
119 119
120 120 $([IPython.events]).on('set_next_input.Notebook', function (event, data) {
121 121 var index = that.find_cell_index(data.cell);
122 122 var new_cell = that.insert_cell_below('code',index);
123 123 new_cell.set_text(data.text);
124 124 that.dirty = true;
125 125 });
126 126
127 127 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
128 128 that.dirty = data.value;
129 129 });
130 130
131 131 $([IPython.events]).on('select.Cell', function (event, data) {
132 132 var index = that.find_cell_index(data.cell);
133 133 that.select(index);
134 134 });
135 135
136 136 $([IPython.events]).on('edit_mode.Cell', function (event, data) {
137 137 var index = that.find_cell_index(data.cell);
138 138 that.select(index);
139 139 that.edit_mode();
140 140 });
141 141
142 142 $([IPython.events]).on('command_mode.Cell', function (event, data) {
143 143 that.command_mode();
144 144 });
145 145
146 146 $([IPython.events]).on('status_autorestarting.Kernel', function () {
147 147 IPython.dialog.modal({
148 148 title: "Kernel Restarting",
149 149 body: "The kernel appears to have died. It will restart automatically.",
150 150 buttons: {
151 151 OK : {
152 152 class : "btn-primary"
153 153 }
154 154 }
155 155 });
156 156 });
157 157
158 158 var collapse_time = function (time) {
159 159 var app_height = $('#ipython-main-app').height(); // content height
160 160 var splitter_height = $('div#pager_splitter').outerHeight(true);
161 161 var new_height = app_height - splitter_height;
162 162 that.element.animate({height : new_height + 'px'}, time);
163 163 };
164 164
165 165 this.element.bind('collapse_pager', function (event, extrap) {
166 166 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
167 167 collapse_time(time);
168 168 });
169 169
170 170 var expand_time = function (time) {
171 171 var app_height = $('#ipython-main-app').height(); // content height
172 172 var splitter_height = $('div#pager_splitter').outerHeight(true);
173 173 var pager_height = $('div#pager').outerHeight(true);
174 174 var new_height = app_height - pager_height - splitter_height;
175 175 that.element.animate({height : new_height + 'px'}, time);
176 176 };
177 177
178 178 this.element.bind('expand_pager', function (event, extrap) {
179 179 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
180 180 expand_time(time);
181 181 });
182 182
183 183 // Firefox 22 broke $(window).on("beforeunload")
184 184 // I'm not sure why or how.
185 185 window.onbeforeunload = function (e) {
186 186 // TODO: Make killing the kernel configurable.
187 187 var kill_kernel = false;
188 188 if (kill_kernel) {
189 189 that.session.kill_kernel();
190 190 }
191 191 // if we are autosaving, trigger an autosave on nav-away.
192 192 // still warn, because if we don't the autosave may fail.
193 193 if (that.dirty) {
194 194 if ( that.autosave_interval ) {
195 195 // schedule autosave in a timeout
196 196 // this gives you a chance to forcefully discard changes
197 197 // by reloading the page if you *really* want to.
198 198 // the timer doesn't start until you *dismiss* the dialog.
199 199 setTimeout(function () {
200 200 if (that.dirty) {
201 201 that.save_notebook();
202 202 }
203 203 }, 1000);
204 204 return "Autosave in progress, latest changes may be lost.";
205 205 } else {
206 206 return "Unsaved changes will be lost.";
207 207 }
208 208 };
209 209 // Null is the *only* return value that will make the browser not
210 210 // pop up the "don't leave" dialog.
211 211 return null;
212 212 };
213 213 };
214 214
215 215 /**
216 216 * Set the dirty flag, and trigger the set_dirty.Notebook event
217 217 *
218 218 * @method set_dirty
219 219 */
220 220 Notebook.prototype.set_dirty = function (value) {
221 221 if (value === undefined) {
222 222 value = true;
223 223 }
224 224 if (this.dirty == value) {
225 225 return;
226 226 }
227 227 $([IPython.events]).trigger('set_dirty.Notebook', {value: value});
228 228 };
229 229
230 230 /**
231 231 * Scroll the top of the page to a given cell.
232 232 *
233 233 * @method scroll_to_cell
234 234 * @param {Number} cell_number An index of the cell to view
235 235 * @param {Number} time Animation time in milliseconds
236 236 * @return {Number} Pixel offset from the top of the container
237 237 */
238 238 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
239 239 var cells = this.get_cells();
240 240 var time = time || 0;
241 241 cell_number = Math.min(cells.length-1,cell_number);
242 242 cell_number = Math.max(0 ,cell_number);
243 243 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
244 244 this.element.animate({scrollTop:scroll_value}, time);
245 245 return scroll_value;
246 246 };
247 247
248 248 /**
249 249 * Scroll to the bottom of the page.
250 250 *
251 251 * @method scroll_to_bottom
252 252 */
253 253 Notebook.prototype.scroll_to_bottom = function () {
254 254 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
255 255 };
256 256
257 257 /**
258 258 * Scroll to the top of the page.
259 259 *
260 260 * @method scroll_to_top
261 261 */
262 262 Notebook.prototype.scroll_to_top = function () {
263 263 this.element.animate({scrollTop:0}, 0);
264 264 };
265 265
266 266 // Edit Notebook metadata
267 267
268 268 Notebook.prototype.edit_metadata = function () {
269 269 var that = this;
270 270 IPython.dialog.edit_metadata(this.metadata, function (md) {
271 271 that.metadata = md;
272 272 }, 'Notebook');
273 273 };
274 274
275 275 // Cell indexing, retrieval, etc.
276 276
277 277 /**
278 278 * Get all cell elements in the notebook.
279 279 *
280 280 * @method get_cell_elements
281 281 * @return {jQuery} A selector of all cell elements
282 282 */
283 283 Notebook.prototype.get_cell_elements = function () {
284 284 return this.container.children("div.cell");
285 285 };
286 286
287 287 /**
288 288 * Get a particular cell element.
289 289 *
290 290 * @method get_cell_element
291 291 * @param {Number} index An index of a cell to select
292 292 * @return {jQuery} A selector of the given cell.
293 293 */
294 294 Notebook.prototype.get_cell_element = function (index) {
295 295 var result = null;
296 296 var e = this.get_cell_elements().eq(index);
297 297 if (e.length !== 0) {
298 298 result = e;
299 299 }
300 300 return result;
301 301 };
302 302
303 303 /**
304 304 * Count the cells in this notebook.
305 305 *
306 306 * @method ncells
307 307 * @return {Number} The number of cells in this notebook
308 308 */
309 309 Notebook.prototype.ncells = function () {
310 310 return this.get_cell_elements().length;
311 311 };
312 312
313 313 /**
314 314 * Get all Cell objects in this notebook.
315 315 *
316 316 * @method get_cells
317 317 * @return {Array} This notebook's Cell objects
318 318 */
319 319 // TODO: we are often calling cells as cells()[i], which we should optimize
320 320 // to cells(i) or a new method.
321 321 Notebook.prototype.get_cells = function () {
322 322 return this.get_cell_elements().toArray().map(function (e) {
323 323 return $(e).data("cell");
324 324 });
325 325 };
326 326
327 327 /**
328 328 * Get a Cell object from this notebook.
329 329 *
330 330 * @method get_cell
331 331 * @param {Number} index An index of a cell to retrieve
332 332 * @return {Cell} A particular cell
333 333 */
334 334 Notebook.prototype.get_cell = function (index) {
335 335 var result = null;
336 336 var ce = this.get_cell_element(index);
337 337 if (ce !== null) {
338 338 result = ce.data('cell');
339 339 }
340 340 return result;
341 341 }
342 342
343 343 /**
344 344 * Get the cell below a given cell.
345 345 *
346 346 * @method get_next_cell
347 347 * @param {Cell} cell The provided cell
348 348 * @return {Cell} The next cell
349 349 */
350 350 Notebook.prototype.get_next_cell = function (cell) {
351 351 var result = null;
352 352 var index = this.find_cell_index(cell);
353 353 if (this.is_valid_cell_index(index+1)) {
354 354 result = this.get_cell(index+1);
355 355 }
356 356 return result;
357 357 }
358 358
359 359 /**
360 360 * Get the cell above a given cell.
361 361 *
362 362 * @method get_prev_cell
363 363 * @param {Cell} cell The provided cell
364 364 * @return {Cell} The previous cell
365 365 */
366 366 Notebook.prototype.get_prev_cell = function (cell) {
367 367 // TODO: off-by-one
368 368 // nb.get_prev_cell(nb.get_cell(1)) is null
369 369 var result = null;
370 370 var index = this.find_cell_index(cell);
371 371 if (index !== null && index > 1) {
372 372 result = this.get_cell(index-1);
373 373 }
374 374 return result;
375 375 }
376 376
377 377 /**
378 378 * Get the numeric index of a given cell.
379 379 *
380 380 * @method find_cell_index
381 381 * @param {Cell} cell The provided cell
382 382 * @return {Number} The cell's numeric index
383 383 */
384 384 Notebook.prototype.find_cell_index = function (cell) {
385 385 var result = null;
386 386 this.get_cell_elements().filter(function (index) {
387 387 if ($(this).data("cell") === cell) {
388 388 result = index;
389 389 };
390 390 });
391 391 return result;
392 392 };
393 393
394 394 /**
395 395 * Get a given index , or the selected index if none is provided.
396 396 *
397 397 * @method index_or_selected
398 398 * @param {Number} index A cell's index
399 399 * @return {Number} The given index, or selected index if none is provided.
400 400 */
401 401 Notebook.prototype.index_or_selected = function (index) {
402 402 var i;
403 403 if (index === undefined || index === null) {
404 404 i = this.get_selected_index();
405 405 if (i === null) {
406 406 i = 0;
407 407 }
408 408 } else {
409 409 i = index;
410 410 }
411 411 return i;
412 412 };
413 413
414 414 /**
415 415 * Get the currently selected cell.
416 416 * @method get_selected_cell
417 417 * @return {Cell} The selected cell
418 418 */
419 419 Notebook.prototype.get_selected_cell = function () {
420 420 var index = this.get_selected_index();
421 421 return this.get_cell(index);
422 422 };
423 423
424 424 /**
425 425 * Check whether a cell index is valid.
426 426 *
427 427 * @method is_valid_cell_index
428 428 * @param {Number} index A cell index
429 429 * @return True if the index is valid, false otherwise
430 430 */
431 431 Notebook.prototype.is_valid_cell_index = function (index) {
432 432 if (index !== null && index >= 0 && index < this.ncells()) {
433 433 return true;
434 434 } else {
435 435 return false;
436 436 };
437 437 }
438 438
439 439 /**
440 440 * Get the index of the currently selected cell.
441 441
442 442 * @method get_selected_index
443 443 * @return {Number} The selected cell's numeric index
444 444 */
445 445 Notebook.prototype.get_selected_index = function () {
446 446 var result = null;
447 447 this.get_cell_elements().filter(function (index) {
448 448 if ($(this).data("cell").selected === true) {
449 449 result = index;
450 450 };
451 451 });
452 452 return result;
453 453 };
454 454
455 455
456 456 // Cell selection.
457 457
458 458 /**
459 459 * Programmatically select a cell.
460 460 *
461 461 * @method select
462 462 * @param {Number} index A cell's index
463 463 * @return {Notebook} This notebook
464 464 */
465 465 Notebook.prototype.select = function (index) {
466 466 if (this.is_valid_cell_index(index)) {
467 467 var sindex = this.get_selected_index()
468 468 if (sindex !== null && index !== sindex) {
469 469 this.command_mode();
470 470 this.get_cell(sindex).unselect();
471 471 };
472 472 var cell = this.get_cell(index);
473 473 cell.select();
474 474 if (cell.cell_type === 'heading') {
475 475 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
476 476 {'cell_type':cell.cell_type,level:cell.level}
477 477 );
478 478 } else {
479 479 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
480 480 {'cell_type':cell.cell_type}
481 481 );
482 482 };
483 483 };
484 484 return this;
485 485 };
486 486
487 487 /**
488 488 * Programmatically select the next cell.
489 489 *
490 490 * @method select_next
491 491 * @return {Notebook} This notebook
492 492 */
493 493 Notebook.prototype.select_next = function () {
494 494 var index = this.get_selected_index();
495 495 this.select(index+1);
496 496 return this;
497 497 };
498 498
499 499 /**
500 500 * Programmatically select the previous cell.
501 501 *
502 502 * @method select_prev
503 503 * @return {Notebook} This notebook
504 504 */
505 505 Notebook.prototype.select_prev = function () {
506 506 var index = this.get_selected_index();
507 507 this.select(index-1);
508 508 return this;
509 509 };
510 510
511 511
512 512 // Edit/Command mode
513 513
514 514 Notebook.prototype.get_edit_index = function () {
515 515 var result = null;
516 516 this.get_cell_elements().filter(function (index) {
517 517 if ($(this).data("cell").mode === 'edit') {
518 518 result = index;
519 519 };
520 520 });
521 521 return result;
522 522 };
523 523
524 524 Notebook.prototype.command_mode = function () {
525 525 if (this.mode !== 'command') {
526 526 console.log('\nNotebook', 'changing to command mode');
527 527 var index = this.get_edit_index();
528 528 var cell = this.get_cell(index);
529 529 if (cell) {
530 530 cell.command_mode();
531 531 };
532 532 this.mode = 'command';
533 533 IPython.keyboard_manager.command_mode();
534 534 };
535 535 };
536 536
537 537 Notebook.prototype.edit_mode = function () {
538 538 if (this.mode !== 'edit') {
539 539 console.log('\nNotebook', 'changing to edit mode');
540 540 var cell = this.get_selected_cell();
541 541 if (cell === null) {return;} // No cell is selected
542 542 // We need to set the mode to edit to prevent reentering this method
543 543 // when cell.edit_mode() is called below.
544 544 this.mode = 'edit';
545 545 IPython.keyboard_manager.edit_mode();
546 546 cell.edit_mode();
547 547 };
548 548 };
549 549
550 Notebook.prototype.focus_cell = function () {
551 var cell = this.get_selected_cell();
552 if (cell === null) {return;} // No cell is selected
553 cell.focus_cell();
554 };
550 555
551 556 // Cell movement
552 557
553 558 /**
554 559 * Move given (or selected) cell up and select it.
555 560 *
556 561 * @method move_cell_up
557 562 * @param [index] {integer} cell index
558 563 * @return {Notebook} This notebook
559 564 **/
560 565 Notebook.prototype.move_cell_up = function (index) {
561 566 var i = this.index_or_selected(index);
562 567 if (this.is_valid_cell_index(i) && i > 0) {
563 568 var pivot = this.get_cell_element(i-1);
564 569 var tomove = this.get_cell_element(i);
565 570 if (pivot !== null && tomove !== null) {
566 571 tomove.detach();
567 572 pivot.before(tomove);
568 573 this.select(i-1);
569 574 var cell = this.get_selected_cell();
570 575 cell.focus_cell();
571 576 };
572 577 this.set_dirty(true);
573 578 };
574 579 return this;
575 580 };
576 581
577 582
578 583 /**
579 584 * Move given (or selected) cell down and select it
580 585 *
581 586 * @method move_cell_down
582 587 * @param [index] {integer} cell index
583 588 * @return {Notebook} This notebook
584 589 **/
585 590 Notebook.prototype.move_cell_down = function (index) {
586 591 var i = this.index_or_selected(index);
587 592 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
588 593 var pivot = this.get_cell_element(i+1);
589 594 var tomove = this.get_cell_element(i);
590 595 if (pivot !== null && tomove !== null) {
591 596 tomove.detach();
592 597 pivot.after(tomove);
593 598 this.select(i+1);
594 599 var cell = this.get_selected_cell();
595 600 cell.focus_cell();
596 601 };
597 602 };
598 603 this.set_dirty();
599 604 return this;
600 605 };
601 606
602 607
603 608 // Insertion, deletion.
604 609
605 610 /**
606 611 * Delete a cell from the notebook.
607 612 *
608 613 * @method delete_cell
609 614 * @param [index] A cell's numeric index
610 615 * @return {Notebook} This notebook
611 616 */
612 617 Notebook.prototype.delete_cell = function (index) {
613 618 var i = this.index_or_selected(index);
614 619 var cell = this.get_selected_cell();
615 620 this.undelete_backup = cell.toJSON();
616 621 $('#undelete_cell').removeClass('disabled');
617 622 if (this.is_valid_cell_index(i)) {
618 623 var old_ncells = this.ncells();
619 624 var ce = this.get_cell_element(i);
620 625 ce.remove();
621 626 if (i === 0) {
622 627 // Always make sure we have at least one cell.
623 628 if (old_ncells === 1) {
624 629 this.insert_cell_below('code');
625 630 }
626 631 this.select(0);
627 632 this.undelete_index = 0;
628 633 this.undelete_below = false;
629 634 } else if (i === old_ncells-1 && i !== 0) {
630 635 this.select(i-1);
631 636 this.undelete_index = i - 1;
632 637 this.undelete_below = true;
633 638 } else {
634 639 this.select(i);
635 640 this.undelete_index = i;
636 641 this.undelete_below = false;
637 642 };
638 643 $([IPython.events]).trigger('delete.Cell', {'cell': cell, 'index': i});
639 644 this.set_dirty(true);
640 645 };
641 646 return this;
642 647 };
643 648
644 649 /**
645 650 * Restore the most recently deleted cell.
646 651 *
647 652 * @method undelete
648 653 */
649 654 Notebook.prototype.undelete_cell = function() {
650 655 if (this.undelete_backup !== null && this.undelete_index !== null) {
651 656 var current_index = this.get_selected_index();
652 657 if (this.undelete_index < current_index) {
653 658 current_index = current_index + 1;
654 659 }
655 660 if (this.undelete_index >= this.ncells()) {
656 661 this.select(this.ncells() - 1);
657 662 }
658 663 else {
659 664 this.select(this.undelete_index);
660 665 }
661 666 var cell_data = this.undelete_backup;
662 667 var new_cell = null;
663 668 if (this.undelete_below) {
664 669 new_cell = this.insert_cell_below(cell_data.cell_type);
665 670 } else {
666 671 new_cell = this.insert_cell_above(cell_data.cell_type);
667 672 }
668 673 new_cell.fromJSON(cell_data);
669 674 if (this.undelete_below) {
670 675 this.select(current_index+1);
671 676 } else {
672 677 this.select(current_index);
673 678 }
674 679 this.undelete_backup = null;
675 680 this.undelete_index = null;
676 681 }
677 682 $('#undelete_cell').addClass('disabled');
678 683 }
679 684
680 685 /**
681 686 * Insert a cell so that after insertion the cell is at given index.
682 687 *
683 688 * Similar to insert_above, but index parameter is mandatory
684 689 *
685 690 * Index will be brought back into the accissible range [0,n]
686 691 *
687 692 * @method insert_cell_at_index
688 693 * @param type {string} in ['code','markdown','heading']
689 694 * @param [index] {int} a valid index where to inser cell
690 695 *
691 696 * @return cell {cell|null} created cell or null
692 697 **/
693 698 Notebook.prototype.insert_cell_at_index = function(type, index){
694 699
695 700 var ncells = this.ncells();
696 701 var index = Math.min(index,ncells);
697 702 index = Math.max(index,0);
698 703 var cell = null;
699 704
700 705 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
701 706 if (type === 'code') {
702 707 cell = new IPython.CodeCell(this.kernel);
703 708 cell.set_input_prompt();
704 709 } else if (type === 'markdown') {
705 710 cell = new IPython.MarkdownCell();
706 711 } else if (type === 'raw') {
707 712 cell = new IPython.RawCell();
708 713 } else if (type === 'heading') {
709 714 cell = new IPython.HeadingCell();
710 715 }
711 716
712 717 if(this._insert_element_at_index(cell.element,index)) {
713 718 cell.render();
714 719 $([IPython.events]).trigger('create.Cell', {'cell': cell, 'index': index});
715 720 cell.refresh();
716 721 // We used to select the cell after we refresh it, but there
717 722 // are now cases were this method is called where select is
718 723 // not appropriate. The selection logic should be handled by the
719 724 // caller of the the top level insert_cell methods.
720 725 this.set_dirty(true);
721 726 }
722 727 }
723 728 return cell;
724 729
725 730 };
726 731
727 732 /**
728 733 * Insert an element at given cell index.
729 734 *
730 735 * @method _insert_element_at_index
731 736 * @param element {dom element} a cell element
732 737 * @param [index] {int} a valid index where to inser cell
733 738 * @private
734 739 *
735 740 * return true if everything whent fine.
736 741 **/
737 742 Notebook.prototype._insert_element_at_index = function(element, index){
738 743 if (element === undefined){
739 744 return false;
740 745 }
741 746
742 747 var ncells = this.ncells();
743 748
744 749 if (ncells === 0) {
745 750 // special case append if empty
746 751 this.element.find('div.end_space').before(element);
747 752 } else if ( ncells === index ) {
748 753 // special case append it the end, but not empty
749 754 this.get_cell_element(index-1).after(element);
750 755 } else if (this.is_valid_cell_index(index)) {
751 756 // otherwise always somewhere to append to
752 757 this.get_cell_element(index).before(element);
753 758 } else {
754 759 return false;
755 760 }
756 761
757 762 if (this.undelete_index !== null && index <= this.undelete_index) {
758 763 this.undelete_index = this.undelete_index + 1;
759 764 this.set_dirty(true);
760 765 }
761 766 return true;
762 767 };
763 768
764 769 /**
765 770 * Insert a cell of given type above given index, or at top
766 771 * of notebook if index smaller than 0.
767 772 *
768 773 * default index value is the one of currently selected cell
769 774 *
770 775 * @method insert_cell_above
771 776 * @param type {string} cell type
772 777 * @param [index] {integer}
773 778 *
774 779 * @return handle to created cell or null
775 780 **/
776 781 Notebook.prototype.insert_cell_above = function (type, index) {
777 782 index = this.index_or_selected(index);
778 783 return this.insert_cell_at_index(type, index);
779 784 };
780 785
781 786 /**
782 787 * Insert a cell of given type below given index, or at bottom
783 788 * of notebook if index greater thatn number of cell
784 789 *
785 790 * default index value is the one of currently selected cell
786 791 *
787 792 * @method insert_cell_below
788 793 * @param type {string} cell type
789 794 * @param [index] {integer}
790 795 *
791 796 * @return handle to created cell or null
792 797 *
793 798 **/
794 799 Notebook.prototype.insert_cell_below = function (type, index) {
795 800 index = this.index_or_selected(index);
796 801 return this.insert_cell_at_index(type, index+1);
797 802 };
798 803
799 804
800 805 /**
801 806 * Insert cell at end of notebook
802 807 *
803 808 * @method insert_cell_at_bottom
804 809 * @param {String} type cell type
805 810 *
806 811 * @return the added cell; or null
807 812 **/
808 813 Notebook.prototype.insert_cell_at_bottom = function (type){
809 814 var len = this.ncells();
810 815 return this.insert_cell_below(type,len-1);
811 816 };
812 817
813 818 /**
814 819 * Turn a cell into a code cell.
815 820 *
816 821 * @method to_code
817 822 * @param {Number} [index] A cell's index
818 823 */
819 824 Notebook.prototype.to_code = function (index) {
820 825 var i = this.index_or_selected(index);
821 826 if (this.is_valid_cell_index(i)) {
822 827 var source_element = this.get_cell_element(i);
823 828 var source_cell = source_element.data("cell");
824 829 if (!(source_cell instanceof IPython.CodeCell)) {
825 830 var target_cell = this.insert_cell_below('code',i);
826 831 var text = source_cell.get_text();
827 832 if (text === source_cell.placeholder) {
828 833 text = '';
829 834 }
830 835 target_cell.set_text(text);
831 836 // make this value the starting point, so that we can only undo
832 837 // to this state, instead of a blank cell
833 838 target_cell.code_mirror.clearHistory();
834 839 source_element.remove();
835 840 this.select(i);
836 841 this.edit_mode();
837 842 this.set_dirty(true);
838 843 };
839 844 };
840 845 };
841 846
842 847 /**
843 848 * Turn a cell into a Markdown cell.
844 849 *
845 850 * @method to_markdown
846 851 * @param {Number} [index] A cell's index
847 852 */
848 853 Notebook.prototype.to_markdown = function (index) {
849 854 var i = this.index_or_selected(index);
850 855 if (this.is_valid_cell_index(i)) {
851 856 var source_element = this.get_cell_element(i);
852 857 var source_cell = source_element.data("cell");
853 858 if (!(source_cell instanceof IPython.MarkdownCell)) {
854 859 var target_cell = this.insert_cell_below('markdown',i);
855 860 var text = source_cell.get_text();
856 861 if (text === source_cell.placeholder) {
857 862 text = '';
858 863 };
859 864 // We must show the editor before setting its contents
860 865 target_cell.unrender();
861 866 target_cell.set_text(text);
862 867 // make this value the starting point, so that we can only undo
863 868 // to this state, instead of a blank cell
864 869 target_cell.code_mirror.clearHistory();
865 870 source_element.remove();
866 871 this.select(i);
867 872 this.edit_mode();
868 873 this.set_dirty(true);
869 874 };
870 875 };
871 876 };
872 877
873 878 /**
874 879 * Turn a cell into a raw text cell.
875 880 *
876 881 * @method to_raw
877 882 * @param {Number} [index] A cell's index
878 883 */
879 884 Notebook.prototype.to_raw = function (index) {
880 885 var i = this.index_or_selected(index);
881 886 if (this.is_valid_cell_index(i)) {
882 887 var source_element = this.get_cell_element(i);
883 888 var source_cell = source_element.data("cell");
884 889 var target_cell = null;
885 890 if (!(source_cell instanceof IPython.RawCell)) {
886 891 target_cell = this.insert_cell_below('raw',i);
887 892 var text = source_cell.get_text();
888 893 if (text === source_cell.placeholder) {
889 894 text = '';
890 895 };
891 896 // We must show the editor before setting its contents
892 897 target_cell.unrender();
893 898 target_cell.set_text(text);
894 899 // make this value the starting point, so that we can only undo
895 900 // to this state, instead of a blank cell
896 901 target_cell.code_mirror.clearHistory();
897 902 source_element.remove();
898 903 this.select(i);
899 904 this.edit_mode();
900 905 this.set_dirty(true);
901 906 };
902 907 };
903 908 };
904 909
905 910 /**
906 911 * Turn a cell into a heading cell.
907 912 *
908 913 * @method to_heading
909 914 * @param {Number} [index] A cell's index
910 915 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
911 916 */
912 917 Notebook.prototype.to_heading = function (index, level) {
913 918 level = level || 1;
914 919 var i = this.index_or_selected(index);
915 920 if (this.is_valid_cell_index(i)) {
916 921 var source_element = this.get_cell_element(i);
917 922 var source_cell = source_element.data("cell");
918 923 var target_cell = null;
919 924 if (source_cell instanceof IPython.HeadingCell) {
920 925 source_cell.set_level(level);
921 926 } else {
922 927 target_cell = this.insert_cell_below('heading',i);
923 928 var text = source_cell.get_text();
924 929 if (text === source_cell.placeholder) {
925 930 text = '';
926 931 };
927 932 // We must show the editor before setting its contents
928 933 target_cell.set_level(level);
929 934 target_cell.unrender();
930 935 target_cell.set_text(text);
931 936 // make this value the starting point, so that we can only undo
932 937 // to this state, instead of a blank cell
933 938 target_cell.code_mirror.clearHistory();
934 939 source_element.remove();
935 940 this.select(i);
936 941 };
937 942 this.edit_mode();
938 943 this.set_dirty(true);
939 944 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
940 945 {'cell_type':'heading',level:level}
941 946 );
942 947 };
943 948 };
944 949
945 950
946 951 // Cut/Copy/Paste
947 952
948 953 /**
949 954 * Enable UI elements for pasting cells.
950 955 *
951 956 * @method enable_paste
952 957 */
953 958 Notebook.prototype.enable_paste = function () {
954 959 var that = this;
955 960 if (!this.paste_enabled) {
956 961 $('#paste_cell_replace').removeClass('disabled')
957 962 .on('click', function () {that.paste_cell_replace();});
958 963 $('#paste_cell_above').removeClass('disabled')
959 964 .on('click', function () {that.paste_cell_above();});
960 965 $('#paste_cell_below').removeClass('disabled')
961 966 .on('click', function () {that.paste_cell_below();});
962 967 this.paste_enabled = true;
963 968 };
964 969 };
965 970
966 971 /**
967 972 * Disable UI elements for pasting cells.
968 973 *
969 974 * @method disable_paste
970 975 */
971 976 Notebook.prototype.disable_paste = function () {
972 977 if (this.paste_enabled) {
973 978 $('#paste_cell_replace').addClass('disabled').off('click');
974 979 $('#paste_cell_above').addClass('disabled').off('click');
975 980 $('#paste_cell_below').addClass('disabled').off('click');
976 981 this.paste_enabled = false;
977 982 };
978 983 };
979 984
980 985 /**
981 986 * Cut a cell.
982 987 *
983 988 * @method cut_cell
984 989 */
985 990 Notebook.prototype.cut_cell = function () {
986 991 this.copy_cell();
987 992 this.delete_cell();
988 993 }
989 994
990 995 /**
991 996 * Copy a cell.
992 997 *
993 998 * @method copy_cell
994 999 */
995 1000 Notebook.prototype.copy_cell = function () {
996 1001 var cell = this.get_selected_cell();
997 1002 this.clipboard = cell.toJSON();
998 1003 this.enable_paste();
999 1004 };
1000 1005
1001 1006 /**
1002 1007 * Replace the selected cell with a cell in the clipboard.
1003 1008 *
1004 1009 * @method paste_cell_replace
1005 1010 */
1006 1011 Notebook.prototype.paste_cell_replace = function () {
1007 1012 if (this.clipboard !== null && this.paste_enabled) {
1008 1013 var cell_data = this.clipboard;
1009 1014 var new_cell = this.insert_cell_above(cell_data.cell_type);
1010 1015 new_cell.fromJSON(cell_data);
1011 1016 var old_cell = this.get_next_cell(new_cell);
1012 1017 this.delete_cell(this.find_cell_index(old_cell));
1013 1018 this.select(this.find_cell_index(new_cell));
1014 1019 };
1015 1020 };
1016 1021
1017 1022 /**
1018 1023 * Paste a cell from the clipboard above the selected cell.
1019 1024 *
1020 1025 * @method paste_cell_above
1021 1026 */
1022 1027 Notebook.prototype.paste_cell_above = function () {
1023 1028 if (this.clipboard !== null && this.paste_enabled) {
1024 1029 var cell_data = this.clipboard;
1025 1030 var new_cell = this.insert_cell_above(cell_data.cell_type);
1026 1031 new_cell.fromJSON(cell_data);
1027 1032 };
1028 1033 };
1029 1034
1030 1035 /**
1031 1036 * Paste a cell from the clipboard below the selected cell.
1032 1037 *
1033 1038 * @method paste_cell_below
1034 1039 */
1035 1040 Notebook.prototype.paste_cell_below = function () {
1036 1041 if (this.clipboard !== null && this.paste_enabled) {
1037 1042 var cell_data = this.clipboard;
1038 1043 var new_cell = this.insert_cell_below(cell_data.cell_type);
1039 1044 new_cell.fromJSON(cell_data);
1040 1045 };
1041 1046 };
1042 1047
1043 1048 // Split/merge
1044 1049
1045 1050 /**
1046 1051 * Split the selected cell into two, at the cursor.
1047 1052 *
1048 1053 * @method split_cell
1049 1054 */
1050 1055 Notebook.prototype.split_cell = function () {
1051 1056 var mdc = IPython.MarkdownCell;
1052 1057 var rc = IPython.RawCell;
1053 1058 var cell = this.get_selected_cell();
1054 1059 if (cell.is_splittable()) {
1055 1060 var texta = cell.get_pre_cursor();
1056 1061 var textb = cell.get_post_cursor();
1057 1062 if (cell instanceof IPython.CodeCell) {
1058 1063 // In this case the operations keep the notebook in its existing mode
1059 1064 // so we don't need to do any post-op mode changes.
1060 1065 cell.set_text(textb);
1061 1066 var new_cell = this.insert_cell_above('code');
1062 1067 new_cell.set_text(texta);
1063 1068 } else if (((cell instanceof mdc) || (cell instanceof rc)) && !cell.rendered) {
1064 1069 // We know cell is !rendered so we can use set_text.
1065 1070 cell.set_text(textb);
1066 1071 var new_cell = this.insert_cell_above(cell.cell_type);
1067 1072 // Unrender the new cell so we can call set_text.
1068 1073 new_cell.unrender();
1069 1074 new_cell.set_text(texta);
1070 1075 }
1071 1076 };
1072 1077 };
1073 1078
1074 1079 /**
1075 1080 * Combine the selected cell into the cell above it.
1076 1081 *
1077 1082 * @method merge_cell_above
1078 1083 */
1079 1084 Notebook.prototype.merge_cell_above = function () {
1080 1085 var mdc = IPython.MarkdownCell;
1081 1086 var rc = IPython.RawCell;
1082 1087 var index = this.get_selected_index();
1083 1088 var cell = this.get_cell(index);
1084 1089 var render = cell.rendered;
1085 1090 if (!cell.is_mergeable()) {
1086 1091 return;
1087 1092 }
1088 1093 if (index > 0) {
1089 1094 var upper_cell = this.get_cell(index-1);
1090 1095 if (!upper_cell.is_mergeable()) {
1091 1096 return;
1092 1097 }
1093 1098 var upper_text = upper_cell.get_text();
1094 1099 var text = cell.get_text();
1095 1100 if (cell instanceof IPython.CodeCell) {
1096 1101 cell.set_text(upper_text+'\n'+text);
1097 1102 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1098 1103 cell.unrender(); // Must unrender before we set_text.
1099 1104 cell.set_text(upper_text+'\n\n'+text);
1100 1105 if (render) {
1101 1106 // The rendered state of the final cell should match
1102 1107 // that of the original selected cell;
1103 1108 cell.render();
1104 1109 }
1105 1110 };
1106 1111 this.delete_cell(index-1);
1107 1112 this.select(this.find_cell_index(cell));
1108 1113 };
1109 1114 };
1110 1115
1111 1116 /**
1112 1117 * Combine the selected cell into the cell below it.
1113 1118 *
1114 1119 * @method merge_cell_below
1115 1120 */
1116 1121 Notebook.prototype.merge_cell_below = function () {
1117 1122 var mdc = IPython.MarkdownCell;
1118 1123 var rc = IPython.RawCell;
1119 1124 var index = this.get_selected_index();
1120 1125 var cell = this.get_cell(index);
1121 1126 var render = cell.rendered;
1122 1127 if (!cell.is_mergeable()) {
1123 1128 return;
1124 1129 }
1125 1130 if (index < this.ncells()-1) {
1126 1131 var lower_cell = this.get_cell(index+1);
1127 1132 if (!lower_cell.is_mergeable()) {
1128 1133 return;
1129 1134 }
1130 1135 var lower_text = lower_cell.get_text();
1131 1136 var text = cell.get_text();
1132 1137 if (cell instanceof IPython.CodeCell) {
1133 1138 cell.set_text(text+'\n'+lower_text);
1134 1139 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1135 1140 cell.unrender(); // Must unrender before we set_text.
1136 1141 cell.set_text(text+'\n\n'+lower_text);
1137 1142 if (render) {
1138 1143 // The rendered state of the final cell should match
1139 1144 // that of the original selected cell;
1140 1145 cell.render();
1141 1146 }
1142 1147 };
1143 1148 this.delete_cell(index+1);
1144 1149 this.select(this.find_cell_index(cell));
1145 1150 };
1146 1151 };
1147 1152
1148 1153
1149 1154 // Cell collapsing and output clearing
1150 1155
1151 1156 /**
1152 1157 * Hide a cell's output.
1153 1158 *
1154 1159 * @method collapse
1155 1160 * @param {Number} index A cell's numeric index
1156 1161 */
1157 1162 Notebook.prototype.collapse = function (index) {
1158 1163 var i = this.index_or_selected(index);
1159 1164 this.get_cell(i).collapse();
1160 1165 this.set_dirty(true);
1161 1166 };
1162 1167
1163 1168 /**
1164 1169 * Show a cell's output.
1165 1170 *
1166 1171 * @method expand
1167 1172 * @param {Number} index A cell's numeric index
1168 1173 */
1169 1174 Notebook.prototype.expand = function (index) {
1170 1175 var i = this.index_or_selected(index);
1171 1176 this.get_cell(i).expand();
1172 1177 this.set_dirty(true);
1173 1178 };
1174 1179
1175 1180 /** Toggle whether a cell's output is collapsed or expanded.
1176 1181 *
1177 1182 * @method toggle_output
1178 1183 * @param {Number} index A cell's numeric index
1179 1184 */
1180 1185 Notebook.prototype.toggle_output = function (index) {
1181 1186 var i = this.index_or_selected(index);
1182 1187 this.get_cell(i).toggle_output();
1183 1188 this.set_dirty(true);
1184 1189 };
1185 1190
1186 1191 /**
1187 1192 * Toggle a scrollbar for long cell outputs.
1188 1193 *
1189 1194 * @method toggle_output_scroll
1190 1195 * @param {Number} index A cell's numeric index
1191 1196 */
1192 1197 Notebook.prototype.toggle_output_scroll = function (index) {
1193 1198 var i = this.index_or_selected(index);
1194 1199 this.get_cell(i).toggle_output_scroll();
1195 1200 };
1196 1201
1197 1202 /**
1198 1203 * Hide each code cell's output area.
1199 1204 *
1200 1205 * @method collapse_all_output
1201 1206 */
1202 1207 Notebook.prototype.collapse_all_output = function () {
1203 1208 var ncells = this.ncells();
1204 1209 var cells = this.get_cells();
1205 1210 for (var i=0; i<ncells; i++) {
1206 1211 if (cells[i] instanceof IPython.CodeCell) {
1207 1212 cells[i].output_area.collapse();
1208 1213 }
1209 1214 };
1210 1215 // this should not be set if the `collapse` key is removed from nbformat
1211 1216 this.set_dirty(true);
1212 1217 };
1213 1218
1214 1219 /**
1215 1220 * Expand each code cell's output area, and add a scrollbar for long output.
1216 1221 *
1217 1222 * @method scroll_all_output
1218 1223 */
1219 1224 Notebook.prototype.scroll_all_output = function () {
1220 1225 var ncells = this.ncells();
1221 1226 var cells = this.get_cells();
1222 1227 for (var i=0; i<ncells; i++) {
1223 1228 if (cells[i] instanceof IPython.CodeCell) {
1224 1229 cells[i].output_area.expand();
1225 1230 cells[i].output_area.scroll_if_long();
1226 1231 }
1227 1232 };
1228 1233 // this should not be set if the `collapse` key is removed from nbformat
1229 1234 this.set_dirty(true);
1230 1235 };
1231 1236
1232 1237 /**
1233 1238 * Expand each code cell's output area, and remove scrollbars.
1234 1239 *
1235 1240 * @method expand_all_output
1236 1241 */
1237 1242 Notebook.prototype.expand_all_output = function () {
1238 1243 var ncells = this.ncells();
1239 1244 var cells = this.get_cells();
1240 1245 for (var i=0; i<ncells; i++) {
1241 1246 if (cells[i] instanceof IPython.CodeCell) {
1242 1247 cells[i].output_area.expand();
1243 1248 cells[i].output_area.unscroll_area();
1244 1249 }
1245 1250 };
1246 1251 // this should not be set if the `collapse` key is removed from nbformat
1247 1252 this.set_dirty(true);
1248 1253 };
1249 1254
1250 1255 /**
1251 1256 * Clear each code cell's output area.
1252 1257 *
1253 1258 * @method clear_all_output
1254 1259 */
1255 1260 Notebook.prototype.clear_all_output = function () {
1256 1261 var ncells = this.ncells();
1257 1262 var cells = this.get_cells();
1258 1263 for (var i=0; i<ncells; i++) {
1259 1264 if (cells[i] instanceof IPython.CodeCell) {
1260 1265 cells[i].clear_output();
1261 1266 // Make all In[] prompts blank, as well
1262 1267 // TODO: make this configurable (via checkbox?)
1263 1268 cells[i].set_input_prompt();
1264 1269 }
1265 1270 };
1266 1271 this.set_dirty(true);
1267 1272 };
1268 1273
1269 1274
1270 1275 // Other cell functions: line numbers, ...
1271 1276
1272 1277 /**
1273 1278 * Toggle line numbers in the selected cell's input area.
1274 1279 *
1275 1280 * @method cell_toggle_line_numbers
1276 1281 */
1277 1282 Notebook.prototype.cell_toggle_line_numbers = function() {
1278 1283 this.get_selected_cell().toggle_line_numbers();
1279 1284 };
1280 1285
1281 1286 // Session related things
1282 1287
1283 1288 /**
1284 1289 * Start a new session and set it on each code cell.
1285 1290 *
1286 1291 * @method start_session
1287 1292 */
1288 1293 Notebook.prototype.start_session = function () {
1289 1294 this.session = new IPython.Session(this.notebook_name, this.notebook_path, this);
1290 1295 this.session.start($.proxy(this._session_started, this));
1291 1296 };
1292 1297
1293 1298
1294 1299 /**
1295 1300 * Once a session is started, link the code cells to the kernel
1296 1301 *
1297 1302 */
1298 1303 Notebook.prototype._session_started = function(){
1299 1304 this.kernel = this.session.kernel;
1300 1305 var ncells = this.ncells();
1301 1306 for (var i=0; i<ncells; i++) {
1302 1307 var cell = this.get_cell(i);
1303 1308 if (cell instanceof IPython.CodeCell) {
1304 1309 cell.set_kernel(this.session.kernel);
1305 1310 };
1306 1311 };
1307 1312 };
1308 1313
1309 1314 /**
1310 1315 * Prompt the user to restart the IPython kernel.
1311 1316 *
1312 1317 * @method restart_kernel
1313 1318 */
1314 1319 Notebook.prototype.restart_kernel = function () {
1315 1320 var that = this;
1316 1321 IPython.dialog.modal({
1317 1322 title : "Restart kernel or continue running?",
1318 1323 body : $("<p/>").html(
1319 1324 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1320 1325 ),
1321 1326 buttons : {
1322 1327 "Continue running" : {},
1323 1328 "Restart" : {
1324 1329 "class" : "btn-danger",
1325 1330 "click" : function() {
1326 1331 that.session.restart_kernel();
1327 1332 }
1328 1333 }
1329 1334 }
1330 1335 });
1331 1336 };
1332 1337
1333 1338 /**
1334 1339 * Run the selected cell.
1335 1340 *
1336 1341 * Execute or render cell outputs.
1337 1342 *
1338 1343 * @method execute_selected_cell
1339 1344 * @param {Object} options Customize post-execution behavior
1340 1345 */
1341 1346 Notebook.prototype.execute_selected_cell = function (mode) {
1342 1347 // mode = shift, ctrl, alt
1343 1348 mode = mode || 'shift'
1344 1349 var cell = this.get_selected_cell();
1345 1350 var cell_index = this.find_cell_index(cell);
1346 1351
1347 1352 cell.execute();
1348 1353
1349 1354 // If we are at the end always insert a new cell and return
1350 1355 if (cell_index === (this.ncells()-1) && mode !== 'shift') {
1351 1356 this.insert_cell_below('code');
1352 1357 this.select(cell_index+1);
1353 1358 this.edit_mode();
1354 1359 this.scroll_to_bottom();
1355 1360 this.set_dirty(true);
1356 1361 return;
1357 1362 }
1358 1363
1359 1364 if (mode === 'shift') {
1360 1365 this.command_mode();
1361 1366 } else if (mode === 'ctrl') {
1362 1367 this.select(cell_index+1);
1363 1368 this.get_cell(cell_index+1).focus_cell();
1364 1369 } else if (mode === 'alt') {
1365 1370 // Only insert a new cell, if we ended up in an already populated cell
1366 1371 var next_text = this.get_cell(cell_index+1).get_text();
1367 1372 if (/\S/.test(next_text) === true) {
1368 1373 this.insert_cell_below('code');
1369 1374 }
1370 1375 this.select(cell_index+1);
1371 1376 this.edit_mode();
1372 1377 }
1373 1378 this.set_dirty(true);
1374 1379 };
1375 1380
1376 1381
1377 1382 /**
1378 1383 * Execute all cells below the selected cell.
1379 1384 *
1380 1385 * @method execute_cells_below
1381 1386 */
1382 1387 Notebook.prototype.execute_cells_below = function () {
1383 1388 this.execute_cell_range(this.get_selected_index(), this.ncells());
1384 1389 this.scroll_to_bottom();
1385 1390 };
1386 1391
1387 1392 /**
1388 1393 * Execute all cells above the selected cell.
1389 1394 *
1390 1395 * @method execute_cells_above
1391 1396 */
1392 1397 Notebook.prototype.execute_cells_above = function () {
1393 1398 this.execute_cell_range(0, this.get_selected_index());
1394 1399 };
1395 1400
1396 1401 /**
1397 1402 * Execute all cells.
1398 1403 *
1399 1404 * @method execute_all_cells
1400 1405 */
1401 1406 Notebook.prototype.execute_all_cells = function () {
1402 1407 this.execute_cell_range(0, this.ncells());
1403 1408 this.scroll_to_bottom();
1404 1409 };
1405 1410
1406 1411 /**
1407 1412 * Execute a contiguous range of cells.
1408 1413 *
1409 1414 * @method execute_cell_range
1410 1415 * @param {Number} start Index of the first cell to execute (inclusive)
1411 1416 * @param {Number} end Index of the last cell to execute (exclusive)
1412 1417 */
1413 1418 Notebook.prototype.execute_cell_range = function (start, end) {
1414 1419 for (var i=start; i<end; i++) {
1415 1420 this.select(i);
1416 1421 this.execute_selected_cell({add_new:false});
1417 1422 };
1418 1423 };
1419 1424
1420 1425 // Persistance and loading
1421 1426
1422 1427 /**
1423 1428 * Getter method for this notebook's name.
1424 1429 *
1425 1430 * @method get_notebook_name
1426 1431 * @return {String} This notebook's name
1427 1432 */
1428 1433 Notebook.prototype.get_notebook_name = function () {
1429 1434 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1430 1435 return nbname;
1431 1436 };
1432 1437
1433 1438 /**
1434 1439 * Setter method for this notebook's name.
1435 1440 *
1436 1441 * @method set_notebook_name
1437 1442 * @param {String} name A new name for this notebook
1438 1443 */
1439 1444 Notebook.prototype.set_notebook_name = function (name) {
1440 1445 this.notebook_name = name;
1441 1446 };
1442 1447
1443 1448 /**
1444 1449 * Check that a notebook's name is valid.
1445 1450 *
1446 1451 * @method test_notebook_name
1447 1452 * @param {String} nbname A name for this notebook
1448 1453 * @return {Boolean} True if the name is valid, false if invalid
1449 1454 */
1450 1455 Notebook.prototype.test_notebook_name = function (nbname) {
1451 1456 nbname = nbname || '';
1452 1457 if (this.notebook_name_blacklist_re.test(nbname) == false && nbname.length>0) {
1453 1458 return true;
1454 1459 } else {
1455 1460 return false;
1456 1461 };
1457 1462 };
1458 1463
1459 1464 /**
1460 1465 * Load a notebook from JSON (.ipynb).
1461 1466 *
1462 1467 * This currently handles one worksheet: others are deleted.
1463 1468 *
1464 1469 * @method fromJSON
1465 1470 * @param {Object} data JSON representation of a notebook
1466 1471 */
1467 1472 Notebook.prototype.fromJSON = function (data) {
1468 1473 var content = data.content;
1469 1474 var ncells = this.ncells();
1470 1475 var i;
1471 1476 for (i=0; i<ncells; i++) {
1472 1477 // Always delete cell 0 as they get renumbered as they are deleted.
1473 1478 this.delete_cell(0);
1474 1479 };
1475 1480 // Save the metadata and name.
1476 1481 this.metadata = content.metadata;
1477 1482 this.notebook_name = data.name;
1478 1483 // Only handle 1 worksheet for now.
1479 1484 var worksheet = content.worksheets[0];
1480 1485 if (worksheet !== undefined) {
1481 1486 if (worksheet.metadata) {
1482 1487 this.worksheet_metadata = worksheet.metadata;
1483 1488 }
1484 1489 var new_cells = worksheet.cells;
1485 1490 ncells = new_cells.length;
1486 1491 var cell_data = null;
1487 1492 var new_cell = null;
1488 1493 for (i=0; i<ncells; i++) {
1489 1494 cell_data = new_cells[i];
1490 1495 // VERSIONHACK: plaintext -> raw
1491 1496 // handle never-released plaintext name for raw cells
1492 1497 if (cell_data.cell_type === 'plaintext'){
1493 1498 cell_data.cell_type = 'raw';
1494 1499 }
1495 1500
1496 1501 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1497 1502 new_cell.fromJSON(cell_data);
1498 1503 };
1499 1504 };
1500 1505 if (content.worksheets.length > 1) {
1501 1506 IPython.dialog.modal({
1502 1507 title : "Multiple worksheets",
1503 1508 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1504 1509 "but this version of IPython can only handle the first. " +
1505 1510 "If you save this notebook, worksheets after the first will be lost.",
1506 1511 buttons : {
1507 1512 OK : {
1508 1513 class : "btn-danger"
1509 1514 }
1510 1515 }
1511 1516 });
1512 1517 }
1513 1518 };
1514 1519
1515 1520 /**
1516 1521 * Dump this notebook into a JSON-friendly object.
1517 1522 *
1518 1523 * @method toJSON
1519 1524 * @return {Object} A JSON-friendly representation of this notebook.
1520 1525 */
1521 1526 Notebook.prototype.toJSON = function () {
1522 1527 var cells = this.get_cells();
1523 1528 var ncells = cells.length;
1524 1529 var cell_array = new Array(ncells);
1525 1530 for (var i=0; i<ncells; i++) {
1526 1531 cell_array[i] = cells[i].toJSON();
1527 1532 };
1528 1533 var data = {
1529 1534 // Only handle 1 worksheet for now.
1530 1535 worksheets : [{
1531 1536 cells: cell_array,
1532 1537 metadata: this.worksheet_metadata
1533 1538 }],
1534 1539 metadata : this.metadata
1535 1540 };
1536 1541 return data;
1537 1542 };
1538 1543
1539 1544 /**
1540 1545 * Start an autosave timer, for periodically saving the notebook.
1541 1546 *
1542 1547 * @method set_autosave_interval
1543 1548 * @param {Integer} interval the autosave interval in milliseconds
1544 1549 */
1545 1550 Notebook.prototype.set_autosave_interval = function (interval) {
1546 1551 var that = this;
1547 1552 // clear previous interval, so we don't get simultaneous timers
1548 1553 if (this.autosave_timer) {
1549 1554 clearInterval(this.autosave_timer);
1550 1555 }
1551 1556
1552 1557 this.autosave_interval = this.minimum_autosave_interval = interval;
1553 1558 if (interval) {
1554 1559 this.autosave_timer = setInterval(function() {
1555 1560 if (that.dirty) {
1556 1561 that.save_notebook();
1557 1562 }
1558 1563 }, interval);
1559 1564 $([IPython.events]).trigger("autosave_enabled.Notebook", interval);
1560 1565 } else {
1561 1566 this.autosave_timer = null;
1562 1567 $([IPython.events]).trigger("autosave_disabled.Notebook");
1563 1568 };
1564 1569 };
1565 1570
1566 1571 /**
1567 1572 * Save this notebook on the server.
1568 1573 *
1569 1574 * @method save_notebook
1570 1575 */
1571 1576 Notebook.prototype.save_notebook = function (extra_settings) {
1572 1577 // Create a JSON model to be sent to the server.
1573 1578 var model = {};
1574 1579 model.name = this.notebook_name;
1575 1580 model.path = this.notebook_path;
1576 1581 model.content = this.toJSON();
1577 1582 model.content.nbformat = this.nbformat;
1578 1583 model.content.nbformat_minor = this.nbformat_minor;
1579 1584 // time the ajax call for autosave tuning purposes.
1580 1585 var start = new Date().getTime();
1581 1586 // We do the call with settings so we can set cache to false.
1582 1587 var settings = {
1583 1588 processData : false,
1584 1589 cache : false,
1585 1590 type : "PUT",
1586 1591 data : JSON.stringify(model),
1587 1592 headers : {'Content-Type': 'application/json'},
1588 1593 success : $.proxy(this.save_notebook_success, this, start),
1589 1594 error : $.proxy(this.save_notebook_error, this)
1590 1595 };
1591 1596 if (extra_settings) {
1592 1597 for (var key in extra_settings) {
1593 1598 settings[key] = extra_settings[key];
1594 1599 }
1595 1600 }
1596 1601 $([IPython.events]).trigger('notebook_saving.Notebook');
1597 1602 var url = utils.url_join_encode(
1598 1603 this._baseProjectUrl,
1599 1604 'api/notebooks',
1600 1605 this.notebook_path,
1601 1606 this.notebook_name
1602 1607 );
1603 1608 $.ajax(url, settings);
1604 1609 };
1605 1610
1606 1611 /**
1607 1612 * Success callback for saving a notebook.
1608 1613 *
1609 1614 * @method save_notebook_success
1610 1615 * @param {Integer} start the time when the save request started
1611 1616 * @param {Object} data JSON representation of a notebook
1612 1617 * @param {String} status Description of response status
1613 1618 * @param {jqXHR} xhr jQuery Ajax object
1614 1619 */
1615 1620 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1616 1621 this.set_dirty(false);
1617 1622 $([IPython.events]).trigger('notebook_saved.Notebook');
1618 1623 this._update_autosave_interval(start);
1619 1624 if (this._checkpoint_after_save) {
1620 1625 this.create_checkpoint();
1621 1626 this._checkpoint_after_save = false;
1622 1627 };
1623 1628 };
1624 1629
1625 1630 /**
1626 1631 * update the autosave interval based on how long the last save took
1627 1632 *
1628 1633 * @method _update_autosave_interval
1629 1634 * @param {Integer} timestamp when the save request started
1630 1635 */
1631 1636 Notebook.prototype._update_autosave_interval = function (start) {
1632 1637 var duration = (new Date().getTime() - start);
1633 1638 if (this.autosave_interval) {
1634 1639 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1635 1640 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1636 1641 // round to 10 seconds, otherwise we will be setting a new interval too often
1637 1642 interval = 10000 * Math.round(interval / 10000);
1638 1643 // set new interval, if it's changed
1639 1644 if (interval != this.autosave_interval) {
1640 1645 this.set_autosave_interval(interval);
1641 1646 }
1642 1647 }
1643 1648 };
1644 1649
1645 1650 /**
1646 1651 * Failure callback for saving a notebook.
1647 1652 *
1648 1653 * @method save_notebook_error
1649 1654 * @param {jqXHR} xhr jQuery Ajax object
1650 1655 * @param {String} status Description of response status
1651 1656 * @param {String} error HTTP error message
1652 1657 */
1653 1658 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1654 1659 $([IPython.events]).trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1655 1660 };
1656 1661
1657 1662 Notebook.prototype.new_notebook = function(){
1658 1663 var path = this.notebook_path;
1659 1664 var base_project_url = this._baseProjectUrl;
1660 1665 var settings = {
1661 1666 processData : false,
1662 1667 cache : false,
1663 1668 type : "POST",
1664 1669 dataType : "json",
1665 1670 async : false,
1666 1671 success : function (data, status, xhr){
1667 1672 var notebook_name = data.name;
1668 1673 window.open(
1669 1674 utils.url_join_encode(
1670 1675 base_project_url,
1671 1676 'notebooks',
1672 1677 path,
1673 1678 notebook_name
1674 1679 ),
1675 1680 '_blank'
1676 1681 );
1677 1682 }
1678 1683 };
1679 1684 var url = utils.url_join_encode(
1680 1685 base_project_url,
1681 1686 'api/notebooks',
1682 1687 path
1683 1688 );
1684 1689 $.ajax(url,settings);
1685 1690 };
1686 1691
1687 1692
1688 1693 Notebook.prototype.copy_notebook = function(){
1689 1694 var path = this.notebook_path;
1690 1695 var base_project_url = this._baseProjectUrl;
1691 1696 var settings = {
1692 1697 processData : false,
1693 1698 cache : false,
1694 1699 type : "POST",
1695 1700 dataType : "json",
1696 1701 data : JSON.stringify({copy_from : this.notebook_name}),
1697 1702 async : false,
1698 1703 success : function (data, status, xhr) {
1699 1704 window.open(utils.url_join_encode(
1700 1705 base_project_url,
1701 1706 'notebooks',
1702 1707 data.path,
1703 1708 data.name
1704 1709 ), '_blank');
1705 1710 }
1706 1711 };
1707 1712 var url = utils.url_join_encode(
1708 1713 base_project_url,
1709 1714 'api/notebooks',
1710 1715 path
1711 1716 );
1712 1717 $.ajax(url,settings);
1713 1718 };
1714 1719
1715 1720 Notebook.prototype.rename = function (nbname) {
1716 1721 var that = this;
1717 1722 var data = {name: nbname + '.ipynb'};
1718 1723 var settings = {
1719 1724 processData : false,
1720 1725 cache : false,
1721 1726 type : "PATCH",
1722 1727 data : JSON.stringify(data),
1723 1728 dataType: "json",
1724 1729 headers : {'Content-Type': 'application/json'},
1725 1730 success : $.proxy(that.rename_success, this),
1726 1731 error : $.proxy(that.rename_error, this)
1727 1732 };
1728 1733 $([IPython.events]).trigger('rename_notebook.Notebook', data);
1729 1734 var url = utils.url_join_encode(
1730 1735 this._baseProjectUrl,
1731 1736 'api/notebooks',
1732 1737 this.notebook_path,
1733 1738 this.notebook_name
1734 1739 );
1735 1740 $.ajax(url, settings);
1736 1741 };
1737 1742
1738 1743
1739 1744 Notebook.prototype.rename_success = function (json, status, xhr) {
1740 1745 this.notebook_name = json.name;
1741 1746 var name = this.notebook_name;
1742 1747 var path = json.path;
1743 1748 this.session.rename_notebook(name, path);
1744 1749 $([IPython.events]).trigger('notebook_renamed.Notebook', json);
1745 1750 }
1746 1751
1747 1752 Notebook.prototype.rename_error = function (xhr, status, error) {
1748 1753 var that = this;
1749 1754 var dialog = $('<div/>').append(
1750 1755 $("<p/>").addClass("rename-message")
1751 1756 .html('This notebook name already exists.')
1752 1757 )
1753 1758 $([IPython.events]).trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
1754 1759 IPython.dialog.modal({
1755 1760 title: "Notebook Rename Error!",
1756 1761 body: dialog,
1757 1762 buttons : {
1758 1763 "Cancel": {},
1759 1764 "OK": {
1760 1765 class: "btn-primary",
1761 1766 click: function () {
1762 1767 IPython.save_widget.rename_notebook();
1763 1768 }}
1764 1769 },
1765 1770 open : function (event, ui) {
1766 1771 var that = $(this);
1767 1772 // Upon ENTER, click the OK button.
1768 1773 that.find('input[type="text"]').keydown(function (event, ui) {
1769 1774 if (event.which === utils.keycodes.ENTER) {
1770 1775 that.find('.btn-primary').first().click();
1771 1776 }
1772 1777 });
1773 1778 that.find('input[type="text"]').focus();
1774 1779 }
1775 1780 });
1776 1781 }
1777 1782
1778 1783 /**
1779 1784 * Request a notebook's data from the server.
1780 1785 *
1781 1786 * @method load_notebook
1782 1787 * @param {String} notebook_name and path A notebook to load
1783 1788 */
1784 1789 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
1785 1790 var that = this;
1786 1791 this.notebook_name = notebook_name;
1787 1792 this.notebook_path = notebook_path;
1788 1793 // We do the call with settings so we can set cache to false.
1789 1794 var settings = {
1790 1795 processData : false,
1791 1796 cache : false,
1792 1797 type : "GET",
1793 1798 dataType : "json",
1794 1799 success : $.proxy(this.load_notebook_success,this),
1795 1800 error : $.proxy(this.load_notebook_error,this),
1796 1801 };
1797 1802 $([IPython.events]).trigger('notebook_loading.Notebook');
1798 1803 var url = utils.url_join_encode(
1799 1804 this._baseProjectUrl,
1800 1805 'api/notebooks',
1801 1806 this.notebook_path,
1802 1807 this.notebook_name
1803 1808 );
1804 1809 $.ajax(url, settings);
1805 1810 };
1806 1811
1807 1812 /**
1808 1813 * Success callback for loading a notebook from the server.
1809 1814 *
1810 1815 * Load notebook data from the JSON response.
1811 1816 *
1812 1817 * @method load_notebook_success
1813 1818 * @param {Object} data JSON representation of a notebook
1814 1819 * @param {String} status Description of response status
1815 1820 * @param {jqXHR} xhr jQuery Ajax object
1816 1821 */
1817 1822 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
1818 1823 this.fromJSON(data);
1819 1824 if (this.ncells() === 0) {
1820 1825 this.insert_cell_below('code');
1821 1826 this.select(0);
1822 1827 this.edit_mode();
1823 1828 } else {
1824 1829 this.select(0);
1825 1830 this.command_mode();
1826 1831 };
1827 1832 this.set_dirty(false);
1828 1833 this.scroll_to_top();
1829 1834 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
1830 1835 var msg = "This notebook has been converted from an older " +
1831 1836 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
1832 1837 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
1833 1838 "newer notebook format will be used and older versions of IPython " +
1834 1839 "may not be able to read it. To keep the older version, close the " +
1835 1840 "notebook without saving it.";
1836 1841 IPython.dialog.modal({
1837 1842 title : "Notebook converted",
1838 1843 body : msg,
1839 1844 buttons : {
1840 1845 OK : {
1841 1846 class : "btn-primary"
1842 1847 }
1843 1848 }
1844 1849 });
1845 1850 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
1846 1851 var that = this;
1847 1852 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
1848 1853 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
1849 1854 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
1850 1855 this_vs + ". You can still work with this notebook, but some features " +
1851 1856 "introduced in later notebook versions may not be available."
1852 1857
1853 1858 IPython.dialog.modal({
1854 1859 title : "Newer Notebook",
1855 1860 body : msg,
1856 1861 buttons : {
1857 1862 OK : {
1858 1863 class : "btn-danger"
1859 1864 }
1860 1865 }
1861 1866 });
1862 1867
1863 1868 }
1864 1869
1865 1870 // Create the session after the notebook is completely loaded to prevent
1866 1871 // code execution upon loading, which is a security risk.
1867 1872 if (this.session == null) {
1868 1873 this.start_session();
1869 1874 }
1870 1875 // load our checkpoint list
1871 1876 this.list_checkpoints();
1872 1877
1873 1878 // load toolbar state
1874 1879 if (this.metadata.celltoolbar) {
1875 1880 IPython.CellToolbar.global_show();
1876 1881 IPython.CellToolbar.activate_preset(this.metadata.celltoolbar);
1877 1882 }
1878 1883
1879 1884 $([IPython.events]).trigger('notebook_loaded.Notebook');
1880 1885 };
1881 1886
1882 1887 /**
1883 1888 * Failure callback for loading a notebook from the server.
1884 1889 *
1885 1890 * @method load_notebook_error
1886 1891 * @param {jqXHR} xhr jQuery Ajax object
1887 1892 * @param {String} status Description of response status
1888 1893 * @param {String} error HTTP error message
1889 1894 */
1890 1895 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
1891 1896 $([IPython.events]).trigger('notebook_load_failed.Notebook', [xhr, status, error]);
1892 1897 if (xhr.status === 400) {
1893 1898 var msg = error;
1894 1899 } else if (xhr.status === 500) {
1895 1900 var msg = "An unknown error occurred while loading this notebook. " +
1896 1901 "This version can load notebook formats " +
1897 1902 "v" + this.nbformat + " or earlier.";
1898 1903 }
1899 1904 IPython.dialog.modal({
1900 1905 title: "Error loading notebook",
1901 1906 body : msg,
1902 1907 buttons : {
1903 1908 "OK": {}
1904 1909 }
1905 1910 });
1906 1911 }
1907 1912
1908 1913 /********************* checkpoint-related *********************/
1909 1914
1910 1915 /**
1911 1916 * Save the notebook then immediately create a checkpoint.
1912 1917 *
1913 1918 * @method save_checkpoint
1914 1919 */
1915 1920 Notebook.prototype.save_checkpoint = function () {
1916 1921 this._checkpoint_after_save = true;
1917 1922 this.save_notebook();
1918 1923 };
1919 1924
1920 1925 /**
1921 1926 * Add a checkpoint for this notebook.
1922 1927 * for use as a callback from checkpoint creation.
1923 1928 *
1924 1929 * @method add_checkpoint
1925 1930 */
1926 1931 Notebook.prototype.add_checkpoint = function (checkpoint) {
1927 1932 var found = false;
1928 1933 for (var i = 0; i < this.checkpoints.length; i++) {
1929 1934 var existing = this.checkpoints[i];
1930 1935 if (existing.id == checkpoint.id) {
1931 1936 found = true;
1932 1937 this.checkpoints[i] = checkpoint;
1933 1938 break;
1934 1939 }
1935 1940 }
1936 1941 if (!found) {
1937 1942 this.checkpoints.push(checkpoint);
1938 1943 }
1939 1944 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
1940 1945 };
1941 1946
1942 1947 /**
1943 1948 * List checkpoints for this notebook.
1944 1949 *
1945 1950 * @method list_checkpoints
1946 1951 */
1947 1952 Notebook.prototype.list_checkpoints = function () {
1948 1953 var url = utils.url_join_encode(
1949 1954 this._baseProjectUrl,
1950 1955 'api/notebooks',
1951 1956 this.notebook_path,
1952 1957 this.notebook_name,
1953 1958 'checkpoints'
1954 1959 );
1955 1960 $.get(url).done(
1956 1961 $.proxy(this.list_checkpoints_success, this)
1957 1962 ).fail(
1958 1963 $.proxy(this.list_checkpoints_error, this)
1959 1964 );
1960 1965 };
1961 1966
1962 1967 /**
1963 1968 * Success callback for listing checkpoints.
1964 1969 *
1965 1970 * @method list_checkpoint_success
1966 1971 * @param {Object} data JSON representation of a checkpoint
1967 1972 * @param {String} status Description of response status
1968 1973 * @param {jqXHR} xhr jQuery Ajax object
1969 1974 */
1970 1975 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
1971 1976 var data = $.parseJSON(data);
1972 1977 this.checkpoints = data;
1973 1978 if (data.length) {
1974 1979 this.last_checkpoint = data[data.length - 1];
1975 1980 } else {
1976 1981 this.last_checkpoint = null;
1977 1982 }
1978 1983 $([IPython.events]).trigger('checkpoints_listed.Notebook', [data]);
1979 1984 };
1980 1985
1981 1986 /**
1982 1987 * Failure callback for listing a checkpoint.
1983 1988 *
1984 1989 * @method list_checkpoint_error
1985 1990 * @param {jqXHR} xhr jQuery Ajax object
1986 1991 * @param {String} status Description of response status
1987 1992 * @param {String} error_msg HTTP error message
1988 1993 */
1989 1994 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
1990 1995 $([IPython.events]).trigger('list_checkpoints_failed.Notebook');
1991 1996 };
1992 1997
1993 1998 /**
1994 1999 * Create a checkpoint of this notebook on the server from the most recent save.
1995 2000 *
1996 2001 * @method create_checkpoint
1997 2002 */
1998 2003 Notebook.prototype.create_checkpoint = function () {
1999 2004 var url = utils.url_join_encode(
2000 2005 this._baseProjectUrl,
2001 2006 'api/notebooks',
2002 2007 this.notebookPath(),
2003 2008 this.notebook_name,
2004 2009 'checkpoints'
2005 2010 );
2006 2011 $.post(url).done(
2007 2012 $.proxy(this.create_checkpoint_success, this)
2008 2013 ).fail(
2009 2014 $.proxy(this.create_checkpoint_error, this)
2010 2015 );
2011 2016 };
2012 2017
2013 2018 /**
2014 2019 * Success callback for creating a checkpoint.
2015 2020 *
2016 2021 * @method create_checkpoint_success
2017 2022 * @param {Object} data JSON representation of a checkpoint
2018 2023 * @param {String} status Description of response status
2019 2024 * @param {jqXHR} xhr jQuery Ajax object
2020 2025 */
2021 2026 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2022 2027 var data = $.parseJSON(data);
2023 2028 this.add_checkpoint(data);
2024 2029 $([IPython.events]).trigger('checkpoint_created.Notebook', data);
2025 2030 };
2026 2031
2027 2032 /**
2028 2033 * Failure callback for creating a checkpoint.
2029 2034 *
2030 2035 * @method create_checkpoint_error
2031 2036 * @param {jqXHR} xhr jQuery Ajax object
2032 2037 * @param {String} status Description of response status
2033 2038 * @param {String} error_msg HTTP error message
2034 2039 */
2035 2040 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2036 2041 $([IPython.events]).trigger('checkpoint_failed.Notebook');
2037 2042 };
2038 2043
2039 2044 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2040 2045 var that = this;
2041 2046 var checkpoint = checkpoint || this.last_checkpoint;
2042 2047 if ( ! checkpoint ) {
2043 2048 console.log("restore dialog, but no checkpoint to restore to!");
2044 2049 return;
2045 2050 }
2046 2051 var body = $('<div/>').append(
2047 2052 $('<p/>').addClass("p-space").text(
2048 2053 "Are you sure you want to revert the notebook to " +
2049 2054 "the latest checkpoint?"
2050 2055 ).append(
2051 2056 $("<strong/>").text(
2052 2057 " This cannot be undone."
2053 2058 )
2054 2059 )
2055 2060 ).append(
2056 2061 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2057 2062 ).append(
2058 2063 $('<p/>').addClass("p-space").text(
2059 2064 Date(checkpoint.last_modified)
2060 2065 ).css("text-align", "center")
2061 2066 );
2062 2067
2063 2068 IPython.dialog.modal({
2064 2069 title : "Revert notebook to checkpoint",
2065 2070 body : body,
2066 2071 buttons : {
2067 2072 Revert : {
2068 2073 class : "btn-danger",
2069 2074 click : function () {
2070 2075 that.restore_checkpoint(checkpoint.id);
2071 2076 }
2072 2077 },
2073 2078 Cancel : {}
2074 2079 }
2075 2080 });
2076 2081 }
2077 2082
2078 2083 /**
2079 2084 * Restore the notebook to a checkpoint state.
2080 2085 *
2081 2086 * @method restore_checkpoint
2082 2087 * @param {String} checkpoint ID
2083 2088 */
2084 2089 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2085 2090 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2086 2091 var url = utils.url_join_encode(
2087 2092 this._baseProjectUrl,
2088 2093 'api/notebooks',
2089 2094 this.notebookPath(),
2090 2095 this.notebook_name,
2091 2096 'checkpoints',
2092 2097 checkpoint
2093 2098 );
2094 2099 $.post(url).done(
2095 2100 $.proxy(this.restore_checkpoint_success, this)
2096 2101 ).fail(
2097 2102 $.proxy(this.restore_checkpoint_error, this)
2098 2103 );
2099 2104 };
2100 2105
2101 2106 /**
2102 2107 * Success callback for restoring a notebook to a checkpoint.
2103 2108 *
2104 2109 * @method restore_checkpoint_success
2105 2110 * @param {Object} data (ignored, should be empty)
2106 2111 * @param {String} status Description of response status
2107 2112 * @param {jqXHR} xhr jQuery Ajax object
2108 2113 */
2109 2114 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2110 2115 $([IPython.events]).trigger('checkpoint_restored.Notebook');
2111 2116 this.load_notebook(this.notebook_name, this.notebook_path);
2112 2117 };
2113 2118
2114 2119 /**
2115 2120 * Failure callback for restoring a notebook to a checkpoint.
2116 2121 *
2117 2122 * @method restore_checkpoint_error
2118 2123 * @param {jqXHR} xhr jQuery Ajax object
2119 2124 * @param {String} status Description of response status
2120 2125 * @param {String} error_msg HTTP error message
2121 2126 */
2122 2127 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2123 2128 $([IPython.events]).trigger('checkpoint_restore_failed.Notebook');
2124 2129 };
2125 2130
2126 2131 /**
2127 2132 * Delete a notebook checkpoint.
2128 2133 *
2129 2134 * @method delete_checkpoint
2130 2135 * @param {String} checkpoint ID
2131 2136 */
2132 2137 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2133 2138 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2134 2139 var url = utils.url_join_encode(
2135 2140 this._baseProjectUrl,
2136 2141 'api/notebooks',
2137 2142 this.notebookPath(),
2138 2143 this.notebook_name,
2139 2144 'checkpoints',
2140 2145 checkpoint
2141 2146 );
2142 2147 $.ajax(url, {
2143 2148 type: 'DELETE',
2144 2149 success: $.proxy(this.delete_checkpoint_success, this),
2145 2150 error: $.proxy(this.delete_notebook_error,this)
2146 2151 });
2147 2152 };
2148 2153
2149 2154 /**
2150 2155 * Success callback for deleting a notebook checkpoint
2151 2156 *
2152 2157 * @method delete_checkpoint_success
2153 2158 * @param {Object} data (ignored, should be empty)
2154 2159 * @param {String} status Description of response status
2155 2160 * @param {jqXHR} xhr jQuery Ajax object
2156 2161 */
2157 2162 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2158 2163 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2159 2164 this.load_notebook(this.notebook_name, this.notebook_path);
2160 2165 };
2161 2166
2162 2167 /**
2163 2168 * Failure callback for deleting a notebook checkpoint.
2164 2169 *
2165 2170 * @method delete_checkpoint_error
2166 2171 * @param {jqXHR} xhr jQuery Ajax object
2167 2172 * @param {String} status Description of response status
2168 2173 * @param {String} error_msg HTTP error message
2169 2174 */
2170 2175 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2171 2176 $([IPython.events]).trigger('checkpoint_delete_failed.Notebook');
2172 2177 };
2173 2178
2174 2179
2175 2180 IPython.Notebook = Notebook;
2176 2181
2177 2182
2178 2183 return IPython;
2179 2184
2180 2185 }(IPython));
@@ -1,601 +1,593
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 = 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 196 this.refresh();
197 197 }
198 198
199 199 };
200 200 return cont;
201 201 };
202 202
203 203 TextCell.prototype.execute = function () {
204 204 this.render();
205 205 };
206 206
207 TextCell.prototype.command_mode = function () {
208 var cont = IPython.Cell.prototype.command_mode.apply(this);
209 if (cont) {
210 this.focus_cell();
211 };
212 return cont;
213 }
214
215 207 TextCell.prototype.edit_mode = function () {
216 208 var cont = IPython.Cell.prototype.edit_mode.apply(this);
217 209 if (cont) {
218 210 this.unrender();
219 211 this.focus_editor();
220 212 };
221 213 return cont;
222 214 }
223 215
224 216 /**
225 217 * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
226 218 * @method get_text
227 219 * @retrun {string} CodeMirror current text value
228 220 */
229 221 TextCell.prototype.get_text = function() {
230 222 return this.code_mirror.getValue();
231 223 };
232 224
233 225 /**
234 226 * @param {string} text - Codemiror text value
235 227 * @see TextCell#get_text
236 228 * @method set_text
237 229 * */
238 230 TextCell.prototype.set_text = function(text) {
239 231 this.code_mirror.setValue(text);
240 232 this.code_mirror.refresh();
241 233 };
242 234
243 235 /**
244 236 * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
245 237 * @method get_rendered
246 238 * @return {html} html of rendered element
247 239 * */
248 240 TextCell.prototype.get_rendered = function() {
249 241 return this.element.find('div.text_cell_render').html();
250 242 };
251 243
252 244 /**
253 245 * @method set_rendered
254 246 */
255 247 TextCell.prototype.set_rendered = function(text) {
256 248 this.element.find('div.text_cell_render').html(text);
257 249 };
258 250
259 251 /**
260 252 * @method at_top
261 253 * @return {Boolean}
262 254 */
263 255 TextCell.prototype.at_top = function () {
264 256 if (this.rendered) {
265 257 return true;
266 258 } else {
267 259 var cursor = this.code_mirror.getCursor();
268 260 if (cursor.line === 0 && cursor.ch === 0) {
269 261 return true;
270 262 } else {
271 263 return false;
272 264 };
273 265 };
274 266 };
275 267
276 268 /**
277 269 * @method at_bottom
278 270 * @return {Boolean}
279 271 * */
280 272 TextCell.prototype.at_bottom = function () {
281 273 if (this.rendered) {
282 274 return true;
283 275 } else {
284 276 var cursor = this.code_mirror.getCursor();
285 277 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
286 278 return true;
287 279 } else {
288 280 return false;
289 281 };
290 282 };
291 283 };
292 284
293 285 /**
294 286 * Create Text cell from JSON
295 287 * @param {json} data - JSON serialized text-cell
296 288 * @method fromJSON
297 289 */
298 290 TextCell.prototype.fromJSON = function (data) {
299 291 IPython.Cell.prototype.fromJSON.apply(this, arguments);
300 292 if (data.cell_type === this.cell_type) {
301 293 if (data.source !== undefined) {
302 294 this.set_text(data.source);
303 295 // make this value the starting point, so that we can only undo
304 296 // to this state, instead of a blank cell
305 297 this.code_mirror.clearHistory();
306 298 this.set_rendered(data.rendered || '');
307 299 this.rendered = false;
308 300 this.render();
309 301 }
310 302 }
311 303 };
312 304
313 305 /** Generate JSON from cell
314 306 * @return {object} cell data serialised to json
315 307 */
316 308 TextCell.prototype.toJSON = function () {
317 309 var data = IPython.Cell.prototype.toJSON.apply(this);
318 310 data.source = this.get_text();
319 311 if (data.source == this.placeholder) {
320 312 data.source = "";
321 313 }
322 314 return data;
323 315 };
324 316
325 317
326 318 /**
327 319 * @class MarkdownCell
328 320 * @constructor MarkdownCell
329 321 * @extends IPython.HTMLCell
330 322 */
331 323 var MarkdownCell = function (options) {
332 324 options = this.mergeopt(MarkdownCell, options);
333 325
334 326 this.cell_type = 'markdown';
335 327 TextCell.apply(this, [options]);
336 328 };
337 329
338 330 MarkdownCell.options_default = {
339 331 cm_config: {
340 332 mode: 'gfm'
341 333 },
342 334 placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
343 335 }
344 336
345 337 MarkdownCell.prototype = new TextCell();
346 338
347 339 /**
348 340 * @method render
349 341 */
350 342 MarkdownCell.prototype.render = function () {
351 343 var cont = IPython.TextCell.prototype.render.apply(this);
352 344 if (cont) {
353 345 var text = this.get_text();
354 346 var math = null;
355 347 if (text === "") { text = this.placeholder; }
356 348 var text_and_math = IPython.mathjaxutils.remove_math(text);
357 349 text = text_and_math[0];
358 350 math = text_and_math[1];
359 351 var html = marked.parser(marked.lexer(text));
360 352 html = $(IPython.mathjaxutils.replace_math(html, math));
361 353 // links in markdown cells should open in new tabs
362 354 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
363 355 try {
364 356 this.set_rendered(html);
365 357 } catch (e) {
366 358 console.log("Error running Javascript in Markdown:");
367 359 console.log(e);
368 360 this.set_rendered($("<div/>").addClass("js-error").html(
369 361 "Error rendering Markdown!<br/>" + e.toString())
370 362 );
371 363 }
372 364 this.element.find('div.text_cell_input').hide();
373 365 this.element.find("div.text_cell_render").show();
374 366 this.typeset()
375 367 };
376 368 return cont;
377 369 };
378 370
379 371
380 372 // RawCell
381 373
382 374 /**
383 375 * @class RawCell
384 376 * @constructor RawCell
385 377 * @extends IPython.TextCell
386 378 */
387 379 var RawCell = function (options) {
388 380
389 381 options = this.mergeopt(RawCell,options)
390 382 TextCell.apply(this, [options]);
391 383 this.cell_type = 'raw';
392 384 // RawCell should always hide its rendered div
393 385 this.element.find('div.text_cell_render').hide();
394 386 };
395 387
396 388 RawCell.options_default = {
397 389 placeholder : "Write raw LaTeX or other formats here, for use with nbconvert.\n" +
398 390 "It will not be rendered in the notebook.\n" +
399 391 "When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
400 392 };
401 393
402 394 RawCell.prototype = new TextCell();
403 395
404 396 /** @method bind_events **/
405 397 RawCell.prototype.bind_events = function () {
406 398 TextCell.prototype.bind_events.apply(this);
407 399 var that = this
408 400 this.element.focusout(function() {
409 401 that.auto_highlight();
410 402 });
411 403 };
412 404
413 405 /**
414 406 * Trigger autodetection of highlight scheme for current cell
415 407 * @method auto_highlight
416 408 */
417 409 RawCell.prototype.auto_highlight = function () {
418 410 this._auto_highlight(IPython.config.raw_cell_highlight);
419 411 };
420 412
421 413 /** @method render **/
422 414 RawCell.prototype.render = function () {
423 415 // Make sure that this cell type can never be rendered
424 416 if (this.rendered) {
425 417 this.unrender();
426 418 }
427 419 var text = this.get_text();
428 420 if (text === "") { text = this.placeholder; }
429 421 this.set_text(text);
430 422 };
431 423
432 424
433 425 /** @method handle_codemirror_keyevent **/
434 426 RawCell.prototype.handle_codemirror_keyevent = function (editor, event) {
435 427
436 428 var that = this;
437 429 if (this.mode === 'command') {
438 430 return false
439 431 } else if (this.mode === 'edit') {
440 432 // TODO: review these handlers...
441 433 if (event.which === key.UPARROW && event.type === 'keydown') {
442 434 // If we are not at the top, let CM handle the up arrow and
443 435 // prevent the global keydown handler from handling it.
444 436 if (!that.at_top()) {
445 437 event.stop();
446 438 return false;
447 439 } else {
448 440 return true;
449 441 };
450 442 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
451 443 // If we are not at the bottom, let CM handle the down arrow and
452 444 // prevent the global keydown handler from handling it.
453 445 if (!that.at_bottom()) {
454 446 event.stop();
455 447 return false;
456 448 } else {
457 449 return true;
458 450 };
459 451 };
460 452 return false;
461 453 };
462 454 return false;
463 455 };
464 456
465 457
466 458 /**
467 459 * @class HeadingCell
468 460 * @extends IPython.TextCell
469 461 */
470 462
471 463 /**
472 464 * @constructor HeadingCell
473 465 * @extends IPython.TextCell
474 466 */
475 467 var HeadingCell = function (options) {
476 468 options = this.mergeopt(HeadingCell, options);
477 469
478 470 this.level = 1;
479 471 this.cell_type = 'heading';
480 472 TextCell.apply(this, [options]);
481 473
482 474 /**
483 475 * heading level of the cell, use getter and setter to access
484 476 * @property level
485 477 */
486 478 };
487 479
488 480 HeadingCell.options_default = {
489 481 placeholder: "Type Heading Here"
490 482 };
491 483
492 484 HeadingCell.prototype = new TextCell();
493 485
494 486 /** @method fromJSON */
495 487 HeadingCell.prototype.fromJSON = function (data) {
496 488 if (data.level != undefined){
497 489 this.level = data.level;
498 490 }
499 491 TextCell.prototype.fromJSON.apply(this, arguments);
500 492 };
501 493
502 494
503 495 /** @method toJSON */
504 496 HeadingCell.prototype.toJSON = function () {
505 497 var data = TextCell.prototype.toJSON.apply(this);
506 498 data.level = this.get_level();
507 499 return data;
508 500 };
509 501
510 502 /**
511 503 * can the cell be split into two cells
512 504 * @method is_splittable
513 505 **/
514 506 HeadingCell.prototype.is_splittable = function () {
515 507 return false;
516 508 };
517 509
518 510
519 511 /**
520 512 * can the cell be merged with other cells
521 513 * @method is_mergeable
522 514 **/
523 515 HeadingCell.prototype.is_mergeable = function () {
524 516 return false;
525 517 };
526 518
527 519 /**
528 520 * Change heading level of cell, and re-render
529 521 * @method set_level
530 522 */
531 523 HeadingCell.prototype.set_level = function (level) {
532 524 this.level = level;
533 525 if (this.rendered) {
534 526 this.rendered = false;
535 527 this.render();
536 528 };
537 529 };
538 530
539 531 /** The depth of header cell, based on html (h1 to h6)
540 532 * @method get_level
541 533 * @return {integer} level - for 1 to 6
542 534 */
543 535 HeadingCell.prototype.get_level = function () {
544 536 return this.level;
545 537 };
546 538
547 539
548 540 HeadingCell.prototype.set_rendered = function (html) {
549 541 this.element.find("div.text_cell_render").html(html);
550 542 };
551 543
552 544
553 545 HeadingCell.prototype.get_rendered = function () {
554 546 var r = this.element.find("div.text_cell_render");
555 547 return r.children().first().html();
556 548 };
557 549
558 550
559 551 HeadingCell.prototype.render = function () {
560 552 var cont = IPython.TextCell.prototype.render.apply(this);
561 553 if (cont) {
562 554 var text = this.get_text();
563 555 var math = null;
564 556 // Markdown headings must be a single line
565 557 text = text.replace(/\n/g, ' ');
566 558 if (text === "") { text = this.placeholder; }
567 559 text = Array(this.level + 1).join("#") + " " + text;
568 560 var text_and_math = IPython.mathjaxutils.remove_math(text);
569 561 text = text_and_math[0];
570 562 math = text_and_math[1];
571 563 var html = marked.parser(marked.lexer(text));
572 564 var h = $(IPython.mathjaxutils.replace_math(html, math));
573 565 // add id and linkback anchor
574 566 var hash = h.text().replace(/ /g, '-');
575 567 h.attr('id', hash);
576 568 h.append(
577 569 $('<a/>')
578 570 .addClass('anchor-link')
579 571 .attr('href', '#' + hash)
580 572 .text('¶')
581 573 );
582 574
583 575 this.set_rendered(h);
584 576 this.typeset();
585 577 this.element.find('div.text_cell_input').hide();
586 578 this.element.find("div.text_cell_render").show();
587 579
588 580 };
589 581 return cont;
590 582 };
591 583
592 584 IPython.TextCell = TextCell;
593 585 IPython.MarkdownCell = MarkdownCell;
594 586 IPython.RawCell = RawCell;
595 587 IPython.HeadingCell = HeadingCell;
596 588
597 589
598 590 return IPython;
599 591
600 592 }(IPython));
601 593
General Comments 0
You need to be logged in to leave comments. Login now