##// END OF EJS Templates
In person review with @ellisonbg
jon -
Show More
@@ -1,563 +1,564 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 ], function(IPython, $, utils) {
9 9 "use strict";
10 10
11 11 // monkey patch CM to be able to syntax highlight cell magics
12 12 // bug reported upstream,
13 13 // see https://github.com/marijnh/CodeMirror2/issues/670
14 14 if(CodeMirror.getMode(1,'text/plain').indent === undefined ){
15 15 CodeMirror.modes.null = function() {
16 16 return {token: function(stream) {stream.skipToEnd();},indent : function(){return 0;}};
17 17 };
18 18 }
19 19
20 20 CodeMirror.patchedGetMode = function(config, mode){
21 21 var cmmode = CodeMirror.getMode(config, mode);
22 22 if(cmmode.indent === null) {
23 23 console.log('patch mode "' , mode, '" on the fly');
24 24 cmmode.indent = function(){return 0;};
25 25 }
26 26 return cmmode;
27 27 };
28 28 // end monkey patching CodeMirror
29 29
30 30 /**
31 31 * The Base `Cell` class from which to inherit
32 32 * @class Cell
33 33 **/
34 34
35 35 /*
36 36 * @constructor
37 37 *
38 38 * @param {object|undefined} [options]
39 39 * @param [options.cm_config] {object} config to pass to CodeMirror, will extend default parameters
40 40 */
41 var Cell = function (options, keyboard_manager, events) {
42 this.keyboard_manager = keyboard_manager;
43 this.events = events;
44 options = this.mergeopt(Cell, options);
41 var Cell = function (options) {
42 options = options || {};
43 this.keyboard_manager = options.keyboard_manager;
44 this.events = options.events;
45 var config = this.mergeopt(Cell, options.config);
45 46 // superclass default overwrite our default
46 47
47 this.placeholder = options.placeholder || '';
48 this.read_only = options.cm_config.readOnly;
48 this.placeholder = config.placeholder || '';
49 this.read_only = config.cm_config.readOnly;
49 50 this.selected = false;
50 51 this.rendered = false;
51 52 this.mode = 'command';
52 53 this.metadata = {};
53 54 // load this from metadata later ?
54 55 this.user_highlight = 'auto';
55 this.cm_config = options.cm_config;
56 this.cm_config = config.cm_config;
56 57 this.cell_id = utils.uuid();
57 this._options = options;
58 this._options = config;
58 59
59 60 // For JS VM engines optimization, attributes should be all set (even
60 61 // to null) in the constructor, and if possible, if different subclass
61 62 // have new attributes with same name, they should be created in the
62 63 // same order. Easiest is to create and set to null in parent class.
63 64
64 65 this.element = null;
65 66 this.cell_type = this.cell_type || null;
66 67 this.code_mirror = null;
67 68
68 69 this.create_element();
69 70 if (this.element !== null) {
70 71 this.element.data("cell", this);
71 72 this.bind_events();
72 73 this.init_classes();
73 74 }
74 75 };
75 76
76 77 Cell.options_default = {
77 78 cm_config : {
78 79 indentUnit : 4,
79 80 readOnly: false,
80 81 theme: "default",
81 82 extraKeys: {
82 83 "Cmd-Right":"goLineRight",
83 84 "End":"goLineRight",
84 85 "Cmd-Left":"goLineLeft"
85 86 }
86 87 }
87 88 };
88 89
89 90 // FIXME: Workaround CM Bug #332 (Safari segfault on drag)
90 91 // by disabling drag/drop altogether on Safari
91 92 // https://github.com/marijnh/CodeMirror/issues/332
92 93 if (utils.browser[0] == "Safari") {
93 94 Cell.options_default.cm_config.dragDrop = false;
94 95 }
95 96
96 97 Cell.prototype.mergeopt = function(_class, options, overwrite){
97 98 options = options || {};
98 99 overwrite = overwrite || {};
99 100 return $.extend(true, {}, _class.options_default, options, overwrite);
100 101 };
101 102
102 103 /**
103 104 * Empty. Subclasses must implement create_element.
104 105 * This should contain all the code to create the DOM element in notebook
105 106 * and will be called by Base Class constructor.
106 107 * @method create_element
107 108 */
108 109 Cell.prototype.create_element = function () {
109 110 };
110 111
111 112 Cell.prototype.init_classes = function () {
112 113 // Call after this.element exists to initialize the css classes
113 114 // related to selected, rendered and mode.
114 115 if (this.selected) {
115 116 this.element.addClass('selected');
116 117 } else {
117 118 this.element.addClass('unselected');
118 119 }
119 120 if (this.rendered) {
120 121 this.element.addClass('rendered');
121 122 } else {
122 123 this.element.addClass('unrendered');
123 124 }
124 125 if (this.mode === 'edit') {
125 126 this.element.addClass('edit_mode');
126 127 } else {
127 128 this.element.addClass('command_mode');
128 129 }
129 130 };
130 131
131 132 /**
132 133 * Subclasses can implement override bind_events.
133 134 * Be carefull to call the parent method when overwriting as it fires event.
134 135 * this will be triggerd after create_element in constructor.
135 136 * @method bind_events
136 137 */
137 138 Cell.prototype.bind_events = function () {
138 139 var that = this;
139 140 // We trigger events so that Cell doesn't have to depend on Notebook.
140 141 that.element.click(function (event) {
141 142 if (!that.selected) {
142 143 that.events.trigger('select.Cell', {'cell':that});
143 144 }
144 145 });
145 146 that.element.focusin(function (event) {
146 147 if (!that.selected) {
147 148 that.events.trigger('select.Cell', {'cell':that});
148 149 }
149 150 });
150 151 if (this.code_mirror) {
151 152 this.code_mirror.on("change", function(cm, change) {
152 153 that.events.trigger("set_dirty.Notebook", {value: true});
153 154 });
154 155 }
155 156 if (this.code_mirror) {
156 157 this.code_mirror.on('focus', function(cm, change) {
157 158 that.events.trigger('edit_mode.Cell', {cell: that});
158 159 });
159 160 }
160 161 if (this.code_mirror) {
161 162 this.code_mirror.on('blur', function(cm, change) {
162 163 that.events.trigger('command_mode.Cell', {cell: that});
163 164 });
164 165 }
165 166 };
166 167
167 168 /**
168 169 * This method gets called in CodeMirror's onKeyDown/onKeyPress
169 170 * handlers and is used to provide custom key handling.
170 171 *
171 172 * To have custom handling, subclasses should override this method, but still call it
172 173 * in order to process the Edit mode keyboard shortcuts.
173 174 *
174 175 * @method handle_codemirror_keyevent
175 176 * @param {CodeMirror} editor - The codemirror instance bound to the cell
176 177 * @param {event} event - key press event which either should or should not be handled by CodeMirror
177 178 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
178 179 */
179 180 Cell.prototype.handle_codemirror_keyevent = function (editor, event) {
180 181 var that = this;
181 182 var shortcuts = this.keyboard_manager.edit_shortcuts;
182 183
183 184 // if this is an edit_shortcuts shortcut, the global keyboard/shortcut
184 185 // manager will handle it
185 186 if (shortcuts.handles(event)) { return true; }
186 187
187 188 return false;
188 189 };
189 190
190 191
191 192 /**
192 193 * Triger typsetting of math by mathjax on current cell element
193 194 * @method typeset
194 195 */
195 196 Cell.prototype.typeset = function () {
196 197 if (window.MathJax) {
197 198 var cell_math = this.element.get(0);
198 199 MathJax.Hub.Queue(["Typeset", MathJax.Hub, cell_math]);
199 200 }
200 201 };
201 202
202 203 /**
203 204 * handle cell level logic when a cell is selected
204 205 * @method select
205 206 * @return is the action being taken
206 207 */
207 208 Cell.prototype.select = function () {
208 209 if (!this.selected) {
209 210 this.element.addClass('selected');
210 211 this.element.removeClass('unselected');
211 212 this.selected = true;
212 213 return true;
213 214 } else {
214 215 return false;
215 216 }
216 217 };
217 218
218 219 /**
219 220 * handle cell level logic when a cell is unselected
220 221 * @method unselect
221 222 * @return is the action being taken
222 223 */
223 224 Cell.prototype.unselect = function () {
224 225 if (this.selected) {
225 226 this.element.addClass('unselected');
226 227 this.element.removeClass('selected');
227 228 this.selected = false;
228 229 return true;
229 230 } else {
230 231 return false;
231 232 }
232 233 };
233 234
234 235 /**
235 236 * handle cell level logic when a cell is rendered
236 237 * @method render
237 238 * @return is the action being taken
238 239 */
239 240 Cell.prototype.render = function () {
240 241 if (!this.rendered) {
241 242 this.element.addClass('rendered');
242 243 this.element.removeClass('unrendered');
243 244 this.rendered = true;
244 245 return true;
245 246 } else {
246 247 return false;
247 248 }
248 249 };
249 250
250 251 /**
251 252 * handle cell level logic when a cell is unrendered
252 253 * @method unrender
253 254 * @return is the action being taken
254 255 */
255 256 Cell.prototype.unrender = function () {
256 257 if (this.rendered) {
257 258 this.element.addClass('unrendered');
258 259 this.element.removeClass('rendered');
259 260 this.rendered = false;
260 261 return true;
261 262 } else {
262 263 return false;
263 264 }
264 265 };
265 266
266 267 /**
267 268 * Delegates keyboard shortcut handling to either IPython keyboard
268 269 * manager when in command mode, or CodeMirror when in edit mode
269 270 *
270 271 * @method handle_keyevent
271 272 * @param {CodeMirror} editor - The codemirror instance bound to the cell
272 273 * @param {event} - key event to be handled
273 274 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
274 275 */
275 276 Cell.prototype.handle_keyevent = function (editor, event) {
276 277
277 278 // console.log('CM', this.mode, event.which, event.type)
278 279
279 280 if (this.mode === 'command') {
280 281 return true;
281 282 } else if (this.mode === 'edit') {
282 283 return this.handle_codemirror_keyevent(editor, event);
283 284 }
284 285 };
285 286
286 287 /**
287 288 * @method at_top
288 289 * @return {Boolean}
289 290 */
290 291 Cell.prototype.at_top = function () {
291 292 var cm = this.code_mirror;
292 293 var cursor = cm.getCursor();
293 294 if (cursor.line === 0 && cursor.ch === 0) {
294 295 return true;
295 296 }
296 297 return false;
297 298 };
298 299
299 300 /**
300 301 * @method at_bottom
301 302 * @return {Boolean}
302 303 * */
303 304 Cell.prototype.at_bottom = function () {
304 305 var cm = this.code_mirror;
305 306 var cursor = cm.getCursor();
306 307 if (cursor.line === (cm.lineCount()-1) && cursor.ch === cm.getLine(cursor.line).length) {
307 308 return true;
308 309 }
309 310 return false;
310 311 };
311 312
312 313 /**
313 314 * enter the command mode for the cell
314 315 * @method command_mode
315 316 * @return is the action being taken
316 317 */
317 318 Cell.prototype.command_mode = function () {
318 319 if (this.mode !== 'command') {
319 320 this.element.addClass('command_mode');
320 321 this.element.removeClass('edit_mode');
321 322 this.mode = 'command';
322 323 return true;
323 324 } else {
324 325 return false;
325 326 }
326 327 };
327 328
328 329 /**
329 330 * enter the edit mode for the cell
330 331 * @method command_mode
331 332 * @return is the action being taken
332 333 */
333 334 Cell.prototype.edit_mode = function () {
334 335 if (this.mode !== 'edit') {
335 336 this.element.addClass('edit_mode');
336 337 this.element.removeClass('command_mode');
337 338 this.mode = 'edit';
338 339 return true;
339 340 } else {
340 341 return false;
341 342 }
342 343 };
343 344
344 345 /**
345 346 * Focus the cell in the DOM sense
346 347 * @method focus_cell
347 348 */
348 349 Cell.prototype.focus_cell = function () {
349 350 this.element.focus();
350 351 };
351 352
352 353 /**
353 354 * Focus the editor area so a user can type
354 355 *
355 356 * NOTE: If codemirror is focused via a mouse click event, you don't want to
356 357 * call this because it will cause a page jump.
357 358 * @method focus_editor
358 359 */
359 360 Cell.prototype.focus_editor = function () {
360 361 this.refresh();
361 362 this.code_mirror.focus();
362 363 };
363 364
364 365 /**
365 366 * Refresh codemirror instance
366 367 * @method refresh
367 368 */
368 369 Cell.prototype.refresh = function () {
369 370 this.code_mirror.refresh();
370 371 };
371 372
372 373 /**
373 374 * should be overritten by subclass
374 375 * @method get_text
375 376 */
376 377 Cell.prototype.get_text = function () {
377 378 };
378 379
379 380 /**
380 381 * should be overritten by subclass
381 382 * @method set_text
382 383 * @param {string} text
383 384 */
384 385 Cell.prototype.set_text = function (text) {
385 386 };
386 387
387 388 /**
388 389 * should be overritten by subclass
389 390 * serialise cell to json.
390 391 * @method toJSON
391 392 **/
392 393 Cell.prototype.toJSON = function () {
393 394 var data = {};
394 395 data.metadata = this.metadata;
395 396 data.cell_type = this.cell_type;
396 397 return data;
397 398 };
398 399
399 400
400 401 /**
401 402 * should be overritten by subclass
402 403 * @method fromJSON
403 404 **/
404 405 Cell.prototype.fromJSON = function (data) {
405 406 if (data.metadata !== undefined) {
406 407 this.metadata = data.metadata;
407 408 }
408 409 this.celltoolbar.rebuild();
409 410 };
410 411
411 412
412 413 /**
413 414 * can the cell be split into two cells
414 415 * @method is_splittable
415 416 **/
416 417 Cell.prototype.is_splittable = function () {
417 418 return true;
418 419 };
419 420
420 421
421 422 /**
422 423 * can the cell be merged with other cells
423 424 * @method is_mergeable
424 425 **/
425 426 Cell.prototype.is_mergeable = function () {
426 427 return true;
427 428 };
428 429
429 430
430 431 /**
431 432 * @return {String} - the text before the cursor
432 433 * @method get_pre_cursor
433 434 **/
434 435 Cell.prototype.get_pre_cursor = function () {
435 436 var cursor = this.code_mirror.getCursor();
436 437 var text = this.code_mirror.getRange({line:0, ch:0}, cursor);
437 438 text = text.replace(/^\n+/, '').replace(/\n+$/, '');
438 439 return text;
439 440 };
440 441
441 442
442 443 /**
443 444 * @return {String} - the text after the cursor
444 445 * @method get_post_cursor
445 446 **/
446 447 Cell.prototype.get_post_cursor = function () {
447 448 var cursor = this.code_mirror.getCursor();
448 449 var last_line_num = this.code_mirror.lineCount()-1;
449 450 var last_line_len = this.code_mirror.getLine(last_line_num).length;
450 451 var end = {line:last_line_num, ch:last_line_len};
451 452 var text = this.code_mirror.getRange(cursor, end);
452 453 text = text.replace(/^\n+/, '').replace(/\n+$/, '');
453 454 return text;
454 455 };
455 456
456 457 /**
457 458 * Show/Hide CodeMirror LineNumber
458 459 * @method show_line_numbers
459 460 *
460 461 * @param value {Bool} show (true), or hide (false) the line number in CodeMirror
461 462 **/
462 463 Cell.prototype.show_line_numbers = function (value) {
463 464 this.code_mirror.setOption('lineNumbers', value);
464 465 this.code_mirror.refresh();
465 466 };
466 467
467 468 /**
468 469 * Toggle CodeMirror LineNumber
469 470 * @method toggle_line_numbers
470 471 **/
471 472 Cell.prototype.toggle_line_numbers = function () {
472 473 var val = this.code_mirror.getOption('lineNumbers');
473 474 this.show_line_numbers(!val);
474 475 };
475 476
476 477 /**
477 478 * Force codemirror highlight mode
478 479 * @method force_highlight
479 480 * @param {object} - CodeMirror mode
480 481 **/
481 482 Cell.prototype.force_highlight = function(mode) {
482 483 this.user_highlight = mode;
483 484 this.auto_highlight();
484 485 };
485 486
486 487 /**
487 488 * Try to autodetect cell highlight mode, or use selected mode
488 489 * @methods _auto_highlight
489 490 * @private
490 491 * @param {String|object|undefined} - CodeMirror mode | 'auto'
491 492 **/
492 493 Cell.prototype._auto_highlight = function (modes) {
493 494 //Here we handle manually selected modes
494 495 var mode;
495 496 if( this.user_highlight !== undefined && this.user_highlight != 'auto' )
496 497 {
497 498 mode = this.user_highlight;
498 499 CodeMirror.autoLoadMode(this.code_mirror, mode);
499 500 this.code_mirror.setOption('mode', mode);
500 501 return;
501 502 }
502 503 var current_mode = this.code_mirror.getOption('mode', mode);
503 504 var first_line = this.code_mirror.getLine(0);
504 505 // loop on every pairs
505 506 for(mode in modes) {
506 507 var regs = modes[mode].reg;
507 508 // only one key every time but regexp can't be keys...
508 509 for(var i=0; i<regs.length; i++) {
509 510 // here we handle non magic_modes
510 511 if(first_line.match(regs[i]) !== null) {
511 512 if(current_mode == mode){
512 513 return;
513 514 }
514 515 if (mode.search('magic_') !== 0) {
515 516 this.code_mirror.setOption('mode', mode);
516 517 CodeMirror.autoLoadMode(this.code_mirror, mode);
517 518 return;
518 519 }
519 520 var open = modes[mode].open || "%%";
520 521 var close = modes[mode].close || "%%end";
521 522 var mmode = mode;
522 523 mode = mmode.substr(6);
523 524 if(current_mode == mode){
524 525 return;
525 526 }
526 527 CodeMirror.autoLoadMode(this.code_mirror, mode);
527 528 // create on the fly a mode that swhitch between
528 529 // plain/text and smth else otherwise `%%` is
529 530 // source of some highlight issues.
530 531 // we use patchedGetMode to circumvent a bug in CM
531 532 CodeMirror.defineMode(mmode , function(config) {
532 533 return CodeMirror.multiplexingMode(
533 534 CodeMirror.patchedGetMode(config, 'text/plain'),
534 535 // always set someting on close
535 536 {open: open, close: close,
536 537 mode: CodeMirror.patchedGetMode(config, mode),
537 538 delimStyle: "delimit"
538 539 }
539 540 );
540 541 });
541 542 this.code_mirror.setOption('mode', mmode);
542 543 return;
543 544 }
544 545 }
545 546 }
546 547 // fallback on default
547 548 var default_mode;
548 549 try {
549 550 default_mode = this._options.cm_config.mode;
550 551 } catch(e) {
551 552 default_mode = 'text/plain';
552 553 }
553 554 if( current_mode === default_mode){
554 555 return;
555 556 }
556 557 this.code_mirror.setOption('mode', default_mode);
557 558 };
558 559
559 560 // Backwards compatability.
560 561 IPython.Cell = Cell;
561 562
562 563 return {'Cell': Cell};
563 564 });
@@ -1,516 +1,519 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'notebook/js/tooltip',
9 9 'base/js/keyboard',
10 10 'notebook/js/cell',
11 11 'notebook/js/outputarea',
12 12 'notebook/js/completer',
13 13 'notebook/js/celltoolbar',
14 14 ], function(IPython, $, utils, tooltip, keyboard, cell, outputarea, completer, celltoolbar) {
15 15 "use strict";
16 16 var Cell = cell.Cell;
17 17
18 18 /* local util for codemirror */
19 19 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;};
20 20
21 21 /**
22 22 *
23 23 * function to delete until previous non blanking space character
24 24 * or first multiple of 4 tabstop.
25 25 * @private
26 26 */
27 27 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
28 28 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
29 29 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
30 30 var cur = cm.getCursor(), line = cm.getLine(cur.line);
31 31 var tabsize = cm.getOption('tabSize');
32 32 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
33 33 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
34 34 var select = cm.getRange(from,cur);
35 35 if( select.match(/^\ +$/) !== null){
36 36 cm.replaceRange("",from,cur);
37 37 } else {
38 38 cm.deleteH(-1,"char");
39 39 }
40 40 };
41 41
42 42 var keycodes = keyboard.keycodes;
43 43
44 44 /**
45 45 * A Cell conceived to write code.
46 46 *
47 47 * The kernel doesn't have to be set at creation time, in that case
48 48 * it will be null and set_kernel has to be called later.
49 49 * @class CodeCell
50 50 * @extends Cell
51 51 *
52 52 * @constructor
53 53 * @param {Object|null} kernel
54 54 * @param {object|undefined} [options]
55 55 * @param [options.cm_config] {object} config to pass to CodeMirror
56 56 */
57 var CodeCell = function (kernel, options, events, config, keyboard_manager, notebook) {
57 var CodeCell = function (kernel, options) {
58 options = options || {};
58 59 this.kernel = kernel || null;
59 this.notebook = notebook;
60 this.notebook = options.notebook;
60 61 this.collapsed = false;
61 this.tooltip = new tooltip.Tooltip(events);
62 this.events = events;
63 this.config = config;
62 this.events = options.events;
63 this.tooltip = new tooltip.Tooltip(this.events);
64 this.config = options.config;
64 65
65 66 // create all attributed in constructor function
66 67 // even if null for V8 VM optimisation
67 68 this.input_prompt_number = null;
68 69 this.celltoolbar = null;
69 70 this.output_area = null;
70 71 this.last_msg_id = null;
71 72 this.completer = null;
72 73
73 74
74 75 var cm_overwrite_options = {
75 76 onKeyEvent: $.proxy(this.handle_keyevent,this)
76 77 };
77 78
78 options = this.mergeopt(CodeCell, options, {cm_config:cm_overwrite_options});
79
80 Cell.apply(this,[options, keyboard_manager, events]);
79 var config = this.mergeopt(CodeCell, this.config, {cm_config: cm_overwrite_options});
80 Cell.apply(this,[{
81 config: config,
82 keyboard_manager: options.keyboard_manager,
83 events: this.events}]);
81 84
82 85 // Attributes we want to override in this subclass.
83 86 this.cell_type = "code";
84 87
85 88 var that = this;
86 89 this.element.focusout(
87 90 function() { that.auto_highlight(); }
88 91 );
89 92 };
90 93
91 94 CodeCell.options_default = {
92 95 cm_config : {
93 96 extraKeys: {
94 97 "Tab" : "indentMore",
95 98 "Shift-Tab" : "indentLess",
96 99 "Backspace" : "delSpaceToPrevTabStop",
97 100 "Cmd-/" : "toggleComment",
98 101 "Ctrl-/" : "toggleComment"
99 102 },
100 103 mode: 'ipython',
101 104 theme: 'ipython',
102 105 matchBrackets: true,
103 106 // don't auto-close strings because of CodeMirror #2385
104 107 autoCloseBrackets: "()[]{}"
105 108 }
106 109 };
107 110
108 111 CodeCell.msg_cells = {};
109 112
110 113 CodeCell.prototype = new Cell();
111 114
112 115 /**
113 116 * @method auto_highlight
114 117 */
115 118 CodeCell.prototype.auto_highlight = function () {
116 119 this._auto_highlight(this.config.cell_magic_highlight);
117 120 };
118 121
119 122 /** @method create_element */
120 123 CodeCell.prototype.create_element = function () {
121 124 Cell.prototype.create_element.apply(this, arguments);
122 125
123 126 var cell = $('<div></div>').addClass('cell border-box-sizing code_cell');
124 127 cell.attr('tabindex','2');
125 128
126 129 var input = $('<div></div>').addClass('input');
127 130 var prompt = $('<div/>').addClass('prompt input_prompt');
128 131 var inner_cell = $('<div/>').addClass('inner_cell');
129 132 this.celltoolbar = new celltoolbar.CellToolbar(this, this.events, this.notebook);
130 133 inner_cell.append(this.celltoolbar.element);
131 134 var input_area = $('<div/>').addClass('input_area');
132 135 this.code_mirror = CodeMirror(input_area.get(0), this.cm_config);
133 136 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
134 137 inner_cell.append(input_area);
135 138 input.append(prompt).append(inner_cell);
136 139
137 140 var widget_area = $('<div/>')
138 141 .addClass('widget-area')
139 142 .hide();
140 143 this.widget_area = widget_area;
141 144 var widget_prompt = $('<div/>')
142 145 .addClass('prompt')
143 146 .appendTo(widget_area);
144 147 var widget_subarea = $('<div/>')
145 148 .addClass('widget-subarea')
146 149 .appendTo(widget_area);
147 150 this.widget_subarea = widget_subarea;
148 151 var widget_clear_buton = $('<button />')
149 152 .addClass('close')
150 153 .html('&times;')
151 154 .click(function() {
152 155 widget_area.slideUp('', function(){ widget_subarea.html(''); });
153 156 })
154 157 .appendTo(widget_prompt);
155 158
156 159 var output = $('<div></div>');
157 160 cell.append(input).append(widget_area).append(output);
158 161 this.element = cell;
159 162 this.output_area = new outputarea.OutputArea(output, true, this.events, this.keyboard_manager);
160 163 this.completer = new completer.Completer(this, this.events);
161 164 };
162 165
163 166 /** @method bind_events */
164 167 CodeCell.prototype.bind_events = function () {
165 168 Cell.prototype.bind_events.apply(this);
166 169 var that = this;
167 170
168 171 this.element.focusout(
169 172 function() { that.auto_highlight(); }
170 173 );
171 174 };
172 175
173 176
174 177 /**
175 178 * This method gets called in CodeMirror's onKeyDown/onKeyPress
176 179 * handlers and is used to provide custom key handling. Its return
177 180 * value is used to determine if CodeMirror should ignore the event:
178 181 * true = ignore, false = don't ignore.
179 182 * @method handle_codemirror_keyevent
180 183 */
181 184 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
182 185
183 186 var that = this;
184 187 // whatever key is pressed, first, cancel the tooltip request before
185 188 // they are sent, and remove tooltip if any, except for tab again
186 189 var tooltip_closed = null;
187 190 if (event.type === 'keydown' && event.which != keycodes.tab ) {
188 191 tooltip_closed = this.tooltip.remove_and_cancel_tooltip();
189 192 }
190 193
191 194 var cur = editor.getCursor();
192 195 if (event.keyCode === keycodes.enter){
193 196 this.auto_highlight();
194 197 }
195 198
196 199 if (event.which === keycodes.down && event.type === 'keypress' && this.tooltip.time_before_tooltip >= 0) {
197 200 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
198 201 // browser and keyboard layout !
199 202 // Pressing '(' , request tooltip, don't forget to reappend it
200 203 // The second argument says to hide the tooltip if the docstring
201 204 // is actually empty
202 205 this.tooltip.pending(that, true);
203 206 } else if ( tooltip_closed && event.which === keycodes.esc && event.type === 'keydown') {
204 207 // If tooltip is active, cancel it. The call to
205 208 // remove_and_cancel_tooltip above doesn't pass, force=true.
206 209 // Because of this it won't actually close the tooltip
207 210 // if it is in sticky mode. Thus, we have to check again if it is open
208 211 // and close it with force=true.
209 212 if (!this.tooltip._hidden) {
210 213 this.tooltip.remove_and_cancel_tooltip(true);
211 214 }
212 215 // If we closed the tooltip, don't let CM or the global handlers
213 216 // handle this event.
214 217 event.stop();
215 218 return true;
216 219 } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
217 220 if (editor.somethingSelected()){
218 221 var anchor = editor.getCursor("anchor");
219 222 var head = editor.getCursor("head");
220 223 if( anchor.line != head.line){
221 224 return false;
222 225 }
223 226 }
224 227 this.tooltip.request(that);
225 228 event.stop();
226 229 return true;
227 230 } else if (event.keyCode === keycodes.tab && event.type == 'keydown') {
228 231 // Tab completion.
229 232 this.tooltip.remove_and_cancel_tooltip();
230 233 if (editor.somethingSelected()) {
231 234 return false;
232 235 }
233 236 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
234 237 if (pre_cursor.trim() === "") {
235 238 // Don't autocomplete if the part of the line before the cursor
236 239 // is empty. In this case, let CodeMirror handle indentation.
237 240 return false;
238 241 } else {
239 242 event.stop();
240 243 this.completer.startCompletion();
241 244 return true;
242 245 }
243 246 }
244 247
245 248 // keyboard event wasn't one of those unique to code cells, let's see
246 249 // if it's one of the generic ones (i.e. check edit mode shortcuts)
247 250 return Cell.prototype.handle_codemirror_keyevent.apply(this, [editor, event]);
248 251 };
249 252
250 253 // Kernel related calls.
251 254
252 255 CodeCell.prototype.set_kernel = function (kernel) {
253 256 this.kernel = kernel;
254 257 };
255 258
256 259 /**
257 260 * Execute current code cell to the kernel
258 261 * @method execute
259 262 */
260 263 CodeCell.prototype.execute = function () {
261 264 this.output_area.clear_output();
262 265
263 266 // Clear widget area
264 267 this.widget_subarea.html('');
265 268 this.widget_subarea.height('');
266 269 this.widget_area.height('');
267 270 this.widget_area.hide();
268 271
269 272 this.set_input_prompt('*');
270 273 this.element.addClass("running");
271 274 if (this.last_msg_id) {
272 275 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
273 276 }
274 277 var callbacks = this.get_callbacks();
275 278
276 279 var old_msg_id = this.last_msg_id;
277 280 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true});
278 281 if (old_msg_id) {
279 282 delete CodeCell.msg_cells[old_msg_id];
280 283 }
281 284 CodeCell.msg_cells[this.last_msg_id] = this;
282 285 };
283 286
284 287 /**
285 288 * Construct the default callbacks for
286 289 * @method get_callbacks
287 290 */
288 291 CodeCell.prototype.get_callbacks = function () {
289 292 return {
290 293 shell : {
291 294 reply : $.proxy(this._handle_execute_reply, this),
292 295 payload : {
293 296 set_next_input : $.proxy(this._handle_set_next_input, this),
294 297 page : $.proxy(this._open_with_pager, this)
295 298 }
296 299 },
297 300 iopub : {
298 301 output : $.proxy(this.output_area.handle_output, this.output_area),
299 302 clear_output : $.proxy(this.output_area.handle_clear_output, this.output_area),
300 303 },
301 304 input : $.proxy(this._handle_input_request, this)
302 305 };
303 306 };
304 307
305 308 CodeCell.prototype._open_with_pager = function (payload) {
306 309 this.events.trigger('open_with_text.Pager', payload);
307 310 };
308 311
309 312 /**
310 313 * @method _handle_execute_reply
311 314 * @private
312 315 */
313 316 CodeCell.prototype._handle_execute_reply = function (msg) {
314 317 this.set_input_prompt(msg.content.execution_count);
315 318 this.element.removeClass("running");
316 319 this.events.trigger('set_dirty.Notebook', {value: true});
317 320 };
318 321
319 322 /**
320 323 * @method _handle_set_next_input
321 324 * @private
322 325 */
323 326 CodeCell.prototype._handle_set_next_input = function (payload) {
324 327 var data = {'cell': this, 'text': payload.text};
325 328 this.events.trigger('set_next_input.Notebook', data);
326 329 };
327 330
328 331 /**
329 332 * @method _handle_input_request
330 333 * @private
331 334 */
332 335 CodeCell.prototype._handle_input_request = function (msg) {
333 336 this.output_area.append_raw_input(msg);
334 337 };
335 338
336 339
337 340 // Basic cell manipulation.
338 341
339 342 CodeCell.prototype.select = function () {
340 343 var cont = Cell.prototype.select.apply(this);
341 344 if (cont) {
342 345 this.code_mirror.refresh();
343 346 this.auto_highlight();
344 347 }
345 348 return cont;
346 349 };
347 350
348 351 CodeCell.prototype.render = function () {
349 352 var cont = Cell.prototype.render.apply(this);
350 353 // Always execute, even if we are already in the rendered state
351 354 return cont;
352 355 };
353 356
354 357 CodeCell.prototype.unrender = function () {
355 358 // CodeCell is always rendered
356 359 return false;
357 360 };
358 361
359 362 CodeCell.prototype.select_all = function () {
360 363 var start = {line: 0, ch: 0};
361 364 var nlines = this.code_mirror.lineCount();
362 365 var last_line = this.code_mirror.getLine(nlines-1);
363 366 var end = {line: nlines-1, ch: last_line.length};
364 367 this.code_mirror.setSelection(start, end);
365 368 };
366 369
367 370
368 371 CodeCell.prototype.collapse_output = function () {
369 372 this.collapsed = true;
370 373 this.output_area.collapse();
371 374 };
372 375
373 376
374 377 CodeCell.prototype.expand_output = function () {
375 378 this.collapsed = false;
376 379 this.output_area.expand();
377 380 this.output_area.unscroll_area();
378 381 };
379 382
380 383 CodeCell.prototype.scroll_output = function () {
381 384 this.output_area.expand();
382 385 this.output_area.scroll_if_long();
383 386 };
384 387
385 388 CodeCell.prototype.toggle_output = function () {
386 389 this.collapsed = Boolean(1 - this.collapsed);
387 390 this.output_area.toggle_output();
388 391 };
389 392
390 393 CodeCell.prototype.toggle_output_scroll = function () {
391 394 this.output_area.toggle_scroll();
392 395 };
393 396
394 397
395 398 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
396 399 var ns;
397 400 if (prompt_value === undefined) {
398 401 ns = "&nbsp;";
399 402 } else {
400 403 ns = encodeURIComponent(prompt_value);
401 404 }
402 405 return 'In&nbsp;[' + ns + ']:';
403 406 };
404 407
405 408 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
406 409 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
407 410 for(var i=1; i < lines_number; i++) {
408 411 html.push(['...:']);
409 412 }
410 413 return html.join('<br/>');
411 414 };
412 415
413 416 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
414 417
415 418
416 419 CodeCell.prototype.set_input_prompt = function (number) {
417 420 var nline = 1;
418 421 if (this.code_mirror !== undefined) {
419 422 nline = this.code_mirror.lineCount();
420 423 }
421 424 this.input_prompt_number = number;
422 425 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
423 426 // This HTML call is okay because the user contents are escaped.
424 427 this.element.find('div.input_prompt').html(prompt_html);
425 428 };
426 429
427 430
428 431 CodeCell.prototype.clear_input = function () {
429 432 this.code_mirror.setValue('');
430 433 };
431 434
432 435
433 436 CodeCell.prototype.get_text = function () {
434 437 return this.code_mirror.getValue();
435 438 };
436 439
437 440
438 441 CodeCell.prototype.set_text = function (code) {
439 442 return this.code_mirror.setValue(code);
440 443 };
441 444
442 445
443 446 CodeCell.prototype.clear_output = function (wait) {
444 447 this.output_area.clear_output(wait);
445 448 this.set_input_prompt();
446 449 };
447 450
448 451
449 452 // JSON serialization
450 453
451 454 CodeCell.prototype.fromJSON = function (data) {
452 455 Cell.prototype.fromJSON.apply(this, arguments);
453 456 if (data.cell_type === 'code') {
454 457 if (data.input !== undefined) {
455 458 this.set_text(data.input);
456 459 // make this value the starting point, so that we can only undo
457 460 // to this state, instead of a blank cell
458 461 this.code_mirror.clearHistory();
459 462 this.auto_highlight();
460 463 }
461 464 if (data.prompt_number !== undefined) {
462 465 this.set_input_prompt(data.prompt_number);
463 466 } else {
464 467 this.set_input_prompt();
465 468 }
466 469 this.output_area.trusted = data.trusted || false;
467 470 this.output_area.fromJSON(data.outputs);
468 471 if (data.collapsed !== undefined) {
469 472 if (data.collapsed) {
470 473 this.collapse_output();
471 474 } else {
472 475 this.expand_output();
473 476 }
474 477 }
475 478 }
476 479 };
477 480
478 481
479 482 CodeCell.prototype.toJSON = function () {
480 483 var data = Cell.prototype.toJSON.apply(this);
481 484 data.input = this.get_text();
482 485 // is finite protect against undefined and '*' value
483 486 if (isFinite(this.input_prompt_number)) {
484 487 data.prompt_number = this.input_prompt_number;
485 488 }
486 489 var outputs = this.output_area.toJSON();
487 490 data.outputs = outputs;
488 491 data.language = 'python';
489 492 data.trusted = this.output_area.trusted;
490 493 data.collapsed = this.collapsed;
491 494 return data;
492 495 };
493 496
494 497 /**
495 498 * handle cell level logic when a cell is unselected
496 499 * @method unselect
497 500 * @return is the action being taken
498 501 */
499 502 CodeCell.prototype.unselect = function () {
500 503 var cont = Cell.prototype.unselect.apply(this);
501 504 if (cont) {
502 505 // When a code cell is usnelected, make sure that the corresponding
503 506 // tooltip and completer to that cell is closed.
504 507 this.tooltip.remove_and_cancel_tooltip(true);
505 508 if (this.completer !== null) {
506 509 this.completer.close();
507 510 }
508 511 }
509 512 return cont;
510 513 };
511 514
512 515 // Backwards compatability.
513 516 IPython.CodeCell = CodeCell;
514 517
515 518 return {'CodeCell': CodeCell};
516 519 });
@@ -1,559 +1,559 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/keyboard',
9 9 ], function(IPython, $, utils, keyboard) {
10 10 "use strict";
11 11
12 12 var browser = utils.browser[0];
13 13 var platform = utils.platform;
14 14
15 15 // Main keyboard manager for the notebook
16 16 var keycodes = keyboard.keycodes;
17 17
18 var KeyboardManager = function (pager, events) {
18 var KeyboardManager = function (options) {
19 19 this.mode = 'command';
20 20 this.enabled = true;
21 this.pager = pager;
21 this.pager = options.pager;
22 22 this.quick_help = undefined;
23 23 this.notebook = undefined;
24 24 this.bind_events();
25 this.command_shortcuts = new keyboard.ShortcutManager(undefined, events);
25 this.command_shortcuts = new keyboard.ShortcutManager(undefined, options.events);
26 26 this.command_shortcuts.add_shortcuts(this.get_default_common_shortcuts());
27 27 this.command_shortcuts.add_shortcuts(this.get_default_command_shortcuts());
28 this.edit_shortcuts = new keyboard.ShortcutManager(undefined, events);
28 this.edit_shortcuts = new keyboard.ShortcutManager(undefined, options.events);
29 29 this.edit_shortcuts.add_shortcuts(this.get_default_common_shortcuts());
30 30 this.edit_shortcuts.add_shortcuts(this.get_default_edit_shortcuts());
31 31 };
32 32
33 33 KeyboardManager.prototype.get_default_common_shortcuts = function() {
34 34 var that = this;
35 35 var shortcuts = {
36 36 'shift' : {
37 37 help : '',
38 38 help_index : '',
39 39 handler : function (event) {
40 40 // ignore shift keydown
41 41 return true;
42 42 }
43 43 },
44 44 'shift-enter' : {
45 45 help : 'run cell, select below',
46 46 help_index : 'ba',
47 47 handler : function (event) {
48 48 that.notebook.execute_cell_and_select_below();
49 49 return false;
50 50 }
51 51 },
52 52 'ctrl-enter' : {
53 53 help : 'run cell',
54 54 help_index : 'bb',
55 55 handler : function (event) {
56 56 that.notebook.execute_cell();
57 57 return false;
58 58 }
59 59 },
60 60 'alt-enter' : {
61 61 help : 'run cell, insert below',
62 62 help_index : 'bc',
63 63 handler : function (event) {
64 64 that.notebook.execute_cell_and_insert_below();
65 65 return false;
66 66 }
67 67 }
68 68 };
69 69
70 70 if (platform === 'MacOS') {
71 71 shortcuts['cmd-s'] =
72 72 {
73 73 help : 'save notebook',
74 74 help_index : 'fb',
75 75 handler : function (event) {
76 76 that.notebook.save_checkpoint();
77 77 event.preventDefault();
78 78 return false;
79 79 }
80 80 };
81 81 } else {
82 82 shortcuts['ctrl-s'] =
83 83 {
84 84 help : 'save notebook',
85 85 help_index : 'fb',
86 86 handler : function (event) {
87 87 that.notebook.save_checkpoint();
88 88 event.preventDefault();
89 89 return false;
90 90 }
91 91 };
92 92 }
93 93 return shortcuts;
94 94 };
95 95
96 96 KeyboardManager.prototype.get_default_edit_shortcuts = function() {
97 97 var that = this;
98 98 return {
99 99 'esc' : {
100 100 help : 'command mode',
101 101 help_index : 'aa',
102 102 handler : function (event) {
103 103 that.notebook.command_mode();
104 104 return false;
105 105 }
106 106 },
107 107 'ctrl-m' : {
108 108 help : 'command mode',
109 109 help_index : 'ab',
110 110 handler : function (event) {
111 111 that.notebook.command_mode();
112 112 return false;
113 113 }
114 114 },
115 115 'up' : {
116 116 help : '',
117 117 help_index : '',
118 118 handler : function (event) {
119 119 var index = that.notebook.get_selected_index();
120 120 var cell = that.notebook.get_cell(index);
121 121 if (cell && cell.at_top() && index !== 0) {
122 122 event.preventDefault();
123 123 that.notebook.command_mode();
124 124 that.notebook.select_prev();
125 125 that.notebook.edit_mode();
126 126 var cm = that.notebook.get_selected_cell().code_mirror;
127 127 cm.setCursor(cm.lastLine(), 0);
128 128 return false;
129 129 } else if (cell) {
130 130 var cm = cell.code_mirror;
131 131 cm.execCommand('goLineUp');
132 132 return false;
133 133 }
134 134 }
135 135 },
136 136 'down' : {
137 137 help : '',
138 138 help_index : '',
139 139 handler : function (event) {
140 140 var index = that.notebook.get_selected_index();
141 141 var cell = that.notebook.get_cell(index);
142 142 if (cell.at_bottom() && index !== (that.notebook.ncells()-1)) {
143 143 event.preventDefault();
144 144 that.notebook.command_mode();
145 145 that.notebook.select_next();
146 146 that.notebook.edit_mode();
147 147 var cm = that.notebook.get_selected_cell().code_mirror;
148 148 cm.setCursor(0, 0);
149 149 return false;
150 150 } else {
151 151 var cm = cell.code_mirror;
152 152 cm.execCommand('goLineDown');
153 153 return false;
154 154 }
155 155 }
156 156 },
157 157 'ctrl-shift--' : {
158 158 help : 'split cell',
159 159 help_index : 'ea',
160 160 handler : function (event) {
161 161 that.notebook.split_cell();
162 162 return false;
163 163 }
164 164 },
165 165 'ctrl-shift-subtract' : {
166 166 help : '',
167 167 help_index : 'eb',
168 168 handler : function (event) {
169 169 that.notebook.split_cell();
170 170 return false;
171 171 }
172 172 },
173 173 };
174 174 };
175 175
176 176 KeyboardManager.prototype.get_default_command_shortcuts = function() {
177 177 var that = this;
178 178 return {
179 179 'enter' : {
180 180 help : 'edit mode',
181 181 help_index : 'aa',
182 182 handler : function (event) {
183 183 that.notebook.edit_mode();
184 184 return false;
185 185 }
186 186 },
187 187 'up' : {
188 188 help : 'select previous cell',
189 189 help_index : 'da',
190 190 handler : function (event) {
191 191 var index = that.notebook.get_selected_index();
192 192 if (index !== 0 && index !== null) {
193 193 that.notebook.select_prev();
194 194 that.notebook.focus_cell();
195 195 }
196 196 return false;
197 197 }
198 198 },
199 199 'down' : {
200 200 help : 'select next cell',
201 201 help_index : 'db',
202 202 handler : function (event) {
203 203 var index = that.notebook.get_selected_index();
204 204 if (index !== (that.notebook.ncells()-1) && index !== null) {
205 205 that.notebook.select_next();
206 206 that.notebook.focus_cell();
207 207 }
208 208 return false;
209 209 }
210 210 },
211 211 'k' : {
212 212 help : 'select previous cell',
213 213 help_index : 'dc',
214 214 handler : function (event) {
215 215 var index = that.notebook.get_selected_index();
216 216 if (index !== 0 && index !== null) {
217 217 that.notebook.select_prev();
218 218 that.notebook.focus_cell();
219 219 }
220 220 return false;
221 221 }
222 222 },
223 223 'j' : {
224 224 help : 'select next cell',
225 225 help_index : 'dd',
226 226 handler : function (event) {
227 227 var index = that.notebook.get_selected_index();
228 228 if (index !== (that.notebook.ncells()-1) && index !== null) {
229 229 that.notebook.select_next();
230 230 that.notebook.focus_cell();
231 231 }
232 232 return false;
233 233 }
234 234 },
235 235 'x' : {
236 236 help : 'cut cell',
237 237 help_index : 'ee',
238 238 handler : function (event) {
239 239 that.notebook.cut_cell();
240 240 return false;
241 241 }
242 242 },
243 243 'c' : {
244 244 help : 'copy cell',
245 245 help_index : 'ef',
246 246 handler : function (event) {
247 247 that.notebook.copy_cell();
248 248 return false;
249 249 }
250 250 },
251 251 'shift-v' : {
252 252 help : 'paste cell above',
253 253 help_index : 'eg',
254 254 handler : function (event) {
255 255 that.notebook.paste_cell_above();
256 256 return false;
257 257 }
258 258 },
259 259 'v' : {
260 260 help : 'paste cell below',
261 261 help_index : 'eh',
262 262 handler : function (event) {
263 263 that.notebook.paste_cell_below();
264 264 return false;
265 265 }
266 266 },
267 267 'd' : {
268 268 help : 'delete cell (press twice)',
269 269 help_index : 'ej',
270 270 count: 2,
271 271 handler : function (event) {
272 272 that.notebook.delete_cell();
273 273 return false;
274 274 }
275 275 },
276 276 'a' : {
277 277 help : 'insert cell above',
278 278 help_index : 'ec',
279 279 handler : function (event) {
280 280 that.notebook.insert_cell_above();
281 281 that.notebook.select_prev();
282 282 that.notebook.focus_cell();
283 283 return false;
284 284 }
285 285 },
286 286 'b' : {
287 287 help : 'insert cell below',
288 288 help_index : 'ed',
289 289 handler : function (event) {
290 290 that.notebook.insert_cell_below();
291 291 that.notebook.select_next();
292 292 that.notebook.focus_cell();
293 293 return false;
294 294 }
295 295 },
296 296 'y' : {
297 297 help : 'to code',
298 298 help_index : 'ca',
299 299 handler : function (event) {
300 300 that.notebook.to_code();
301 301 return false;
302 302 }
303 303 },
304 304 'm' : {
305 305 help : 'to markdown',
306 306 help_index : 'cb',
307 307 handler : function (event) {
308 308 that.notebook.to_markdown();
309 309 return false;
310 310 }
311 311 },
312 312 'r' : {
313 313 help : 'to raw',
314 314 help_index : 'cc',
315 315 handler : function (event) {
316 316 that.notebook.to_raw();
317 317 return false;
318 318 }
319 319 },
320 320 '1' : {
321 321 help : 'to heading 1',
322 322 help_index : 'cd',
323 323 handler : function (event) {
324 324 that.notebook.to_heading(undefined, 1);
325 325 return false;
326 326 }
327 327 },
328 328 '2' : {
329 329 help : 'to heading 2',
330 330 help_index : 'ce',
331 331 handler : function (event) {
332 332 that.notebook.to_heading(undefined, 2);
333 333 return false;
334 334 }
335 335 },
336 336 '3' : {
337 337 help : 'to heading 3',
338 338 help_index : 'cf',
339 339 handler : function (event) {
340 340 that.notebook.to_heading(undefined, 3);
341 341 return false;
342 342 }
343 343 },
344 344 '4' : {
345 345 help : 'to heading 4',
346 346 help_index : 'cg',
347 347 handler : function (event) {
348 348 that.notebook.to_heading(undefined, 4);
349 349 return false;
350 350 }
351 351 },
352 352 '5' : {
353 353 help : 'to heading 5',
354 354 help_index : 'ch',
355 355 handler : function (event) {
356 356 that.notebook.to_heading(undefined, 5);
357 357 return false;
358 358 }
359 359 },
360 360 '6' : {
361 361 help : 'to heading 6',
362 362 help_index : 'ci',
363 363 handler : function (event) {
364 364 that.notebook.to_heading(undefined, 6);
365 365 return false;
366 366 }
367 367 },
368 368 'o' : {
369 369 help : 'toggle output',
370 370 help_index : 'gb',
371 371 handler : function (event) {
372 372 that.notebook.toggle_output();
373 373 return false;
374 374 }
375 375 },
376 376 'shift-o' : {
377 377 help : 'toggle output scrolling',
378 378 help_index : 'gc',
379 379 handler : function (event) {
380 380 that.notebook.toggle_output_scroll();
381 381 return false;
382 382 }
383 383 },
384 384 's' : {
385 385 help : 'save notebook',
386 386 help_index : 'fa',
387 387 handler : function (event) {
388 388 that.notebook.save_checkpoint();
389 389 return false;
390 390 }
391 391 },
392 392 'ctrl-j' : {
393 393 help : 'move cell down',
394 394 help_index : 'eb',
395 395 handler : function (event) {
396 396 that.notebook.move_cell_down();
397 397 return false;
398 398 }
399 399 },
400 400 'ctrl-k' : {
401 401 help : 'move cell up',
402 402 help_index : 'ea',
403 403 handler : function (event) {
404 404 that.notebook.move_cell_up();
405 405 return false;
406 406 }
407 407 },
408 408 'l' : {
409 409 help : 'toggle line numbers',
410 410 help_index : 'ga',
411 411 handler : function (event) {
412 412 that.notebook.cell_toggle_line_numbers();
413 413 return false;
414 414 }
415 415 },
416 416 'i' : {
417 417 help : 'interrupt kernel (press twice)',
418 418 help_index : 'ha',
419 419 count: 2,
420 420 handler : function (event) {
421 421 that.notebook.kernel.interrupt();
422 422 return false;
423 423 }
424 424 },
425 425 '0' : {
426 426 help : 'restart kernel (press twice)',
427 427 help_index : 'hb',
428 428 count: 2,
429 429 handler : function (event) {
430 430 that.notebook.restart_kernel();
431 431 return false;
432 432 }
433 433 },
434 434 'h' : {
435 435 help : 'keyboard shortcuts',
436 436 help_index : 'ge',
437 437 handler : function (event) {
438 438 that.quick_help.show_keyboard_shortcuts();
439 439 return false;
440 440 }
441 441 },
442 442 'z' : {
443 443 help : 'undo last delete',
444 444 help_index : 'ei',
445 445 handler : function (event) {
446 446 that.notebook.undelete_cell();
447 447 return false;
448 448 }
449 449 },
450 450 'shift-m' : {
451 451 help : 'merge cell below',
452 452 help_index : 'ek',
453 453 handler : function (event) {
454 454 that.notebook.merge_cell_below();
455 455 return false;
456 456 }
457 457 },
458 458 'q' : {
459 459 help : 'close pager',
460 460 help_index : 'gd',
461 461 handler : function (event) {
462 462 that.pager.collapse();
463 463 return false;
464 464 }
465 465 },
466 466 };
467 467 };
468 468
469 469 KeyboardManager.prototype.bind_events = function () {
470 470 var that = this;
471 471 $(document).keydown(function (event) {
472 472 return that.handle_keydown(event);
473 473 });
474 474 };
475 475
476 476 KeyboardManager.prototype.handle_keydown = function (event) {
477 477 var notebook = this.notebook;
478 478
479 479 if (event.which === keycodes.esc) {
480 480 // Intercept escape at highest level to avoid closing
481 481 // websocket connection with firefox
482 482 event.preventDefault();
483 483 }
484 484
485 485 if (!this.enabled) {
486 486 if (event.which === keycodes.esc) {
487 487 // ESC
488 488 notebook.command_mode();
489 489 return false;
490 490 }
491 491 return true;
492 492 }
493 493
494 494 if (this.mode === 'edit') {
495 495 return this.edit_shortcuts.call_handler(event);
496 496 } else if (this.mode === 'command') {
497 497 return this.command_shortcuts.call_handler(event);
498 498 }
499 499 return true;
500 500 };
501 501
502 502 KeyboardManager.prototype.edit_mode = function () {
503 503 this.last_mode = this.mode;
504 504 this.mode = 'edit';
505 505 };
506 506
507 507 KeyboardManager.prototype.command_mode = function () {
508 508 this.last_mode = this.mode;
509 509 this.mode = 'command';
510 510 };
511 511
512 512 KeyboardManager.prototype.enable = function () {
513 513 this.enabled = true;
514 514 };
515 515
516 516 KeyboardManager.prototype.disable = function () {
517 517 this.enabled = false;
518 518 };
519 519
520 520 KeyboardManager.prototype.register_events = function (e) {
521 521 var that = this;
522 522 var handle_focus = function () {
523 523 that.disable();
524 524 };
525 525 var handle_blur = function () {
526 526 that.enable();
527 527 };
528 528 e.on('focusin', handle_focus);
529 529 e.on('focusout', handle_blur);
530 530 // TODO: Very strange. The focusout event does not seem fire for the
531 531 // bootstrap textboxes on FF25&26... This works around that by
532 532 // registering focus and blur events recursively on all inputs within
533 533 // registered element.
534 534 e.find('input').blur(handle_blur);
535 535 e.on('DOMNodeInserted', function (event) {
536 536 var target = $(event.target);
537 537 if (target.is('input')) {
538 538 target.blur(handle_blur);
539 539 } else {
540 540 target.find('input').blur(handle_blur);
541 541 }
542 542 });
543 543 // There are times (raw_input) where we remove the element from the DOM before
544 544 // focusout is called. In this case we bind to the remove event of jQueryUI,
545 545 // which gets triggered upon removal, iff it is focused at the time.
546 546 // is_focused must be used to check for the case where an element within
547 547 // the element being removed is focused.
548 548 e.on('remove', function () {
549 549 if (utils.is_focused(e[0])) {
550 550 that.enable();
551 551 }
552 552 });
553 553 };
554 554
555 555 // For backwards compatability.
556 556 IPython.KeyboardManager = KeyboardManager;
557 557
558 558 return {'KeyboardManager': KeyboardManager};
559 559 });
@@ -1,109 +1,132 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 var ipython = ipython || {};
5 5 require([
6 6 'base/js/namespace',
7 7 'jquery',
8 8 'notebook/js/notebook',
9 9 'base/js/utils',
10 10 'base/js/page',
11 11 'notebook/js/layoutmanager',
12 12 'base/js/events',
13 13 'auth/js/loginwidget',
14 14 'notebook/js/maintoolbar',
15 15 'notebook/js/pager',
16 16 'notebook/js/quickhelp',
17 17 'notebook/js/menubar',
18 18 'notebook/js/notificationarea',
19 19 'notebook/js/savewidget',
20 20 'notebook/js/keyboardmanager',
21 21 'notebook/js/config',
22 22 ], function(
23 23 IPython,
24 24 $,
25 25 notebook,
26 26 utils,
27 27 page,
28 28 layoutmanager,
29 29 events,
30 30 loginwidget,
31 31 maintoolbar,
32 32 pager,
33 33 quickhelp,
34 34 menubar,
35 35 notificationarea,
36 36 savewidget,
37 37 keyboardmanager,
38 38 config
39 39 ) {
40 40 "use strict";
41 41
42 42 $('#ipython-main-app').addClass('border-box-sizing');
43 43 $('div#notebook_panel').addClass('border-box-sizing');
44 44
45 var options = {
45 var common_options = {
46 46 base_url : utils.get_body_data("baseUrl"),
47 47 notebook_path : utils.get_body_data("notebookPath"),
48 48 notebook_name : utils.get_body_data('notebookName')
49 49 };
50 50
51 51 var user_config = $.extend({}, config.default_config);
52 52 var page = new page.Page();
53 53 var layout_manager = new layoutmanager.LayoutManager();
54 54 var events = $([new events.Events()]);
55 var pager = new pager.Pager('div#pager', 'div#pager_splitter', layout_manager, events);
56 var keyboard_manager = new keyboardmanager.KeyboardManager(pager, events);
55 var pager = new pager.Pager('div#pager', 'div#pager_splitter', {
56 layout_manager: layout_manager,
57 events: events});
58 var keyboard_manager = new keyboardmanager.KeyboardManager({
59 pager: pager,
60 events: events});
57 61 var save_widget = new savewidget.SaveWidget('span#save_widget', events);
58 var notebook = new notebook.Notebook('div#notebook', options, events, keyboard_manager, save_widget, user_config);
59 var login_widget = new loginwidget.LoginWidget('span#login_widget', options);
60 var toolbar = new maintoolbar.MainToolBar('#maintoolbar-container', layout_manager, notebook, events);
61 var quick_help = new quickhelp.QuickHelp(undefined, keyboard_manager, events);
62 var menubar = new menubar.MenuBar('#menubar', options, notebook, layout_manager, events, save_widget, quick_help);
63 var notification_area = new notificationarea.NotificationArea('#notification_area', events, save_widget, notebook);
62 var notebook = new notebook.Notebook('div#notebook', $.extend({
63 events: events,
64 keyboard_manager: keyboard_manager,
65 save_widget: save_widget,
66 config: user_config},
67 common_options));
68 var login_widget = new loginwidget.LoginWidget('span#login_widget', common_options);
69 var toolbar = new maintoolbar.MainToolBar('#maintoolbar-container', {
70 notebook: notebook,
71 events: events});
72 var quick_help = new quickhelp.QuickHelp({
73 keyboard_manager: keyboard_manager,
74 events: events});
75 var menubar = new menubar.MenuBar('#menubar', $.extend({
76 notebook: notebook,
77 layout_manager: layout_manager,
78 events: events,
79 save_widget: save_widget,
80 quick_help: quick_help},
81 common_options));
82 var notification_area = new notificationarea.NotificationArea(
83 '#notification_area', {
84 events: events,
85 save_widget: save_widget,
86 notebook: notebook});
64 87 notification_area.init_notification_widgets();
65 88
66 89 $('body').append('<div id="fonttest"><pre><span id="test1">x</span>'+
67 90 '<span id="test2" style="font-weight: bold;">x</span>'+
68 91 '<span id="test3" style="font-style: italic;">x</span></pre></div>');
69 92 var nh = $('#test1').innerHeight();
70 93 var bh = $('#test2').innerHeight();
71 94 var ih = $('#test3').innerHeight();
72 95 if(nh != bh || nh != ih) {
73 96 $('head').append('<style>.CodeMirror span { vertical-align: bottom; }</style>');
74 97 }
75 98 $('#fonttest').remove();
76 99
77 100 page.show();
78 101
79 102 layout_manager.do_resize();
80 103 var first_load = function () {
81 104 layout_manager.do_resize();
82 105 var hash = document.location.hash;
83 106 if (hash) {
84 107 document.location.hash = '';
85 108 document.location.hash = hash;
86 109 }
87 110 notebook.set_autosave_interval(notebook.minimum_autosave_interval);
88 111 // only do this once
89 112 events.off('notebook_loaded.Notebook', first_load);
90 113 };
91 114
92 115 events.on('notebook_loaded.Notebook', first_load);
93 116 events.trigger('app_initialized.NotebookApp');
94 notebook.load_notebook(options.notebook_name, options.notebook_path);
117 notebook.load_notebook(common_options.notebook_name, common_options.notebook_path);
95 118
96 119 ipython.page = page;
97 120 ipython.layout_manager = layout_manager;
98 121 ipython.notebook = notebook;
99 122 ipython.pager = pager;
100 123 ipython.quick_help = quick_help;
101 124 ipython.login_widget = login_widget;
102 125 ipython.menubar = menubar;
103 126 ipython.toolbar = toolbar;
104 127 ipython.notification_area = notification_area;
105 128 ipython.events = events;
106 129 ipython.keyboard_manager = keyboard_manager;
107 130 ipython.save_widget = save_widget;
108 131 ipython.config = user_config;
109 132 });
@@ -1,221 +1,221 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'notebook/js/toolbar',
8 8 'notebook/js/celltoolbar',
9 9 ], function(IPython, $, toolbar, celltoolbar) {
10 10 "use strict";
11 11
12 var MainToolBar = function (selector, layout_manager, notebook, events) {
12 var MainToolBar = function (selector, options) {
13 13 toolbar.ToolBar.apply(this, arguments);
14 this.events = events;
15 this.notebook = notebook;
14 this.events = options.events;
15 this.notebook = options.notebook;
16 16 this.construct();
17 17 this.add_celltype_list();
18 18 this.add_celltoolbar_list();
19 19 this.bind_events();
20 20 };
21 21
22 22 MainToolBar.prototype = new toolbar.ToolBar();
23 23
24 24 MainToolBar.prototype.construct = function () {
25 25 var that = this;
26 26 this.add_buttons_group([
27 27 {
28 28 id : 'save_b',
29 29 label : 'Save and Checkpoint',
30 30 icon : 'icon-save',
31 31 callback : function () {
32 32 that.notebook.save_checkpoint();
33 33 }
34 34 }
35 35 ]);
36 36
37 37 this.add_buttons_group([
38 38 {
39 39 id : 'insert_below_b',
40 40 label : 'Insert Cell Below',
41 41 icon : 'icon-plus-sign',
42 42 callback : function () {
43 43 that.notebook.insert_cell_below('code');
44 44 that.notebook.select_next();
45 45 that.notebook.focus_cell();
46 46 }
47 47 }
48 48 ],'insert_above_below');
49 49
50 50 this.add_buttons_group([
51 51 {
52 52 id : 'cut_b',
53 53 label : 'Cut Cell',
54 54 icon : 'icon-cut',
55 55 callback : function () {
56 56 that.notebook.cut_cell();
57 57 }
58 58 },
59 59 {
60 60 id : 'copy_b',
61 61 label : 'Copy Cell',
62 62 icon : 'icon-copy',
63 63 callback : function () {
64 64 that.notebook.copy_cell();
65 65 }
66 66 },
67 67 {
68 68 id : 'paste_b',
69 69 label : 'Paste Cell Below',
70 70 icon : 'icon-paste',
71 71 callback : function () {
72 72 that.notebook.paste_cell_below();
73 73 }
74 74 }
75 75 ],'cut_copy_paste');
76 76
77 77 this.add_buttons_group([
78 78 {
79 79 id : 'move_up_b',
80 80 label : 'Move Cell Up',
81 81 icon : 'icon-arrow-up',
82 82 callback : function () {
83 83 that.notebook.move_cell_up();
84 84 }
85 85 },
86 86 {
87 87 id : 'move_down_b',
88 88 label : 'Move Cell Down',
89 89 icon : 'icon-arrow-down',
90 90 callback : function () {
91 91 that.notebook.move_cell_down();
92 92 }
93 93 }
94 94 ],'move_up_down');
95 95
96 96
97 97 this.add_buttons_group([
98 98 {
99 99 id : 'run_b',
100 100 label : 'Run Cell',
101 101 icon : 'icon-play',
102 102 callback : function () {
103 103 // emulate default shift-enter behavior
104 104 that.notebook.execute_cell_and_select_below();
105 105 }
106 106 },
107 107 {
108 108 id : 'interrupt_b',
109 109 label : 'Interrupt',
110 110 icon : 'icon-stop',
111 111 callback : function () {
112 112 that.notebook.session.interrupt_kernel();
113 113 }
114 114 },
115 115 {
116 116 id : 'repeat_b',
117 117 label : 'Restart Kernel',
118 118 icon : 'icon-repeat',
119 119 callback : function () {
120 120 that.notebook.restart_kernel();
121 121 }
122 122 }
123 123 ],'run_int');
124 124 };
125 125
126 126 MainToolBar.prototype.add_celltype_list = function () {
127 127 this.element
128 128 .append($('<select/>')
129 129 .attr('id','cell_type')
130 130 .addClass('form-control select-xs')
131 131 // .addClass('ui-widget-content')
132 132 .append($('<option/>').attr('value','code').text('Code'))
133 133 .append($('<option/>').attr('value','markdown').text('Markdown'))
134 134 .append($('<option/>').attr('value','raw').text('Raw NBConvert'))
135 135 .append($('<option/>').attr('value','heading1').text('Heading 1'))
136 136 .append($('<option/>').attr('value','heading2').text('Heading 2'))
137 137 .append($('<option/>').attr('value','heading3').text('Heading 3'))
138 138 .append($('<option/>').attr('value','heading4').text('Heading 4'))
139 139 .append($('<option/>').attr('value','heading5').text('Heading 5'))
140 140 .append($('<option/>').attr('value','heading6').text('Heading 6'))
141 141 );
142 142 };
143 143
144 144
145 145 MainToolBar.prototype.add_celltoolbar_list = function () {
146 146 var label = $('<span/>').addClass("navbar-text").text('Cell Toolbar:');
147 147 var select = $('<select/>')
148 148 // .addClass('ui-widget-content')
149 149 .attr('id', 'ctb_select')
150 150 .addClass('form-control select-xs')
151 151 .append($('<option/>').attr('value', '').text('None'));
152 152 this.element.append(label).append(select);
153 153 select.change(function() {
154 154 var val = $(this).val();
155 155 if (val ==='') {
156 156 celltoolbar.CellToolbar.global_hide();
157 157 delete that.notebook.metadata.celltoolbar;
158 158 } else {
159 159 celltoolbar.CellToolbar.global_show();
160 160 celltoolbar.CellToolbar.activate_preset(val);
161 161 that.notebook.metadata.celltoolbar = val;
162 162 }
163 163 });
164 164 // Setup the currently registered presets.
165 165 var presets = celltoolbar.CellToolbar.list_presets();
166 166 for (var i=0; i<presets.length; i++) {
167 167 var name = presets[i];
168 168 select.append($('<option/>').attr('value', name).text(name));
169 169 }
170 170 // Setup future preset registrations.
171 171 this.events.on('preset_added.CellToolbar', function (event, data) {
172 172 var name = data.name;
173 173 select.append($('<option/>').attr('value', name).text(name));
174 174 });
175 175 // Update select value when a preset is activated.
176 176 this.events.on('preset_activated.CellToolbar', function (event, data) {
177 177 if (select.val() !== data.name)
178 178 select.val(data.name);
179 179 });
180 180 };
181 181
182 182
183 183 MainToolBar.prototype.bind_events = function () {
184 184 var that = this;
185 185
186 186 this.element.find('#cell_type').change(function () {
187 187 var cell_type = $(this).val();
188 188 if (cell_type === 'code') {
189 189 that.notebook.to_code();
190 190 } else if (cell_type === 'markdown') {
191 191 that.notebook.to_markdown();
192 192 } else if (cell_type === 'raw') {
193 193 that.notebook.to_raw();
194 194 } else if (cell_type === 'heading1') {
195 195 that.notebook.to_heading(undefined, 1);
196 196 } else if (cell_type === 'heading2') {
197 197 that.notebook.to_heading(undefined, 2);
198 198 } else if (cell_type === 'heading3') {
199 199 that.notebook.to_heading(undefined, 3);
200 200 } else if (cell_type === 'heading4') {
201 201 that.notebook.to_heading(undefined, 4);
202 202 } else if (cell_type === 'heading5') {
203 203 that.notebook.to_heading(undefined, 5);
204 204 } else if (cell_type === 'heading6') {
205 205 that.notebook.to_heading(undefined, 6);
206 206 }
207 207 });
208 208 this.events.on('selected_cell_type_changed.Notebook', function (event, data) {
209 209 if (data.cell_type === 'heading') {
210 210 that.element.find('#cell_type').val(data.cell_type+data.level);
211 211 } else {
212 212 that.element.find('#cell_type').val(data.cell_type);
213 213 }
214 214 });
215 215 };
216 216
217 217 // Backwards compatability.
218 218 IPython.MainToolBar = MainToolBar;
219 219
220 220 return {'MainToolBar': MainToolBar};
221 221 });
@@ -1,347 +1,347 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'notebook/js/tour',
9 9 'components/bootstrap-tour/build/js/bootstrap-tour.min',
10 10 ], function(IPython, $, utils, tour) {
11 11 "use strict";
12 12
13 13 /**
14 14 * A MenuBar Class to generate the menubar of IPython notebook
15 15 * @Class MenuBar
16 16 *
17 17 * @constructor
18 18 *
19 19 *
20 20 * @param selector {string} selector for the menubar element in DOM
21 21 * @param {object} [options]
22 22 * @param [options.base_url] {String} String to use for the
23 23 * base project url. Default is to inspect
24 24 * $('body').data('baseUrl');
25 25 * does not support change for now is set through this option
26 26 */
27 var MenuBar = function (selector, options, notebook, layout_manager, events, save_widget, quick_help) {
27 var MenuBar = function (selector, options) {
28 28 options = options || {};
29 29 this.base_url = options.base_url || utils.get_body_data("baseUrl");
30 30 this.selector = selector;
31 this.notebook = notebook;
32 this.layout_manager = layout_manager;
33 this.events = events;
34 this.save_widget = save_widget;
35 this.quick_help = quick_help;
31 this.notebook = options.notebook;
32 this.layout_manager = options.layout_manager;
33 this.events = options.events;
34 this.save_widget = options.save_widget;
35 this.quick_help = options.quick_help;
36 36
37 37 try {
38 this.tour = new tour.Tour(notebook, events);
38 this.tour = new tour.Tour(this.notebook, this.events);
39 39 } catch (e) {
40 40 this.tour = undefined;
41 41 console.log("Failed to instantiate Notebook Tour", e);
42 42 }
43 43
44 44 if (this.selector !== undefined) {
45 45 this.element = $(selector);
46 46 this.style();
47 47 this.bind_events();
48 48 }
49 49 };
50 50
51 51 MenuBar.prototype.style = function () {
52 52 var that = this;
53 53 this.element.addClass('border-box-sizing');
54 54 this.element.find("li").click(function (event, ui) {
55 55 // The selected cell loses focus when the menu is entered, so we
56 56 // re-select it upon selection.
57 57 var i = that.notebook.get_selected_index();
58 58 that.notebook.select(i);
59 59 }
60 60 );
61 61 };
62 62
63 63 MenuBar.prototype._nbconvert = function (format, download) {
64 64 download = download || false;
65 65 var notebook_path = this.notebook.notebook_path;
66 66 var notebook_name = this.notebook.notebook_name;
67 67 if (this.notebook.dirty) {
68 68 this.notebook.save_notebook({async : false});
69 69 }
70 70 var url = utils.url_join_encode(
71 71 this.base_url,
72 72 'nbconvert',
73 73 format,
74 74 notebook_path,
75 75 notebook_name
76 76 ) + "?download=" + download.toString();
77 77
78 78 window.open(url);
79 79 };
80 80
81 81 MenuBar.prototype.bind_events = function () {
82 82 // File
83 83 var that = this;
84 84 this.element.find('#new_notebook').click(function () {
85 85 that.notebook.new_notebook();
86 86 });
87 87 this.element.find('#open_notebook').click(function () {
88 88 window.open(utils.url_join_encode(
89 89 that.notebook.base_url,
90 90 'tree',
91 91 that.notebook.notebook_path
92 92 ));
93 93 });
94 94 this.element.find('#copy_notebook').click(function () {
95 95 that.notebook.copy_notebook();
96 96 return false;
97 97 });
98 98 this.element.find('#download_ipynb').click(function () {
99 99 var base_url = that.notebook.base_url;
100 100 var notebook_path = that.notebook.notebook_path;
101 101 var notebook_name = that.notebook.notebook_name;
102 102 if (that.notebook.dirty) {
103 103 that.notebook.save_notebook({async : false});
104 104 }
105 105
106 106 var url = utils.url_join_encode(
107 107 base_url,
108 108 'files',
109 109 notebook_path,
110 110 notebook_name
111 111 );
112 112 window.location.assign(url);
113 113 });
114 114
115 115 this.element.find('#print_preview').click(function () {
116 116 that._nbconvert('html', false);
117 117 });
118 118
119 119 this.element.find('#download_py').click(function () {
120 120 that._nbconvert('python', true);
121 121 });
122 122
123 123 this.element.find('#download_html').click(function () {
124 124 that._nbconvert('html', true);
125 125 });
126 126
127 127 this.element.find('#download_rst').click(function () {
128 128 that._nbconvert('rst', true);
129 129 });
130 130
131 131 this.element.find('#download_pdf').click(function () {
132 132 that._nbconvert('pdf', true);
133 133 });
134 134
135 135 this.element.find('#rename_notebook').click(function () {
136 136 that.save_widget.rename_notebook();
137 137 });
138 138 this.element.find('#save_checkpoint').click(function () {
139 139 that.notebook.save_checkpoint();
140 140 });
141 141 this.element.find('#restore_checkpoint').click(function () {
142 142 });
143 143 this.element.find('#trust_notebook').click(function () {
144 144 that.notebook.trust_notebook();
145 145 });
146 146 this.events.on('trust_changed.Notebook', function (event, trusted) {
147 147 if (trusted) {
148 148 that.element.find('#trust_notebook')
149 149 .addClass("disabled")
150 150 .find("a").text("Trusted Notebook");
151 151 } else {
152 152 that.element.find('#trust_notebook')
153 153 .removeClass("disabled")
154 154 .find("a").text("Trust Notebook");
155 155 }
156 156 });
157 157 this.element.find('#kill_and_exit').click(function () {
158 158 that.notebook.session.delete();
159 159 setTimeout(function(){
160 160 // allow closing of new tabs in Chromium, impossible in FF
161 161 window.open('', '_self', '');
162 162 window.close();
163 163 }, 500);
164 164 });
165 165 // Edit
166 166 this.element.find('#cut_cell').click(function () {
167 167 that.notebook.cut_cell();
168 168 });
169 169 this.element.find('#copy_cell').click(function () {
170 170 that.notebook.copy_cell();
171 171 });
172 172 this.element.find('#delete_cell').click(function () {
173 173 that.notebook.delete_cell();
174 174 });
175 175 this.element.find('#undelete_cell').click(function () {
176 176 that.notebook.undelete_cell();
177 177 });
178 178 this.element.find('#split_cell').click(function () {
179 179 that.notebook.split_cell();
180 180 });
181 181 this.element.find('#merge_cell_above').click(function () {
182 182 that.notebook.merge_cell_above();
183 183 });
184 184 this.element.find('#merge_cell_below').click(function () {
185 185 that.notebook.merge_cell_below();
186 186 });
187 187 this.element.find('#move_cell_up').click(function () {
188 188 that.notebook.move_cell_up();
189 189 });
190 190 this.element.find('#move_cell_down').click(function () {
191 191 that.notebook.move_cell_down();
192 192 });
193 193 this.element.find('#edit_nb_metadata').click(function () {
194 194 that.notebook.edit_metadata();
195 195 });
196 196
197 197 // View
198 198 this.element.find('#toggle_header').click(function () {
199 199 $('div#header').toggle();
200 200 that.layout_manager.do_resize();
201 201 });
202 202 this.element.find('#toggle_toolbar').click(function () {
203 203 $('div#maintoolbar').toggle();
204 204 that.layout_manager.do_resize();
205 205 });
206 206 // Insert
207 207 this.element.find('#insert_cell_above').click(function () {
208 208 that.notebook.insert_cell_above('code');
209 209 that.notebook.select_prev();
210 210 });
211 211 this.element.find('#insert_cell_below').click(function () {
212 212 that.notebook.insert_cell_below('code');
213 213 that.notebook.select_next();
214 214 });
215 215 // Cell
216 216 this.element.find('#run_cell').click(function () {
217 217 that.notebook.execute_cell();
218 218 });
219 219 this.element.find('#run_cell_select_below').click(function () {
220 220 that.notebook.execute_cell_and_select_below();
221 221 });
222 222 this.element.find('#run_cell_insert_below').click(function () {
223 223 that.notebook.execute_cell_and_insert_below();
224 224 });
225 225 this.element.find('#run_all_cells').click(function () {
226 226 that.notebook.execute_all_cells();
227 227 });
228 228 this.element.find('#run_all_cells_above').click(function () {
229 229 that.notebook.execute_cells_above();
230 230 });
231 231 this.element.find('#run_all_cells_below').click(function () {
232 232 that.notebook.execute_cells_below();
233 233 });
234 234 this.element.find('#to_code').click(function () {
235 235 that.notebook.to_code();
236 236 });
237 237 this.element.find('#to_markdown').click(function () {
238 238 that.notebook.to_markdown();
239 239 });
240 240 this.element.find('#to_raw').click(function () {
241 241 that.notebook.to_raw();
242 242 });
243 243 this.element.find('#to_heading1').click(function () {
244 244 that.notebook.to_heading(undefined, 1);
245 245 });
246 246 this.element.find('#to_heading2').click(function () {
247 247 that.notebook.to_heading(undefined, 2);
248 248 });
249 249 this.element.find('#to_heading3').click(function () {
250 250 that.notebook.to_heading(undefined, 3);
251 251 });
252 252 this.element.find('#to_heading4').click(function () {
253 253 that.notebook.to_heading(undefined, 4);
254 254 });
255 255 this.element.find('#to_heading5').click(function () {
256 256 that.notebook.to_heading(undefined, 5);
257 257 });
258 258 this.element.find('#to_heading6').click(function () {
259 259 that.notebook.to_heading(undefined, 6);
260 260 });
261 261
262 262 this.element.find('#toggle_current_output').click(function () {
263 263 that.notebook.toggle_output();
264 264 });
265 265 this.element.find('#toggle_current_output_scroll').click(function () {
266 266 that.notebook.toggle_output_scroll();
267 267 });
268 268 this.element.find('#clear_current_output').click(function () {
269 269 that.notebook.clear_output();
270 270 });
271 271
272 272 this.element.find('#toggle_all_output').click(function () {
273 273 that.notebook.toggle_all_output();
274 274 });
275 275 this.element.find('#toggle_all_output_scroll').click(function () {
276 276 that.notebook.toggle_all_output_scroll();
277 277 });
278 278 this.element.find('#clear_all_output').click(function () {
279 279 that.notebook.clear_all_output();
280 280 });
281 281
282 282 // Kernel
283 283 this.element.find('#int_kernel').click(function () {
284 284 that.notebook.session.interrupt_kernel();
285 285 });
286 286 this.element.find('#restart_kernel').click(function () {
287 287 that.notebook.restart_kernel();
288 288 });
289 289 // Help
290 290 if (this.tour) {
291 291 this.element.find('#notebook_tour').click(function () {
292 292 that.tour.start();
293 293 });
294 294 } else {
295 295 this.element.find('#notebook_tour').addClass("disabled");
296 296 }
297 297 this.element.find('#keyboard_shortcuts').click(function () {
298 298 that.quick_help.show_keyboard_shortcuts();
299 299 });
300 300
301 301 this.update_restore_checkpoint(null);
302 302
303 303 this.events.on('checkpoints_listed.Notebook', function (event, data) {
304 304 that.update_restore_checkpoint(that.notebook.checkpoints);
305 305 });
306 306
307 307 this.events.on('checkpoint_created.Notebook', function (event, data) {
308 308 that.update_restore_checkpoint(that.notebook.checkpoints);
309 309 });
310 310 };
311 311
312 312 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
313 313 var ul = this.element.find("#restore_checkpoint").find("ul");
314 314 ul.empty();
315 315 if (!checkpoints || checkpoints.length === 0) {
316 316 ul.append(
317 317 $("<li/>")
318 318 .addClass("disabled")
319 319 .append(
320 320 $("<a/>")
321 321 .text("No checkpoints")
322 322 )
323 323 );
324 324 return;
325 325 }
326 326
327 327 var that = this;
328 328 checkpoints.map(function (checkpoint) {
329 329 var d = new Date(checkpoint.last_modified);
330 330 ul.append(
331 331 $("<li/>").append(
332 332 $("<a/>")
333 333 .attr("href", "#")
334 334 .text(d.format("mmm dd HH:MM:ss"))
335 335 .click(function () {
336 336 that.notebook.restore_checkpoint_dialog(checkpoint);
337 337 })
338 338 )
339 339 );
340 340 });
341 341 };
342 342
343 343 // Backwards compatability.
344 344 IPython.MenuBar = MenuBar;
345 345
346 346 return {'MenuBar': MenuBar};
347 347 });
@@ -1,2481 +1,2493 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 'notebook/js/textcell',
10 10 'notebook/js/codecell',
11 11 'services/sessions/js/session',
12 12 'notebook/js/celltoolbar',
13 13 'components/marked/lib/marked',
14 14 'notebook/js/mathjaxutils',
15 15 'base/js/keyboard',
16 16 'components/jquery-ui/ui/minified/jquery-ui.min',
17 17 'components/bootstrap/js/bootstrap.min',
18 18 ], function (
19 19 IPython,
20 20 $,
21 21 utils,
22 22 dialog,
23 23 cells,
24 24 codecell,
25 25 session,
26 26 celltoolbar,
27 27 marked,
28 28 mathjaxutils,
29 29 keyboard
30 30 ) {
31 31
32 32 /**
33 33 * A notebook contains and manages cells.
34 34 *
35 35 * @class Notebook
36 36 * @constructor
37 37 * @param {String} selector A jQuery selector for the notebook's DOM element
38 38 * @param {Object} [options] A config object
39 39 * @param {Object} [events] An events object
40 40 */
41 var Notebook = function (selector, options, events, keyboard_manager, save_widget, config) {
42 this.config = config;
43 this.events = events;
44 this.keyboard_manager = keyboard_manager;
41 var Notebook = function (selector, options) {
42 this.config = options.config || {};
43 this.base_url = options.base_url;
44 this.notebook_path = options.notebook_path;
45 this.notebook_name = options.notebook_name;
46 this.events = options.events;
47 this.keyboard_manager = options.keyboard_manager;
48 this.save_widget = options.save_widget;
45 49 // TODO: This code smells (and the other `= this` line a couple lines down)
46 50 // We need a better way to deal with circular instance references.
47 keyboard_manager.notebook = this;
48 this.save_widget = save_widget;
49 save_widget.notebook = this;
51 this.keyboard_manager.notebook = this;
52 this.save_widget.notebook = this;
50 53
51 54 mathjaxutils.init();
52 55
53 56 if (marked) {
54 57 marked.setOptions({
55 58 gfm : true,
56 59 tables: true,
57 60 langPrefix: "language-",
58 61 highlight: function(code, lang) {
59 62 if (!lang) {
60 63 // no language, no highlight
61 64 return code;
62 65 }
63 66 var highlighted;
64 67 try {
65 68 highlighted = hljs.highlight(lang, code, false);
66 69 } catch(err) {
67 70 highlighted = hljs.highlightAuto(code);
68 71 }
69 72 return highlighted.value;
70 73 }
71 74 });
72 75 }
73 76
74 77 // Backwards compatability.
75 78 IPython.keyboard_manager = this.keyboard_manager;
76 79 IPython.save_widget = this.save_widget;
77 80 IPython.keyboard = this.keyboard;
78 81
79 this.options = options = options || {};
80 this.base_url = options.base_url;
81 this.notebook_path = options.notebook_path;
82 this.notebook_name = options.notebook_name;
83 82 this.element = $(selector);
84 83 this.element.scroll();
85 84 this.element.data("notebook", this);
86 85 this.next_prompt_number = 1;
87 86 this.session = null;
88 87 this.kernel = null;
89 88 this.clipboard = null;
90 89 this.undelete_backup = null;
91 90 this.undelete_index = null;
92 91 this.undelete_below = false;
93 92 this.paste_enabled = false;
94 93 // It is important to start out in command mode to match the intial mode
95 94 // of the KeyboardManager.
96 95 this.mode = 'command';
97 96 this.set_dirty(false);
98 97 this.metadata = {};
99 98 this._checkpoint_after_save = false;
100 99 this.last_checkpoint = null;
101 100 this.checkpoints = [];
102 101 this.autosave_interval = 0;
103 102 this.autosave_timer = null;
104 103 // autosave *at most* every two minutes
105 104 this.minimum_autosave_interval = 120000;
106 105 // single worksheet for now
107 106 this.worksheet_metadata = {};
108 107 this.notebook_name_blacklist_re = /[\/\\:]/;
109 108 this.nbformat = 3; // Increment this when changing the nbformat
110 109 this.nbformat_minor = 0; // Increment this when changing the nbformat
111 110 this.style();
112 111 this.create_elements();
113 112 this.bind_events();
114 113 this.save_notebook = function() { // don't allow save until notebook_loaded
115 114 this.save_notebook_error(null, null, "Load failed, save is disabled");
116 115 };
117 116 };
118 117
119 118 /**
120 119 * Tweak the notebook's CSS style.
121 120 *
122 121 * @method style
123 122 */
124 123 Notebook.prototype.style = function () {
125 124 $('div#notebook').addClass('border-box-sizing');
126 125 };
127 126
128 127 /**
129 128 * Create an HTML and CSS representation of the notebook.
130 129 *
131 130 * @method create_elements
132 131 */
133 132 Notebook.prototype.create_elements = function () {
134 133 var that = this;
135 134 this.element.attr('tabindex','-1');
136 135 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
137 136 // We add this end_space div to the end of the notebook div to:
138 137 // i) provide a margin between the last cell and the end of the notebook
139 138 // ii) to prevent the div from scrolling up when the last cell is being
140 139 // edited, but is too low on the page, which browsers will do automatically.
141 140 var end_space = $('<div/>').addClass('end_space');
142 141 end_space.dblclick(function (e) {
143 142 var ncells = that.ncells();
144 143 that.insert_cell_below('code',ncells-1);
145 144 });
146 145 this.element.append(this.container);
147 146 this.container.append(end_space);
148 147 };
149 148
150 149 /**
151 150 * Bind JavaScript events: key presses and custom IPython events.
152 151 *
153 152 * @method bind_events
154 153 */
155 154 Notebook.prototype.bind_events = function () {
156 155 var that = this;
157 156
158 157 this.events.on('set_next_input.Notebook', function (event, data) {
159 158 var index = that.find_cell_index(data.cell);
160 159 var new_cell = that.insert_cell_below('code',index);
161 160 new_cell.set_text(data.text);
162 161 that.dirty = true;
163 162 });
164 163
165 164 this.events.on('set_dirty.Notebook', function (event, data) {
166 165 that.dirty = data.value;
167 166 });
168 167
169 168 this.events.on('trust_changed.Notebook', function (event, data) {
170 169 that.trusted = data.value;
171 170 });
172 171
173 172 this.events.on('select.Cell', function (event, data) {
174 173 var index = that.find_cell_index(data.cell);
175 174 that.select(index);
176 175 });
177 176
178 177 this.events.on('edit_mode.Cell', function (event, data) {
179 178 that.handle_edit_mode(data.cell);
180 179 });
181 180
182 181 this.events.on('command_mode.Cell', function (event, data) {
183 182 that.handle_command_mode(data.cell);
184 183 });
185 184
186 185 this.events.on('status_autorestarting.Kernel', function () {
187 186 dialog.modal({
188 187 title: "Kernel Restarting",
189 188 body: "The kernel appears to have died. It will restart automatically.",
190 189 buttons: {
191 190 OK : {
192 191 class : "btn-primary"
193 192 }
194 193 }
195 194 });
196 195 });
197 196
198 197 var collapse_time = function (time) {
199 198 var app_height = $('#ipython-main-app').height(); // content height
200 199 var splitter_height = $('div#pager_splitter').outerHeight(true);
201 200 var new_height = app_height - splitter_height;
202 201 that.element.animate({height : new_height + 'px'}, time);
203 202 };
204 203
205 204 this.element.bind('collapse_pager', function (event, extrap) {
206 205 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
207 206 collapse_time(time);
208 207 });
209 208
210 209 var expand_time = function (time) {
211 210 var app_height = $('#ipython-main-app').height(); // content height
212 211 var splitter_height = $('div#pager_splitter').outerHeight(true);
213 212 var pager_height = $('div#pager').outerHeight(true);
214 213 var new_height = app_height - pager_height - splitter_height;
215 214 that.element.animate({height : new_height + 'px'}, time);
216 215 };
217 216
218 217 this.element.bind('expand_pager', function (event, extrap) {
219 218 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
220 219 expand_time(time);
221 220 });
222 221
223 222 // Firefox 22 broke $(window).on("beforeunload")
224 223 // I'm not sure why or how.
225 224 window.onbeforeunload = function (e) {
226 225 // TODO: Make killing the kernel configurable.
227 226 var kill_kernel = false;
228 227 if (kill_kernel) {
229 228 that.session.kill_kernel();
230 229 }
231 230 // if we are autosaving, trigger an autosave on nav-away.
232 231 // still warn, because if we don't the autosave may fail.
233 232 if (that.dirty) {
234 233 if ( that.autosave_interval ) {
235 234 // schedule autosave in a timeout
236 235 // this gives you a chance to forcefully discard changes
237 236 // by reloading the page if you *really* want to.
238 237 // the timer doesn't start until you *dismiss* the dialog.
239 238 setTimeout(function () {
240 239 if (that.dirty) {
241 240 that.save_notebook();
242 241 }
243 242 }, 1000);
244 243 return "Autosave in progress, latest changes may be lost.";
245 244 } else {
246 245 return "Unsaved changes will be lost.";
247 246 }
248 247 }
249 248 // Null is the *only* return value that will make the browser not
250 249 // pop up the "don't leave" dialog.
251 250 return null;
252 251 };
253 252 };
254 253
255 254 /**
256 255 * Set the dirty flag, and trigger the set_dirty.Notebook event
257 256 *
258 257 * @method set_dirty
259 258 */
260 259 Notebook.prototype.set_dirty = function (value) {
261 260 if (value === undefined) {
262 261 value = true;
263 262 }
264 263 if (this.dirty == value) {
265 264 return;
266 265 }
267 266 this.events.trigger('set_dirty.Notebook', {value: value});
268 267 };
269 268
270 269 /**
271 270 * Scroll the top of the page to a given cell.
272 271 *
273 272 * @method scroll_to_cell
274 273 * @param {Number} cell_number An index of the cell to view
275 274 * @param {Number} time Animation time in milliseconds
276 275 * @return {Number} Pixel offset from the top of the container
277 276 */
278 277 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
279 278 var cells = this.get_cells();
280 279 time = time || 0;
281 280 cell_number = Math.min(cells.length-1,cell_number);
282 281 cell_number = Math.max(0 ,cell_number);
283 282 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
284 283 this.element.animate({scrollTop:scroll_value}, time);
285 284 return scroll_value;
286 285 };
287 286
288 287 /**
289 288 * Scroll to the bottom of the page.
290 289 *
291 290 * @method scroll_to_bottom
292 291 */
293 292 Notebook.prototype.scroll_to_bottom = function () {
294 293 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
295 294 };
296 295
297 296 /**
298 297 * Scroll to the top of the page.
299 298 *
300 299 * @method scroll_to_top
301 300 */
302 301 Notebook.prototype.scroll_to_top = function () {
303 302 this.element.animate({scrollTop:0}, 0);
304 303 };
305 304
306 305 // Edit Notebook metadata
307 306
308 307 Notebook.prototype.edit_metadata = function () {
309 308 var that = this;
310 309 dialog.edit_metadata(this.metadata, function (md) {
311 310 that.metadata = md;
312 311 }, 'Notebook');
313 312 };
314 313
315 314 // Cell indexing, retrieval, etc.
316 315
317 316 /**
318 317 * Get all cell elements in the notebook.
319 318 *
320 319 * @method get_cell_elements
321 320 * @return {jQuery} A selector of all cell elements
322 321 */
323 322 Notebook.prototype.get_cell_elements = function () {
324 323 return this.container.children("div.cell");
325 324 };
326 325
327 326 /**
328 327 * Get a particular cell element.
329 328 *
330 329 * @method get_cell_element
331 330 * @param {Number} index An index of a cell to select
332 331 * @return {jQuery} A selector of the given cell.
333 332 */
334 333 Notebook.prototype.get_cell_element = function (index) {
335 334 var result = null;
336 335 var e = this.get_cell_elements().eq(index);
337 336 if (e.length !== 0) {
338 337 result = e;
339 338 }
340 339 return result;
341 340 };
342 341
343 342 /**
344 343 * Try to get a particular cell by msg_id.
345 344 *
346 345 * @method get_msg_cell
347 346 * @param {String} msg_id A message UUID
348 347 * @return {Cell} Cell or null if no cell was found.
349 348 */
350 349 Notebook.prototype.get_msg_cell = function (msg_id) {
351 350 return codecell.CodeCell.msg_cells[msg_id] || null;
352 351 };
353 352
354 353 /**
355 354 * Count the cells in this notebook.
356 355 *
357 356 * @method ncells
358 357 * @return {Number} The number of cells in this notebook
359 358 */
360 359 Notebook.prototype.ncells = function () {
361 360 return this.get_cell_elements().length;
362 361 };
363 362
364 363 /**
365 364 * Get all Cell objects in this notebook.
366 365 *
367 366 * @method get_cells
368 367 * @return {Array} This notebook's Cell objects
369 368 */
370 369 // TODO: we are often calling cells as cells()[i], which we should optimize
371 370 // to cells(i) or a new method.
372 371 Notebook.prototype.get_cells = function () {
373 372 return this.get_cell_elements().toArray().map(function (e) {
374 373 return $(e).data("cell");
375 374 });
376 375 };
377 376
378 377 /**
379 378 * Get a Cell object from this notebook.
380 379 *
381 380 * @method get_cell
382 381 * @param {Number} index An index of a cell to retrieve
383 382 * @return {Cell} A particular cell
384 383 */
385 384 Notebook.prototype.get_cell = function (index) {
386 385 var result = null;
387 386 var ce = this.get_cell_element(index);
388 387 if (ce !== null) {
389 388 result = ce.data('cell');
390 389 }
391 390 return result;
392 391 };
393 392
394 393 /**
395 394 * Get the cell below a given cell.
396 395 *
397 396 * @method get_next_cell
398 397 * @param {Cell} cell The provided cell
399 398 * @return {Cell} The next cell
400 399 */
401 400 Notebook.prototype.get_next_cell = function (cell) {
402 401 var result = null;
403 402 var index = this.find_cell_index(cell);
404 403 if (this.is_valid_cell_index(index+1)) {
405 404 result = this.get_cell(index+1);
406 405 }
407 406 return result;
408 407 };
409 408
410 409 /**
411 410 * Get the cell above a given cell.
412 411 *
413 412 * @method get_prev_cell
414 413 * @param {Cell} cell The provided cell
415 414 * @return {Cell} The previous cell
416 415 */
417 416 Notebook.prototype.get_prev_cell = function (cell) {
418 417 // TODO: off-by-one
419 418 // nb.get_prev_cell(nb.get_cell(1)) is null
420 419 var result = null;
421 420 var index = this.find_cell_index(cell);
422 421 if (index !== null && index > 1) {
423 422 result = this.get_cell(index-1);
424 423 }
425 424 return result;
426 425 };
427 426
428 427 /**
429 428 * Get the numeric index of a given cell.
430 429 *
431 430 * @method find_cell_index
432 431 * @param {Cell} cell The provided cell
433 432 * @return {Number} The cell's numeric index
434 433 */
435 434 Notebook.prototype.find_cell_index = function (cell) {
436 435 var result = null;
437 436 this.get_cell_elements().filter(function (index) {
438 437 if ($(this).data("cell") === cell) {
439 438 result = index;
440 439 }
441 440 });
442 441 return result;
443 442 };
444 443
445 444 /**
446 445 * Get a given index , or the selected index if none is provided.
447 446 *
448 447 * @method index_or_selected
449 448 * @param {Number} index A cell's index
450 449 * @return {Number} The given index, or selected index if none is provided.
451 450 */
452 451 Notebook.prototype.index_or_selected = function (index) {
453 452 var i;
454 453 if (index === undefined || index === null) {
455 454 i = this.get_selected_index();
456 455 if (i === null) {
457 456 i = 0;
458 457 }
459 458 } else {
460 459 i = index;
461 460 }
462 461 return i;
463 462 };
464 463
465 464 /**
466 465 * Get the currently selected cell.
467 466 * @method get_selected_cell
468 467 * @return {Cell} The selected cell
469 468 */
470 469 Notebook.prototype.get_selected_cell = function () {
471 470 var index = this.get_selected_index();
472 471 return this.get_cell(index);
473 472 };
474 473
475 474 /**
476 475 * Check whether a cell index is valid.
477 476 *
478 477 * @method is_valid_cell_index
479 478 * @param {Number} index A cell index
480 479 * @return True if the index is valid, false otherwise
481 480 */
482 481 Notebook.prototype.is_valid_cell_index = function (index) {
483 482 if (index !== null && index >= 0 && index < this.ncells()) {
484 483 return true;
485 484 } else {
486 485 return false;
487 486 }
488 487 };
489 488
490 489 /**
491 490 * Get the index of the currently selected cell.
492 491
493 492 * @method get_selected_index
494 493 * @return {Number} The selected cell's numeric index
495 494 */
496 495 Notebook.prototype.get_selected_index = function () {
497 496 var result = null;
498 497 this.get_cell_elements().filter(function (index) {
499 498 if ($(this).data("cell").selected === true) {
500 499 result = index;
501 500 }
502 501 });
503 502 return result;
504 503 };
505 504
506 505
507 506 // Cell selection.
508 507
509 508 /**
510 509 * Programmatically select a cell.
511 510 *
512 511 * @method select
513 512 * @param {Number} index A cell's index
514 513 * @return {Notebook} This notebook
515 514 */
516 515 Notebook.prototype.select = function (index) {
517 516 if (this.is_valid_cell_index(index)) {
518 517 var sindex = this.get_selected_index();
519 518 if (sindex !== null && index !== sindex) {
520 519 // If we are about to select a different cell, make sure we are
521 520 // first in command mode.
522 521 if (this.mode !== 'command') {
523 522 this.command_mode();
524 523 }
525 524 this.get_cell(sindex).unselect();
526 525 }
527 526 var cell = this.get_cell(index);
528 527 cell.select();
529 528 if (cell.cell_type === 'heading') {
530 529 this.events.trigger('selected_cell_type_changed.Notebook',
531 530 {'cell_type':cell.cell_type,level:cell.level}
532 531 );
533 532 } else {
534 533 this.events.trigger('selected_cell_type_changed.Notebook',
535 534 {'cell_type':cell.cell_type}
536 535 );
537 536 }
538 537 }
539 538 return this;
540 539 };
541 540
542 541 /**
543 542 * Programmatically select the next cell.
544 543 *
545 544 * @method select_next
546 545 * @return {Notebook} This notebook
547 546 */
548 547 Notebook.prototype.select_next = function () {
549 548 var index = this.get_selected_index();
550 549 this.select(index+1);
551 550 return this;
552 551 };
553 552
554 553 /**
555 554 * Programmatically select the previous cell.
556 555 *
557 556 * @method select_prev
558 557 * @return {Notebook} This notebook
559 558 */
560 559 Notebook.prototype.select_prev = function () {
561 560 var index = this.get_selected_index();
562 561 this.select(index-1);
563 562 return this;
564 563 };
565 564
566 565
567 566 // Edit/Command mode
568 567
569 568 /**
570 569 * Gets the index of the cell that is in edit mode.
571 570 *
572 571 * @method get_edit_index
573 572 *
574 573 * @return index {int}
575 574 **/
576 575 Notebook.prototype.get_edit_index = function () {
577 576 var result = null;
578 577 this.get_cell_elements().filter(function (index) {
579 578 if ($(this).data("cell").mode === 'edit') {
580 579 result = index;
581 580 }
582 581 });
583 582 return result;
584 583 };
585 584
586 585 /**
587 586 * Handle when a a cell blurs and the notebook should enter command mode.
588 587 *
589 588 * @method handle_command_mode
590 589 * @param [cell] {Cell} Cell to enter command mode on.
591 590 **/
592 591 Notebook.prototype.handle_command_mode = function (cell) {
593 592 if (this.mode !== 'command') {
594 593 cell.command_mode();
595 594 this.mode = 'command';
596 595 this.events.trigger('command_mode.Notebook');
597 596 this.keyboard_manager.command_mode();
598 597 }
599 598 };
600 599
601 600 /**
602 601 * Make the notebook enter command mode.
603 602 *
604 603 * @method command_mode
605 604 **/
606 605 Notebook.prototype.command_mode = function () {
607 606 var cell = this.get_cell(this.get_edit_index());
608 607 if (cell && this.mode !== 'command') {
609 608 // We don't call cell.command_mode, but rather call cell.focus_cell()
610 609 // which will blur and CM editor and trigger the call to
611 610 // handle_command_mode.
612 611 cell.focus_cell();
613 612 }
614 613 };
615 614
616 615 /**
617 616 * Handle when a cell fires it's edit_mode event.
618 617 *
619 618 * @method handle_edit_mode
620 619 * @param [cell] {Cell} Cell to enter edit mode on.
621 620 **/
622 621 Notebook.prototype.handle_edit_mode = function (cell) {
623 622 if (cell && this.mode !== 'edit') {
624 623 cell.edit_mode();
625 624 this.mode = 'edit';
626 625 this.events.trigger('edit_mode.Notebook');
627 626 this.keyboard_manager.edit_mode();
628 627 }
629 628 };
630 629
631 630 /**
632 631 * Make a cell enter edit mode.
633 632 *
634 633 * @method edit_mode
635 634 **/
636 635 Notebook.prototype.edit_mode = function () {
637 636 var cell = this.get_selected_cell();
638 637 if (cell && this.mode !== 'edit') {
639 638 cell.unrender();
640 639 cell.focus_editor();
641 640 }
642 641 };
643 642
644 643 /**
645 644 * Focus the currently selected cell.
646 645 *
647 646 * @method focus_cell
648 647 **/
649 648 Notebook.prototype.focus_cell = function () {
650 649 var cell = this.get_selected_cell();
651 650 if (cell === null) {return;} // No cell is selected
652 651 cell.focus_cell();
653 652 };
654 653
655 654 // Cell movement
656 655
657 656 /**
658 657 * Move given (or selected) cell up and select it.
659 658 *
660 659 * @method move_cell_up
661 660 * @param [index] {integer} cell index
662 661 * @return {Notebook} This notebook
663 662 **/
664 663 Notebook.prototype.move_cell_up = function (index) {
665 664 var i = this.index_or_selected(index);
666 665 if (this.is_valid_cell_index(i) && i > 0) {
667 666 var pivot = this.get_cell_element(i-1);
668 667 var tomove = this.get_cell_element(i);
669 668 if (pivot !== null && tomove !== null) {
670 669 tomove.detach();
671 670 pivot.before(tomove);
672 671 this.select(i-1);
673 672 var cell = this.get_selected_cell();
674 673 cell.focus_cell();
675 674 }
676 675 this.set_dirty(true);
677 676 }
678 677 return this;
679 678 };
680 679
681 680
682 681 /**
683 682 * Move given (or selected) cell down and select it
684 683 *
685 684 * @method move_cell_down
686 685 * @param [index] {integer} cell index
687 686 * @return {Notebook} This notebook
688 687 **/
689 688 Notebook.prototype.move_cell_down = function (index) {
690 689 var i = this.index_or_selected(index);
691 690 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
692 691 var pivot = this.get_cell_element(i+1);
693 692 var tomove = this.get_cell_element(i);
694 693 if (pivot !== null && tomove !== null) {
695 694 tomove.detach();
696 695 pivot.after(tomove);
697 696 this.select(i+1);
698 697 var cell = this.get_selected_cell();
699 698 cell.focus_cell();
700 699 }
701 700 }
702 701 this.set_dirty();
703 702 return this;
704 703 };
705 704
706 705
707 706 // Insertion, deletion.
708 707
709 708 /**
710 709 * Delete a cell from the notebook.
711 710 *
712 711 * @method delete_cell
713 712 * @param [index] A cell's numeric index
714 713 * @return {Notebook} This notebook
715 714 */
716 715 Notebook.prototype.delete_cell = function (index) {
717 716 var i = this.index_or_selected(index);
718 717 var cell = this.get_selected_cell();
719 718 this.undelete_backup = cell.toJSON();
720 719 $('#undelete_cell').removeClass('disabled');
721 720 if (this.is_valid_cell_index(i)) {
722 721 var old_ncells = this.ncells();
723 722 var ce = this.get_cell_element(i);
724 723 ce.remove();
725 724 if (i === 0) {
726 725 // Always make sure we have at least one cell.
727 726 if (old_ncells === 1) {
728 727 this.insert_cell_below('code');
729 728 }
730 729 this.select(0);
731 730 this.undelete_index = 0;
732 731 this.undelete_below = false;
733 732 } else if (i === old_ncells-1 && i !== 0) {
734 733 this.select(i-1);
735 734 this.undelete_index = i - 1;
736 735 this.undelete_below = true;
737 736 } else {
738 737 this.select(i);
739 738 this.undelete_index = i;
740 739 this.undelete_below = false;
741 740 }
742 741 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
743 742 this.set_dirty(true);
744 743 }
745 744 return this;
746 745 };
747 746
748 747 /**
749 748 * Restore the most recently deleted cell.
750 749 *
751 750 * @method undelete
752 751 */
753 752 Notebook.prototype.undelete_cell = function() {
754 753 if (this.undelete_backup !== null && this.undelete_index !== null) {
755 754 var current_index = this.get_selected_index();
756 755 if (this.undelete_index < current_index) {
757 756 current_index = current_index + 1;
758 757 }
759 758 if (this.undelete_index >= this.ncells()) {
760 759 this.select(this.ncells() - 1);
761 760 }
762 761 else {
763 762 this.select(this.undelete_index);
764 763 }
765 764 var cell_data = this.undelete_backup;
766 765 var new_cell = null;
767 766 if (this.undelete_below) {
768 767 new_cell = this.insert_cell_below(cell_data.cell_type);
769 768 } else {
770 769 new_cell = this.insert_cell_above(cell_data.cell_type);
771 770 }
772 771 new_cell.fromJSON(cell_data);
773 772 if (this.undelete_below) {
774 773 this.select(current_index+1);
775 774 } else {
776 775 this.select(current_index);
777 776 }
778 777 this.undelete_backup = null;
779 778 this.undelete_index = null;
780 779 }
781 780 $('#undelete_cell').addClass('disabled');
782 781 };
783 782
784 783 /**
785 784 * Insert a cell so that after insertion the cell is at given index.
786 785 *
787 786 * If cell type is not provided, it will default to the type of the
788 787 * currently active cell.
789 788 *
790 789 * Similar to insert_above, but index parameter is mandatory
791 790 *
792 791 * Index will be brought back into the accessible range [0,n]
793 792 *
794 793 * @method insert_cell_at_index
795 794 * @param [type] {string} in ['code','markdown','heading'], defaults to 'code'
796 795 * @param [index] {int} a valid index where to insert cell
797 796 *
798 797 * @return cell {cell|null} created cell or null
799 798 **/
800 799 Notebook.prototype.insert_cell_at_index = function(type, index){
801 800
802 801 var ncells = this.ncells();
803 802 index = Math.min(index,ncells);
804 803 index = Math.max(index,0);
805 804 var cell = null;
806 805 type = type || this.get_selected_cell().cell_type;
807 806
808 807 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
808 var cell_options = {
809 base_url: base_url,
810 notebook_path: notebook_path,
811 notebook_name: notebook_name,
812 events: this.events,
813 config: this.config,
814 keyboard_manager: this.keyboard_manager,
815 notebook: this
816 };
809 817 if (type === 'code') {
810 cell = new codecell.CodeCell(this.kernel, this.options, this.events, this.config, this.keyboard_manager, this);
818 cell = new codecell.CodeCell(this.kernel, cell_options);
811 819 cell.set_input_prompt();
812 820 } else if (type === 'markdown') {
813 cell = new cells.MarkdownCell(this.options, this.events, this.config, this.keyboard_manager, this);
821 cell = new cells.MarkdownCell(cell_options);
814 822 } else if (type === 'raw') {
815 cell = new cells.RawCell(this.options, this.events, this.config, this.keyboard_manager, this);
823 cell = new cells.RawCell(cell_options);
816 824 } else if (type === 'heading') {
817 cell = new cells.HeadingCell(this.options, this.events, this.config, this.keyboard_manager, this);
825 cell = new cells.HeadingCell(cell_options);
818 826 }
819 827
820 828 if(this._insert_element_at_index(cell.element,index)) {
821 829 cell.render();
822 830 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
823 831 cell.refresh();
824 832 // We used to select the cell after we refresh it, but there
825 833 // are now cases were this method is called where select is
826 834 // not appropriate. The selection logic should be handled by the
827 835 // caller of the the top level insert_cell methods.
828 836 this.set_dirty(true);
829 837 }
830 838 }
831 839 return cell;
832 840
833 841 };
834 842
835 843 /**
836 844 * Insert an element at given cell index.
837 845 *
838 846 * @method _insert_element_at_index
839 847 * @param element {dom element} a cell element
840 848 * @param [index] {int} a valid index where to inser cell
841 849 * @private
842 850 *
843 851 * return true if everything whent fine.
844 852 **/
845 853 Notebook.prototype._insert_element_at_index = function(element, index){
846 854 if (element === undefined){
847 855 return false;
848 856 }
849 857
850 858 var ncells = this.ncells();
851 859
852 860 if (ncells === 0) {
853 861 // special case append if empty
854 862 this.element.find('div.end_space').before(element);
855 863 } else if ( ncells === index ) {
856 864 // special case append it the end, but not empty
857 865 this.get_cell_element(index-1).after(element);
858 866 } else if (this.is_valid_cell_index(index)) {
859 867 // otherwise always somewhere to append to
860 868 this.get_cell_element(index).before(element);
861 869 } else {
862 870 return false;
863 871 }
864 872
865 873 if (this.undelete_index !== null && index <= this.undelete_index) {
866 874 this.undelete_index = this.undelete_index + 1;
867 875 this.set_dirty(true);
868 876 }
869 877 return true;
870 878 };
871 879
872 880 /**
873 881 * Insert a cell of given type above given index, or at top
874 882 * of notebook if index smaller than 0.
875 883 *
876 884 * default index value is the one of currently selected cell
877 885 *
878 886 * @method insert_cell_above
879 887 * @param [type] {string} cell type
880 888 * @param [index] {integer}
881 889 *
882 890 * @return handle to created cell or null
883 891 **/
884 892 Notebook.prototype.insert_cell_above = function (type, index) {
885 893 index = this.index_or_selected(index);
886 894 return this.insert_cell_at_index(type, index);
887 895 };
888 896
889 897 /**
890 898 * Insert a cell of given type below given index, or at bottom
891 899 * of notebook if index greater than number of cells
892 900 *
893 901 * default index value is the one of currently selected cell
894 902 *
895 903 * @method insert_cell_below
896 904 * @param [type] {string} cell type
897 905 * @param [index] {integer}
898 906 *
899 907 * @return handle to created cell or null
900 908 *
901 909 **/
902 910 Notebook.prototype.insert_cell_below = function (type, index) {
903 911 index = this.index_or_selected(index);
904 912 return this.insert_cell_at_index(type, index+1);
905 913 };
906 914
907 915
908 916 /**
909 917 * Insert cell at end of notebook
910 918 *
911 919 * @method insert_cell_at_bottom
912 920 * @param {String} type cell type
913 921 *
914 922 * @return the added cell; or null
915 923 **/
916 924 Notebook.prototype.insert_cell_at_bottom = function (type){
917 925 var len = this.ncells();
918 926 return this.insert_cell_below(type,len-1);
919 927 };
920 928
921 929 /**
922 930 * Turn a cell into a code cell.
923 931 *
924 932 * @method to_code
925 933 * @param {Number} [index] A cell's index
926 934 */
927 935 Notebook.prototype.to_code = function (index) {
928 936 var i = this.index_or_selected(index);
929 937 if (this.is_valid_cell_index(i)) {
930 938 var source_element = this.get_cell_element(i);
931 939 var source_cell = source_element.data("cell");
932 940 if (!(source_cell instanceof codecell.CodeCell)) {
933 941 var target_cell = this.insert_cell_below('code',i);
934 942 var text = source_cell.get_text();
935 943 if (text === source_cell.placeholder) {
936 944 text = '';
937 945 }
938 946 target_cell.set_text(text);
939 947 // make this value the starting point, so that we can only undo
940 948 // to this state, instead of a blank cell
941 949 target_cell.code_mirror.clearHistory();
942 950 source_element.remove();
943 951 this.select(i);
944 952 var cursor = source_cell.code_mirror.getCursor();
945 953 target_cell.code_mirror.setCursor(cursor);
946 954 this.set_dirty(true);
947 955 }
948 956 }
949 957 };
950 958
951 959 /**
952 960 * Turn a cell into a Markdown cell.
953 961 *
954 962 * @method to_markdown
955 963 * @param {Number} [index] A cell's index
956 964 */
957 965 Notebook.prototype.to_markdown = function (index) {
958 966 var i = this.index_or_selected(index);
959 967 if (this.is_valid_cell_index(i)) {
960 968 var source_element = this.get_cell_element(i);
961 969 var source_cell = source_element.data("cell");
962 970 if (!(source_cell instanceof cells.MarkdownCell)) {
963 971 var target_cell = this.insert_cell_below('markdown',i);
964 972 var text = source_cell.get_text();
965 973 if (text === source_cell.placeholder) {
966 974 text = '';
967 975 }
968 976 // We must show the editor before setting its contents
969 977 target_cell.unrender();
970 978 target_cell.set_text(text);
971 979 // make this value the starting point, so that we can only undo
972 980 // to this state, instead of a blank cell
973 981 target_cell.code_mirror.clearHistory();
974 982 source_element.remove();
975 983 this.select(i);
976 984 if ((source_cell instanceof cells.TextCell) && source_cell.rendered) {
977 985 target_cell.render();
978 986 }
979 987 var cursor = source_cell.code_mirror.getCursor();
980 988 target_cell.code_mirror.setCursor(cursor);
981 989 this.set_dirty(true);
982 990 }
983 991 }
984 992 };
985 993
986 994 /**
987 995 * Turn a cell into a raw text cell.
988 996 *
989 997 * @method to_raw
990 998 * @param {Number} [index] A cell's index
991 999 */
992 1000 Notebook.prototype.to_raw = function (index) {
993 1001 var i = this.index_or_selected(index);
994 1002 if (this.is_valid_cell_index(i)) {
995 1003 var source_element = this.get_cell_element(i);
996 1004 var source_cell = source_element.data("cell");
997 1005 var target_cell = null;
998 1006 if (!(source_cell instanceof cells.RawCell)) {
999 1007 target_cell = this.insert_cell_below('raw',i);
1000 1008 var text = source_cell.get_text();
1001 1009 if (text === source_cell.placeholder) {
1002 1010 text = '';
1003 1011 }
1004 1012 // We must show the editor before setting its contents
1005 1013 target_cell.unrender();
1006 1014 target_cell.set_text(text);
1007 1015 // make this value the starting point, so that we can only undo
1008 1016 // to this state, instead of a blank cell
1009 1017 target_cell.code_mirror.clearHistory();
1010 1018 source_element.remove();
1011 1019 this.select(i);
1012 1020 var cursor = source_cell.code_mirror.getCursor();
1013 1021 target_cell.code_mirror.setCursor(cursor);
1014 1022 this.set_dirty(true);
1015 1023 }
1016 1024 }
1017 1025 };
1018 1026
1019 1027 /**
1020 1028 * Turn a cell into a heading cell.
1021 1029 *
1022 1030 * @method to_heading
1023 1031 * @param {Number} [index] A cell's index
1024 1032 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
1025 1033 */
1026 1034 Notebook.prototype.to_heading = function (index, level) {
1027 1035 level = level || 1;
1028 1036 var i = this.index_or_selected(index);
1029 1037 if (this.is_valid_cell_index(i)) {
1030 1038 var source_element = this.get_cell_element(i);
1031 1039 var source_cell = source_element.data("cell");
1032 1040 var target_cell = null;
1033 1041 if (source_cell instanceof cells.HeadingCell) {
1034 1042 source_cell.set_level(level);
1035 1043 } else {
1036 1044 target_cell = this.insert_cell_below('heading',i);
1037 1045 var text = source_cell.get_text();
1038 1046 if (text === source_cell.placeholder) {
1039 1047 text = '';
1040 1048 }
1041 1049 // We must show the editor before setting its contents
1042 1050 target_cell.set_level(level);
1043 1051 target_cell.unrender();
1044 1052 target_cell.set_text(text);
1045 1053 // make this value the starting point, so that we can only undo
1046 1054 // to this state, instead of a blank cell
1047 1055 target_cell.code_mirror.clearHistory();
1048 1056 source_element.remove();
1049 1057 this.select(i);
1050 1058 var cursor = source_cell.code_mirror.getCursor();
1051 1059 target_cell.code_mirror.setCursor(cursor);
1052 1060 if ((source_cell instanceof cells.TextCell) && source_cell.rendered) {
1053 1061 target_cell.render();
1054 1062 }
1055 1063 }
1056 1064 this.set_dirty(true);
1057 1065 this.events.trigger('selected_cell_type_changed.Notebook',
1058 1066 {'cell_type':'heading',level:level}
1059 1067 );
1060 1068 }
1061 1069 };
1062 1070
1063 1071
1064 1072 // Cut/Copy/Paste
1065 1073
1066 1074 /**
1067 1075 * Enable UI elements for pasting cells.
1068 1076 *
1069 1077 * @method enable_paste
1070 1078 */
1071 1079 Notebook.prototype.enable_paste = function () {
1072 1080 var that = this;
1073 1081 if (!this.paste_enabled) {
1074 1082 $('#paste_cell_replace').removeClass('disabled')
1075 1083 .on('click', function () {that.paste_cell_replace();});
1076 1084 $('#paste_cell_above').removeClass('disabled')
1077 1085 .on('click', function () {that.paste_cell_above();});
1078 1086 $('#paste_cell_below').removeClass('disabled')
1079 1087 .on('click', function () {that.paste_cell_below();});
1080 1088 this.paste_enabled = true;
1081 1089 }
1082 1090 };
1083 1091
1084 1092 /**
1085 1093 * Disable UI elements for pasting cells.
1086 1094 *
1087 1095 * @method disable_paste
1088 1096 */
1089 1097 Notebook.prototype.disable_paste = function () {
1090 1098 if (this.paste_enabled) {
1091 1099 $('#paste_cell_replace').addClass('disabled').off('click');
1092 1100 $('#paste_cell_above').addClass('disabled').off('click');
1093 1101 $('#paste_cell_below').addClass('disabled').off('click');
1094 1102 this.paste_enabled = false;
1095 1103 }
1096 1104 };
1097 1105
1098 1106 /**
1099 1107 * Cut a cell.
1100 1108 *
1101 1109 * @method cut_cell
1102 1110 */
1103 1111 Notebook.prototype.cut_cell = function () {
1104 1112 this.copy_cell();
1105 1113 this.delete_cell();
1106 1114 };
1107 1115
1108 1116 /**
1109 1117 * Copy a cell.
1110 1118 *
1111 1119 * @method copy_cell
1112 1120 */
1113 1121 Notebook.prototype.copy_cell = function () {
1114 1122 var cell = this.get_selected_cell();
1115 1123 this.clipboard = cell.toJSON();
1116 1124 this.enable_paste();
1117 1125 };
1118 1126
1119 1127 /**
1120 1128 * Replace the selected cell with a cell in the clipboard.
1121 1129 *
1122 1130 * @method paste_cell_replace
1123 1131 */
1124 1132 Notebook.prototype.paste_cell_replace = function () {
1125 1133 if (this.clipboard !== null && this.paste_enabled) {
1126 1134 var cell_data = this.clipboard;
1127 1135 var new_cell = this.insert_cell_above(cell_data.cell_type);
1128 1136 new_cell.fromJSON(cell_data);
1129 1137 var old_cell = this.get_next_cell(new_cell);
1130 1138 this.delete_cell(this.find_cell_index(old_cell));
1131 1139 this.select(this.find_cell_index(new_cell));
1132 1140 }
1133 1141 };
1134 1142
1135 1143 /**
1136 1144 * Paste a cell from the clipboard above the selected cell.
1137 1145 *
1138 1146 * @method paste_cell_above
1139 1147 */
1140 1148 Notebook.prototype.paste_cell_above = function () {
1141 1149 if (this.clipboard !== null && this.paste_enabled) {
1142 1150 var cell_data = this.clipboard;
1143 1151 var new_cell = this.insert_cell_above(cell_data.cell_type);
1144 1152 new_cell.fromJSON(cell_data);
1145 1153 new_cell.focus_cell();
1146 1154 }
1147 1155 };
1148 1156
1149 1157 /**
1150 1158 * Paste a cell from the clipboard below the selected cell.
1151 1159 *
1152 1160 * @method paste_cell_below
1153 1161 */
1154 1162 Notebook.prototype.paste_cell_below = function () {
1155 1163 if (this.clipboard !== null && this.paste_enabled) {
1156 1164 var cell_data = this.clipboard;
1157 1165 var new_cell = this.insert_cell_below(cell_data.cell_type);
1158 1166 new_cell.fromJSON(cell_data);
1159 1167 new_cell.focus_cell();
1160 1168 }
1161 1169 };
1162 1170
1163 1171 // Split/merge
1164 1172
1165 1173 /**
1166 1174 * Split the selected cell into two, at the cursor.
1167 1175 *
1168 1176 * @method split_cell
1169 1177 */
1170 1178 Notebook.prototype.split_cell = function () {
1171 1179 var mdc = cells.MarkdownCell;
1172 1180 var rc = cells.RawCell;
1173 1181 var cell = this.get_selected_cell();
1174 1182 if (cell.is_splittable()) {
1175 1183 var texta = cell.get_pre_cursor();
1176 1184 var textb = cell.get_post_cursor();
1177 1185 if (cell instanceof codecell.CodeCell) {
1178 1186 // In this case the operations keep the notebook in its existing mode
1179 1187 // so we don't need to do any post-op mode changes.
1180 1188 cell.set_text(textb);
1181 1189 var new_cell = this.insert_cell_above('code');
1182 1190 new_cell.set_text(texta);
1183 1191 } else if ((cell instanceof mdc && !cell.rendered) || (cell instanceof rc)) {
1184 1192 // We know cell is !rendered so we can use set_text.
1185 1193 cell.set_text(textb);
1186 1194 var new_cell = this.insert_cell_above(cell.cell_type);
1187 1195 // Unrender the new cell so we can call set_text.
1188 1196 new_cell.unrender();
1189 1197 new_cell.set_text(texta);
1190 1198 }
1191 1199 }
1192 1200 };
1193 1201
1194 1202 /**
1195 1203 * Combine the selected cell into the cell above it.
1196 1204 *
1197 1205 * @method merge_cell_above
1198 1206 */
1199 1207 Notebook.prototype.merge_cell_above = function () {
1200 1208 var mdc = cells.MarkdownCell;
1201 1209 var rc = cells.RawCell;
1202 1210 var index = this.get_selected_index();
1203 1211 var cell = this.get_cell(index);
1204 1212 var render = cell.rendered;
1205 1213 if (!cell.is_mergeable()) {
1206 1214 return;
1207 1215 }
1208 1216 if (index > 0) {
1209 1217 var upper_cell = this.get_cell(index-1);
1210 1218 if (!upper_cell.is_mergeable()) {
1211 1219 return;
1212 1220 }
1213 1221 var upper_text = upper_cell.get_text();
1214 1222 var text = cell.get_text();
1215 1223 if (cell instanceof codecell.CodeCell) {
1216 1224 cell.set_text(upper_text+'\n'+text);
1217 1225 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1218 1226 cell.unrender(); // Must unrender before we set_text.
1219 1227 cell.set_text(upper_text+'\n\n'+text);
1220 1228 if (render) {
1221 1229 // The rendered state of the final cell should match
1222 1230 // that of the original selected cell;
1223 1231 cell.render();
1224 1232 }
1225 1233 }
1226 1234 this.delete_cell(index-1);
1227 1235 this.select(this.find_cell_index(cell));
1228 1236 }
1229 1237 };
1230 1238
1231 1239 /**
1232 1240 * Combine the selected cell into the cell below it.
1233 1241 *
1234 1242 * @method merge_cell_below
1235 1243 */
1236 1244 Notebook.prototype.merge_cell_below = function () {
1237 1245 var mdc = cells.MarkdownCell;
1238 1246 var rc = cells.RawCell;
1239 1247 var index = this.get_selected_index();
1240 1248 var cell = this.get_cell(index);
1241 1249 var render = cell.rendered;
1242 1250 if (!cell.is_mergeable()) {
1243 1251 return;
1244 1252 }
1245 1253 if (index < this.ncells()-1) {
1246 1254 var lower_cell = this.get_cell(index+1);
1247 1255 if (!lower_cell.is_mergeable()) {
1248 1256 return;
1249 1257 }
1250 1258 var lower_text = lower_cell.get_text();
1251 1259 var text = cell.get_text();
1252 1260 if (cell instanceof codecell.CodeCell) {
1253 1261 cell.set_text(text+'\n'+lower_text);
1254 1262 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1255 1263 cell.unrender(); // Must unrender before we set_text.
1256 1264 cell.set_text(text+'\n\n'+lower_text);
1257 1265 if (render) {
1258 1266 // The rendered state of the final cell should match
1259 1267 // that of the original selected cell;
1260 1268 cell.render();
1261 1269 }
1262 1270 }
1263 1271 this.delete_cell(index+1);
1264 1272 this.select(this.find_cell_index(cell));
1265 1273 }
1266 1274 };
1267 1275
1268 1276
1269 1277 // Cell collapsing and output clearing
1270 1278
1271 1279 /**
1272 1280 * Hide a cell's output.
1273 1281 *
1274 1282 * @method collapse_output
1275 1283 * @param {Number} index A cell's numeric index
1276 1284 */
1277 1285 Notebook.prototype.collapse_output = function (index) {
1278 1286 var i = this.index_or_selected(index);
1279 1287 var cell = this.get_cell(i);
1280 1288 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1281 1289 cell.collapse_output();
1282 1290 this.set_dirty(true);
1283 1291 }
1284 1292 };
1285 1293
1286 1294 /**
1287 1295 * Hide each code cell's output area.
1288 1296 *
1289 1297 * @method collapse_all_output
1290 1298 */
1291 1299 Notebook.prototype.collapse_all_output = function () {
1292 1300 $.map(this.get_cells(), function (cell, i) {
1293 1301 if (cell instanceof codecell.CodeCell) {
1294 1302 cell.collapse_output();
1295 1303 }
1296 1304 });
1297 1305 // this should not be set if the `collapse` key is removed from nbformat
1298 1306 this.set_dirty(true);
1299 1307 };
1300 1308
1301 1309 /**
1302 1310 * Show a cell's output.
1303 1311 *
1304 1312 * @method expand_output
1305 1313 * @param {Number} index A cell's numeric index
1306 1314 */
1307 1315 Notebook.prototype.expand_output = function (index) {
1308 1316 var i = this.index_or_selected(index);
1309 1317 var cell = this.get_cell(i);
1310 1318 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1311 1319 cell.expand_output();
1312 1320 this.set_dirty(true);
1313 1321 }
1314 1322 };
1315 1323
1316 1324 /**
1317 1325 * Expand each code cell's output area, and remove scrollbars.
1318 1326 *
1319 1327 * @method expand_all_output
1320 1328 */
1321 1329 Notebook.prototype.expand_all_output = function () {
1322 1330 $.map(this.get_cells(), function (cell, i) {
1323 1331 if (cell instanceof codecell.CodeCell) {
1324 1332 cell.expand_output();
1325 1333 }
1326 1334 });
1327 1335 // this should not be set if the `collapse` key is removed from nbformat
1328 1336 this.set_dirty(true);
1329 1337 };
1330 1338
1331 1339 /**
1332 1340 * Clear the selected CodeCell's output area.
1333 1341 *
1334 1342 * @method clear_output
1335 1343 * @param {Number} index A cell's numeric index
1336 1344 */
1337 1345 Notebook.prototype.clear_output = function (index) {
1338 1346 var i = this.index_or_selected(index);
1339 1347 var cell = this.get_cell(i);
1340 1348 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1341 1349 cell.clear_output();
1342 1350 this.set_dirty(true);
1343 1351 }
1344 1352 };
1345 1353
1346 1354 /**
1347 1355 * Clear each code cell's output area.
1348 1356 *
1349 1357 * @method clear_all_output
1350 1358 */
1351 1359 Notebook.prototype.clear_all_output = function () {
1352 1360 $.map(this.get_cells(), function (cell, i) {
1353 1361 if (cell instanceof codecell.CodeCell) {
1354 1362 cell.clear_output();
1355 1363 }
1356 1364 });
1357 1365 this.set_dirty(true);
1358 1366 };
1359 1367
1360 1368 /**
1361 1369 * Scroll the selected CodeCell's output area.
1362 1370 *
1363 1371 * @method scroll_output
1364 1372 * @param {Number} index A cell's numeric index
1365 1373 */
1366 1374 Notebook.prototype.scroll_output = function (index) {
1367 1375 var i = this.index_or_selected(index);
1368 1376 var cell = this.get_cell(i);
1369 1377 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1370 1378 cell.scroll_output();
1371 1379 this.set_dirty(true);
1372 1380 }
1373 1381 };
1374 1382
1375 1383 /**
1376 1384 * Expand each code cell's output area, and add a scrollbar for long output.
1377 1385 *
1378 1386 * @method scroll_all_output
1379 1387 */
1380 1388 Notebook.prototype.scroll_all_output = function () {
1381 1389 $.map(this.get_cells(), function (cell, i) {
1382 1390 if (cell instanceof codecell.CodeCell) {
1383 1391 cell.scroll_output();
1384 1392 }
1385 1393 });
1386 1394 // this should not be set if the `collapse` key is removed from nbformat
1387 1395 this.set_dirty(true);
1388 1396 };
1389 1397
1390 1398 /** Toggle whether a cell's output is collapsed or expanded.
1391 1399 *
1392 1400 * @method toggle_output
1393 1401 * @param {Number} index A cell's numeric index
1394 1402 */
1395 1403 Notebook.prototype.toggle_output = function (index) {
1396 1404 var i = this.index_or_selected(index);
1397 1405 var cell = this.get_cell(i);
1398 1406 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1399 1407 cell.toggle_output();
1400 1408 this.set_dirty(true);
1401 1409 }
1402 1410 };
1403 1411
1404 1412 /**
1405 1413 * Hide/show the output of all cells.
1406 1414 *
1407 1415 * @method toggle_all_output
1408 1416 */
1409 1417 Notebook.prototype.toggle_all_output = function () {
1410 1418 $.map(this.get_cells(), function (cell, i) {
1411 1419 if (cell instanceof codecell.CodeCell) {
1412 1420 cell.toggle_output();
1413 1421 }
1414 1422 });
1415 1423 // this should not be set if the `collapse` key is removed from nbformat
1416 1424 this.set_dirty(true);
1417 1425 };
1418 1426
1419 1427 /**
1420 1428 * Toggle a scrollbar for long cell outputs.
1421 1429 *
1422 1430 * @method toggle_output_scroll
1423 1431 * @param {Number} index A cell's numeric index
1424 1432 */
1425 1433 Notebook.prototype.toggle_output_scroll = function (index) {
1426 1434 var i = this.index_or_selected(index);
1427 1435 var cell = this.get_cell(i);
1428 1436 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1429 1437 cell.toggle_output_scroll();
1430 1438 this.set_dirty(true);
1431 1439 }
1432 1440 };
1433 1441
1434 1442 /**
1435 1443 * Toggle the scrolling of long output on all cells.
1436 1444 *
1437 1445 * @method toggle_all_output_scrolling
1438 1446 */
1439 1447 Notebook.prototype.toggle_all_output_scroll = function () {
1440 1448 $.map(this.get_cells(), function (cell, i) {
1441 1449 if (cell instanceof codecell.CodeCell) {
1442 1450 cell.toggle_output_scroll();
1443 1451 }
1444 1452 });
1445 1453 // this should not be set if the `collapse` key is removed from nbformat
1446 1454 this.set_dirty(true);
1447 1455 };
1448 1456
1449 1457 // Other cell functions: line numbers, ...
1450 1458
1451 1459 /**
1452 1460 * Toggle line numbers in the selected cell's input area.
1453 1461 *
1454 1462 * @method cell_toggle_line_numbers
1455 1463 */
1456 1464 Notebook.prototype.cell_toggle_line_numbers = function() {
1457 1465 this.get_selected_cell().toggle_line_numbers();
1458 1466 };
1459 1467
1460 1468 // Session related things
1461 1469
1462 1470 /**
1463 1471 * Start a new session and set it on each code cell.
1464 1472 *
1465 1473 * @method start_session
1466 1474 */
1467 1475 Notebook.prototype.start_session = function () {
1468 this.session = new session.Session(this, this.options);
1476 this.session = new session.Session(this, {
1477 base_url: base_url,
1478 notebook_path: notebook_path,
1479 notebook_name: notebook_name,
1480 notebook: this});
1469 1481 this.session.start($.proxy(this._session_started, this));
1470 1482 };
1471 1483
1472 1484
1473 1485 /**
1474 1486 * Once a session is started, link the code cells to the kernel and pass the
1475 1487 * comm manager to the widget manager
1476 1488 *
1477 1489 */
1478 1490 Notebook.prototype._session_started = function(){
1479 1491 this.kernel = this.session.kernel;
1480 1492 var ncells = this.ncells();
1481 1493 for (var i=0; i<ncells; i++) {
1482 1494 var cell = this.get_cell(i);
1483 1495 if (cell instanceof codecell.CodeCell) {
1484 1496 cell.set_kernel(this.session.kernel);
1485 1497 }
1486 1498 }
1487 1499 };
1488 1500
1489 1501 /**
1490 1502 * Prompt the user to restart the IPython kernel.
1491 1503 *
1492 1504 * @method restart_kernel
1493 1505 */
1494 1506 Notebook.prototype.restart_kernel = function () {
1495 1507 var that = this;
1496 1508 dialog.modal({
1497 1509 title : "Restart kernel or continue running?",
1498 1510 body : $("<p/>").text(
1499 1511 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1500 1512 ),
1501 1513 buttons : {
1502 1514 "Continue running" : {},
1503 1515 "Restart" : {
1504 1516 "class" : "btn-danger",
1505 1517 "click" : function() {
1506 1518 that.session.restart_kernel();
1507 1519 }
1508 1520 }
1509 1521 }
1510 1522 });
1511 1523 };
1512 1524
1513 1525 /**
1514 1526 * Execute or render cell outputs and go into command mode.
1515 1527 *
1516 1528 * @method execute_cell
1517 1529 */
1518 1530 Notebook.prototype.execute_cell = function () {
1519 1531 // mode = shift, ctrl, alt
1520 1532 var cell = this.get_selected_cell();
1521 1533 var cell_index = this.find_cell_index(cell);
1522 1534
1523 1535 cell.execute();
1524 1536 this.command_mode();
1525 1537 this.set_dirty(true);
1526 1538 };
1527 1539
1528 1540 /**
1529 1541 * Execute or render cell outputs and insert a new cell below.
1530 1542 *
1531 1543 * @method execute_cell_and_insert_below
1532 1544 */
1533 1545 Notebook.prototype.execute_cell_and_insert_below = function () {
1534 1546 var cell = this.get_selected_cell();
1535 1547 var cell_index = this.find_cell_index(cell);
1536 1548
1537 1549 cell.execute();
1538 1550
1539 1551 // If we are at the end always insert a new cell and return
1540 1552 if (cell_index === (this.ncells()-1)) {
1541 1553 this.command_mode();
1542 1554 this.insert_cell_below();
1543 1555 this.select(cell_index+1);
1544 1556 this.edit_mode();
1545 1557 this.scroll_to_bottom();
1546 1558 this.set_dirty(true);
1547 1559 return;
1548 1560 }
1549 1561
1550 1562 this.command_mode();
1551 1563 this.insert_cell_below();
1552 1564 this.select(cell_index+1);
1553 1565 this.edit_mode();
1554 1566 this.set_dirty(true);
1555 1567 };
1556 1568
1557 1569 /**
1558 1570 * Execute or render cell outputs and select the next cell.
1559 1571 *
1560 1572 * @method execute_cell_and_select_below
1561 1573 */
1562 1574 Notebook.prototype.execute_cell_and_select_below = function () {
1563 1575
1564 1576 var cell = this.get_selected_cell();
1565 1577 var cell_index = this.find_cell_index(cell);
1566 1578
1567 1579 cell.execute();
1568 1580
1569 1581 // If we are at the end always insert a new cell and return
1570 1582 if (cell_index === (this.ncells()-1)) {
1571 1583 this.command_mode();
1572 1584 this.insert_cell_below();
1573 1585 this.select(cell_index+1);
1574 1586 this.edit_mode();
1575 1587 this.scroll_to_bottom();
1576 1588 this.set_dirty(true);
1577 1589 return;
1578 1590 }
1579 1591
1580 1592 this.command_mode();
1581 1593 this.select(cell_index+1);
1582 1594 this.focus_cell();
1583 1595 this.set_dirty(true);
1584 1596 };
1585 1597
1586 1598 /**
1587 1599 * Execute all cells below the selected cell.
1588 1600 *
1589 1601 * @method execute_cells_below
1590 1602 */
1591 1603 Notebook.prototype.execute_cells_below = function () {
1592 1604 this.execute_cell_range(this.get_selected_index(), this.ncells());
1593 1605 this.scroll_to_bottom();
1594 1606 };
1595 1607
1596 1608 /**
1597 1609 * Execute all cells above the selected cell.
1598 1610 *
1599 1611 * @method execute_cells_above
1600 1612 */
1601 1613 Notebook.prototype.execute_cells_above = function () {
1602 1614 this.execute_cell_range(0, this.get_selected_index());
1603 1615 };
1604 1616
1605 1617 /**
1606 1618 * Execute all cells.
1607 1619 *
1608 1620 * @method execute_all_cells
1609 1621 */
1610 1622 Notebook.prototype.execute_all_cells = function () {
1611 1623 this.execute_cell_range(0, this.ncells());
1612 1624 this.scroll_to_bottom();
1613 1625 };
1614 1626
1615 1627 /**
1616 1628 * Execute a contiguous range of cells.
1617 1629 *
1618 1630 * @method execute_cell_range
1619 1631 * @param {Number} start Index of the first cell to execute (inclusive)
1620 1632 * @param {Number} end Index of the last cell to execute (exclusive)
1621 1633 */
1622 1634 Notebook.prototype.execute_cell_range = function (start, end) {
1623 1635 this.command_mode();
1624 1636 for (var i=start; i<end; i++) {
1625 1637 this.select(i);
1626 1638 this.execute_cell();
1627 1639 }
1628 1640 };
1629 1641
1630 1642 // Persistance and loading
1631 1643
1632 1644 /**
1633 1645 * Getter method for this notebook's name.
1634 1646 *
1635 1647 * @method get_notebook_name
1636 1648 * @return {String} This notebook's name (excluding file extension)
1637 1649 */
1638 1650 Notebook.prototype.get_notebook_name = function () {
1639 1651 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1640 1652 return nbname;
1641 1653 };
1642 1654
1643 1655 /**
1644 1656 * Setter method for this notebook's name.
1645 1657 *
1646 1658 * @method set_notebook_name
1647 1659 * @param {String} name A new name for this notebook
1648 1660 */
1649 1661 Notebook.prototype.set_notebook_name = function (name) {
1650 1662 this.notebook_name = name;
1651 1663 };
1652 1664
1653 1665 /**
1654 1666 * Check that a notebook's name is valid.
1655 1667 *
1656 1668 * @method test_notebook_name
1657 1669 * @param {String} nbname A name for this notebook
1658 1670 * @return {Boolean} True if the name is valid, false if invalid
1659 1671 */
1660 1672 Notebook.prototype.test_notebook_name = function (nbname) {
1661 1673 nbname = nbname || '';
1662 1674 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1663 1675 return true;
1664 1676 } else {
1665 1677 return false;
1666 1678 }
1667 1679 };
1668 1680
1669 1681 /**
1670 1682 * Load a notebook from JSON (.ipynb).
1671 1683 *
1672 1684 * This currently handles one worksheet: others are deleted.
1673 1685 *
1674 1686 * @method fromJSON
1675 1687 * @param {Object} data JSON representation of a notebook
1676 1688 */
1677 1689 Notebook.prototype.fromJSON = function (data) {
1678 1690 var content = data.content;
1679 1691 var ncells = this.ncells();
1680 1692 var i;
1681 1693 for (i=0; i<ncells; i++) {
1682 1694 // Always delete cell 0 as they get renumbered as they are deleted.
1683 1695 this.delete_cell(0);
1684 1696 }
1685 1697 // Save the metadata and name.
1686 1698 this.metadata = content.metadata;
1687 1699 this.notebook_name = data.name;
1688 1700 var trusted = true;
1689 1701 // Only handle 1 worksheet for now.
1690 1702 var worksheet = content.worksheets[0];
1691 1703 if (worksheet !== undefined) {
1692 1704 if (worksheet.metadata) {
1693 1705 this.worksheet_metadata = worksheet.metadata;
1694 1706 }
1695 1707 var new_cells = worksheet.cells;
1696 1708 ncells = new_cells.length;
1697 1709 var cell_data = null;
1698 1710 var new_cell = null;
1699 1711 for (i=0; i<ncells; i++) {
1700 1712 cell_data = new_cells[i];
1701 1713 // VERSIONHACK: plaintext -> raw
1702 1714 // handle never-released plaintext name for raw cells
1703 1715 if (cell_data.cell_type === 'plaintext'){
1704 1716 cell_data.cell_type = 'raw';
1705 1717 }
1706 1718
1707 1719 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1708 1720 new_cell.fromJSON(cell_data);
1709 1721 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1710 1722 trusted = false;
1711 1723 }
1712 1724 }
1713 1725 }
1714 1726 if (trusted != this.trusted) {
1715 1727 this.trusted = trusted;
1716 1728 this.events.trigger("trust_changed.Notebook", trusted);
1717 1729 }
1718 1730 if (content.worksheets.length > 1) {
1719 1731 dialog.modal({
1720 1732 title : "Multiple worksheets",
1721 1733 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1722 1734 "but this version of IPython can only handle the first. " +
1723 1735 "If you save this notebook, worksheets after the first will be lost.",
1724 1736 buttons : {
1725 1737 OK : {
1726 1738 class : "btn-danger"
1727 1739 }
1728 1740 }
1729 1741 });
1730 1742 }
1731 1743 };
1732 1744
1733 1745 /**
1734 1746 * Dump this notebook into a JSON-friendly object.
1735 1747 *
1736 1748 * @method toJSON
1737 1749 * @return {Object} A JSON-friendly representation of this notebook.
1738 1750 */
1739 1751 Notebook.prototype.toJSON = function () {
1740 1752 var cells = this.get_cells();
1741 1753 var ncells = cells.length;
1742 1754 var cell_array = new Array(ncells);
1743 1755 var trusted = true;
1744 1756 for (var i=0; i<ncells; i++) {
1745 1757 var cell = cells[i];
1746 1758 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1747 1759 trusted = false;
1748 1760 }
1749 1761 cell_array[i] = cell.toJSON();
1750 1762 }
1751 1763 var data = {
1752 1764 // Only handle 1 worksheet for now.
1753 1765 worksheets : [{
1754 1766 cells: cell_array,
1755 1767 metadata: this.worksheet_metadata
1756 1768 }],
1757 1769 metadata : this.metadata
1758 1770 };
1759 1771 if (trusted != this.trusted) {
1760 1772 this.trusted = trusted;
1761 1773 this.events.trigger("trust_changed.Notebook", trusted);
1762 1774 }
1763 1775 return data;
1764 1776 };
1765 1777
1766 1778 /**
1767 1779 * Start an autosave timer, for periodically saving the notebook.
1768 1780 *
1769 1781 * @method set_autosave_interval
1770 1782 * @param {Integer} interval the autosave interval in milliseconds
1771 1783 */
1772 1784 Notebook.prototype.set_autosave_interval = function (interval) {
1773 1785 var that = this;
1774 1786 // clear previous interval, so we don't get simultaneous timers
1775 1787 if (this.autosave_timer) {
1776 1788 clearInterval(this.autosave_timer);
1777 1789 }
1778 1790
1779 1791 this.autosave_interval = this.minimum_autosave_interval = interval;
1780 1792 if (interval) {
1781 1793 this.autosave_timer = setInterval(function() {
1782 1794 if (that.dirty) {
1783 1795 that.save_notebook();
1784 1796 }
1785 1797 }, interval);
1786 1798 this.events.trigger("autosave_enabled.Notebook", interval);
1787 1799 } else {
1788 1800 this.autosave_timer = null;
1789 1801 this.events.trigger("autosave_disabled.Notebook");
1790 1802 }
1791 1803 };
1792 1804
1793 1805 /**
1794 1806 * Save this notebook on the server. This becomes a notebook instance's
1795 1807 * .save_notebook method *after* the entire notebook has been loaded.
1796 1808 *
1797 1809 * @method save_notebook
1798 1810 */
1799 1811 Notebook.prototype.save_notebook = function (extra_settings) {
1800 1812 // Create a JSON model to be sent to the server.
1801 1813 var model = {};
1802 1814 model.name = this.notebook_name;
1803 1815 model.path = this.notebook_path;
1804 1816 model.content = this.toJSON();
1805 1817 model.content.nbformat = this.nbformat;
1806 1818 model.content.nbformat_minor = this.nbformat_minor;
1807 1819 // time the ajax call for autosave tuning purposes.
1808 1820 var start = new Date().getTime();
1809 1821 // We do the call with settings so we can set cache to false.
1810 1822 var settings = {
1811 1823 processData : false,
1812 1824 cache : false,
1813 1825 type : "PUT",
1814 1826 data : JSON.stringify(model),
1815 1827 headers : {'Content-Type': 'application/json'},
1816 1828 success : $.proxy(this.save_notebook_success, this, start),
1817 1829 error : $.proxy(this.save_notebook_error, this)
1818 1830 };
1819 1831 if (extra_settings) {
1820 1832 for (var key in extra_settings) {
1821 1833 settings[key] = extra_settings[key];
1822 1834 }
1823 1835 }
1824 1836 this.events.trigger('notebook_saving.Notebook');
1825 1837 var url = utils.url_join_encode(
1826 1838 this.base_url,
1827 1839 'api/notebooks',
1828 1840 this.notebook_path,
1829 1841 this.notebook_name
1830 1842 );
1831 1843 $.ajax(url, settings);
1832 1844 };
1833 1845
1834 1846 /**
1835 1847 * Success callback for saving a notebook.
1836 1848 *
1837 1849 * @method save_notebook_success
1838 1850 * @param {Integer} start the time when the save request started
1839 1851 * @param {Object} data JSON representation of a notebook
1840 1852 * @param {String} status Description of response status
1841 1853 * @param {jqXHR} xhr jQuery Ajax object
1842 1854 */
1843 1855 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1844 1856 this.set_dirty(false);
1845 1857 this.events.trigger('notebook_saved.Notebook');
1846 1858 this._update_autosave_interval(start);
1847 1859 if (this._checkpoint_after_save) {
1848 1860 this.create_checkpoint();
1849 1861 this._checkpoint_after_save = false;
1850 1862 }
1851 1863 };
1852 1864
1853 1865 /**
1854 1866 * update the autosave interval based on how long the last save took
1855 1867 *
1856 1868 * @method _update_autosave_interval
1857 1869 * @param {Integer} timestamp when the save request started
1858 1870 */
1859 1871 Notebook.prototype._update_autosave_interval = function (start) {
1860 1872 var duration = (new Date().getTime() - start);
1861 1873 if (this.autosave_interval) {
1862 1874 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1863 1875 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1864 1876 // round to 10 seconds, otherwise we will be setting a new interval too often
1865 1877 interval = 10000 * Math.round(interval / 10000);
1866 1878 // set new interval, if it's changed
1867 1879 if (interval != this.autosave_interval) {
1868 1880 this.set_autosave_interval(interval);
1869 1881 }
1870 1882 }
1871 1883 };
1872 1884
1873 1885 /**
1874 1886 * Failure callback for saving a notebook.
1875 1887 *
1876 1888 * @method save_notebook_error
1877 1889 * @param {jqXHR} xhr jQuery Ajax object
1878 1890 * @param {String} status Description of response status
1879 1891 * @param {String} error HTTP error message
1880 1892 */
1881 1893 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1882 1894 this.events.trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1883 1895 };
1884 1896
1885 1897 /**
1886 1898 * Explicitly trust the output of this notebook.
1887 1899 *
1888 1900 * @method trust_notebook
1889 1901 */
1890 1902 Notebook.prototype.trust_notebook = function (extra_settings) {
1891 1903 var body = $("<div>").append($("<p>")
1892 1904 .text("A trusted IPython notebook may execute hidden malicious code ")
1893 1905 .append($("<strong>")
1894 1906 .append(
1895 1907 $("<em>").text("when you open it")
1896 1908 )
1897 1909 ).append(".").append(
1898 1910 " Selecting trust will immediately reload this notebook in a trusted state."
1899 1911 ).append(
1900 1912 " For more information, see the "
1901 1913 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
1902 1914 .text("IPython security documentation")
1903 1915 ).append(".")
1904 1916 );
1905 1917
1906 1918 var nb = this;
1907 1919 dialog.modal({
1908 1920 title: "Trust this notebook?",
1909 1921 body: body,
1910 1922
1911 1923 buttons: {
1912 1924 Cancel : {},
1913 1925 Trust : {
1914 1926 class : "btn-danger",
1915 1927 click : function () {
1916 1928 var cells = nb.get_cells();
1917 1929 for (var i = 0; i < cells.length; i++) {
1918 1930 var cell = cells[i];
1919 1931 if (cell.cell_type == 'code') {
1920 1932 cell.output_area.trusted = true;
1921 1933 }
1922 1934 }
1923 1935 this.events.on('notebook_saved.Notebook', function () {
1924 1936 window.location.reload();
1925 1937 });
1926 1938 nb.save_notebook();
1927 1939 }
1928 1940 }
1929 1941 }
1930 1942 });
1931 1943 };
1932 1944
1933 1945 Notebook.prototype.new_notebook = function(){
1934 1946 var path = this.notebook_path;
1935 1947 var base_url = this.base_url;
1936 1948 var settings = {
1937 1949 processData : false,
1938 1950 cache : false,
1939 1951 type : "POST",
1940 1952 dataType : "json",
1941 1953 async : false,
1942 1954 success : function (data, status, xhr){
1943 1955 var notebook_name = data.name;
1944 1956 window.open(
1945 1957 utils.url_join_encode(
1946 1958 base_url,
1947 1959 'notebooks',
1948 1960 path,
1949 1961 notebook_name
1950 1962 ),
1951 1963 '_blank'
1952 1964 );
1953 1965 },
1954 1966 error : utils.log_ajax_error,
1955 1967 };
1956 1968 var url = utils.url_join_encode(
1957 1969 base_url,
1958 1970 'api/notebooks',
1959 1971 path
1960 1972 );
1961 1973 $.ajax(url,settings);
1962 1974 };
1963 1975
1964 1976
1965 1977 Notebook.prototype.copy_notebook = function(){
1966 1978 var path = this.notebook_path;
1967 1979 var base_url = this.base_url;
1968 1980 var settings = {
1969 1981 processData : false,
1970 1982 cache : false,
1971 1983 type : "POST",
1972 1984 dataType : "json",
1973 1985 data : JSON.stringify({copy_from : this.notebook_name}),
1974 1986 async : false,
1975 1987 success : function (data, status, xhr) {
1976 1988 window.open(utils.url_join_encode(
1977 1989 base_url,
1978 1990 'notebooks',
1979 1991 data.path,
1980 1992 data.name
1981 1993 ), '_blank');
1982 1994 },
1983 1995 error : utils.log_ajax_error,
1984 1996 };
1985 1997 var url = utils.url_join_encode(
1986 1998 base_url,
1987 1999 'api/notebooks',
1988 2000 path
1989 2001 );
1990 2002 $.ajax(url,settings);
1991 2003 };
1992 2004
1993 2005 Notebook.prototype.rename = function (nbname) {
1994 2006 var that = this;
1995 2007 if (!nbname.match(/\.ipynb$/)) {
1996 2008 nbname = nbname + ".ipynb";
1997 2009 }
1998 2010 var data = {name: nbname};
1999 2011 var settings = {
2000 2012 processData : false,
2001 2013 cache : false,
2002 2014 type : "PATCH",
2003 2015 data : JSON.stringify(data),
2004 2016 dataType: "json",
2005 2017 headers : {'Content-Type': 'application/json'},
2006 2018 success : $.proxy(that.rename_success, this),
2007 2019 error : $.proxy(that.rename_error, this)
2008 2020 };
2009 2021 this.events.trigger('rename_notebook.Notebook', data);
2010 2022 var url = utils.url_join_encode(
2011 2023 this.base_url,
2012 2024 'api/notebooks',
2013 2025 this.notebook_path,
2014 2026 this.notebook_name
2015 2027 );
2016 2028 $.ajax(url, settings);
2017 2029 };
2018 2030
2019 2031 Notebook.prototype.delete = function () {
2020 2032 var that = this;
2021 2033 var settings = {
2022 2034 processData : false,
2023 2035 cache : false,
2024 2036 type : "DELETE",
2025 2037 dataType: "json",
2026 2038 error : utils.log_ajax_error,
2027 2039 };
2028 2040 var url = utils.url_join_encode(
2029 2041 this.base_url,
2030 2042 'api/notebooks',
2031 2043 this.notebook_path,
2032 2044 this.notebook_name
2033 2045 );
2034 2046 $.ajax(url, settings);
2035 2047 };
2036 2048
2037 2049
2038 2050 Notebook.prototype.rename_success = function (json, status, xhr) {
2039 2051 var name = this.notebook_name = json.name;
2040 2052 var path = json.path;
2041 2053 this.session.rename_notebook(name, path);
2042 2054 this.events.trigger('notebook_renamed.Notebook', json);
2043 2055 };
2044 2056
2045 2057 Notebook.prototype.rename_error = function (xhr, status, error) {
2046 2058 var that = this;
2047 2059 var dialog_body = $('<div/>').append(
2048 2060 $("<p/>").addClass("rename-message")
2049 2061 .text('This notebook name already exists.')
2050 2062 );
2051 2063 this.events.trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
2052 2064 dialog.modal({
2053 2065 title: "Notebook Rename Error!",
2054 2066 body: dialog_body,
2055 2067 buttons : {
2056 2068 "Cancel": {},
2057 2069 "OK": {
2058 2070 class: "btn-primary",
2059 2071 click: function () {
2060 2072 this.save_widget.rename_notebook();
2061 2073 }}
2062 2074 },
2063 2075 open : function (event, ui) {
2064 2076 var that = $(this);
2065 2077 // Upon ENTER, click the OK button.
2066 2078 that.find('input[type="text"]').keydown(function (event, ui) {
2067 2079 if (event.which === this.keyboard.keycodes.enter) {
2068 2080 that.find('.btn-primary').first().click();
2069 2081 }
2070 2082 });
2071 2083 that.find('input[type="text"]').focus();
2072 2084 }
2073 2085 });
2074 2086 };
2075 2087
2076 2088 /**
2077 2089 * Request a notebook's data from the server.
2078 2090 *
2079 2091 * @method load_notebook
2080 2092 * @param {String} notebook_name and path A notebook to load
2081 2093 */
2082 2094 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2083 2095 var that = this;
2084 2096 this.notebook_name = notebook_name;
2085 2097 this.notebook_path = notebook_path;
2086 2098 // We do the call with settings so we can set cache to false.
2087 2099 var settings = {
2088 2100 processData : false,
2089 2101 cache : false,
2090 2102 type : "GET",
2091 2103 dataType : "json",
2092 2104 success : $.proxy(this.load_notebook_success,this),
2093 2105 error : $.proxy(this.load_notebook_error,this),
2094 2106 };
2095 2107 this.events.trigger('notebook_loading.Notebook');
2096 2108 var url = utils.url_join_encode(
2097 2109 this.base_url,
2098 2110 'api/notebooks',
2099 2111 this.notebook_path,
2100 2112 this.notebook_name
2101 2113 );
2102 2114 $.ajax(url, settings);
2103 2115 };
2104 2116
2105 2117 /**
2106 2118 * Success callback for loading a notebook from the server.
2107 2119 *
2108 2120 * Load notebook data from the JSON response.
2109 2121 *
2110 2122 * @method load_notebook_success
2111 2123 * @param {Object} data JSON representation of a notebook
2112 2124 * @param {String} status Description of response status
2113 2125 * @param {jqXHR} xhr jQuery Ajax object
2114 2126 */
2115 2127 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
2116 2128 this.fromJSON(data);
2117 2129 if (this.ncells() === 0) {
2118 2130 this.insert_cell_below('code');
2119 2131 this.edit_mode(0);
2120 2132 } else {
2121 2133 this.select(0);
2122 2134 this.handle_command_mode(this.get_cell(0));
2123 2135 }
2124 2136 this.set_dirty(false);
2125 2137 this.scroll_to_top();
2126 2138 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
2127 2139 var msg = "This notebook has been converted from an older " +
2128 2140 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
2129 2141 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
2130 2142 "newer notebook format will be used and older versions of IPython " +
2131 2143 "may not be able to read it. To keep the older version, close the " +
2132 2144 "notebook without saving it.";
2133 2145 dialog.modal({
2134 2146 title : "Notebook converted",
2135 2147 body : msg,
2136 2148 buttons : {
2137 2149 OK : {
2138 2150 class : "btn-primary"
2139 2151 }
2140 2152 }
2141 2153 });
2142 2154 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
2143 2155 var that = this;
2144 2156 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
2145 2157 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
2146 2158 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2147 2159 this_vs + ". You can still work with this notebook, but some features " +
2148 2160 "introduced in later notebook versions may not be available.";
2149 2161
2150 2162 dialog.modal({
2151 2163 title : "Newer Notebook",
2152 2164 body : msg,
2153 2165 buttons : {
2154 2166 OK : {
2155 2167 class : "btn-danger"
2156 2168 }
2157 2169 }
2158 2170 });
2159 2171
2160 2172 }
2161 2173
2162 2174 // Create the session after the notebook is completely loaded to prevent
2163 2175 // code execution upon loading, which is a security risk.
2164 2176 if (this.session === null) {
2165 2177 this.start_session();
2166 2178 }
2167 2179 // load our checkpoint list
2168 2180 this.list_checkpoints();
2169 2181
2170 2182 // load toolbar state
2171 2183 if (this.metadata.celltoolbar) {
2172 2184 celltoolbar.CellToolbar.global_show();
2173 2185 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2174 2186 } else {
2175 2187 celltoolbar.CellToolbar.global_hide();
2176 2188 }
2177 2189
2178 2190 // now that we're fully loaded, it is safe to restore save functionality
2179 2191 delete(this.save_notebook);
2180 2192 this.events.trigger('notebook_loaded.Notebook');
2181 2193 };
2182 2194
2183 2195 /**
2184 2196 * Failure callback for loading a notebook from the server.
2185 2197 *
2186 2198 * @method load_notebook_error
2187 2199 * @param {jqXHR} xhr jQuery Ajax object
2188 2200 * @param {String} status Description of response status
2189 2201 * @param {String} error HTTP error message
2190 2202 */
2191 2203 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2192 2204 this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]);
2193 2205 var msg;
2194 2206 if (xhr.status === 400) {
2195 2207 msg = error;
2196 2208 } else if (xhr.status === 500) {
2197 2209 msg = "An unknown error occurred while loading this notebook. " +
2198 2210 "This version can load notebook formats " +
2199 2211 "v" + this.nbformat + " or earlier.";
2200 2212 }
2201 2213 dialog.modal({
2202 2214 title: "Error loading notebook",
2203 2215 body : msg,
2204 2216 buttons : {
2205 2217 "OK": {}
2206 2218 }
2207 2219 });
2208 2220 };
2209 2221
2210 2222 /********************* checkpoint-related *********************/
2211 2223
2212 2224 /**
2213 2225 * Save the notebook then immediately create a checkpoint.
2214 2226 *
2215 2227 * @method save_checkpoint
2216 2228 */
2217 2229 Notebook.prototype.save_checkpoint = function () {
2218 2230 this._checkpoint_after_save = true;
2219 2231 this.save_notebook();
2220 2232 };
2221 2233
2222 2234 /**
2223 2235 * Add a checkpoint for this notebook.
2224 2236 * for use as a callback from checkpoint creation.
2225 2237 *
2226 2238 * @method add_checkpoint
2227 2239 */
2228 2240 Notebook.prototype.add_checkpoint = function (checkpoint) {
2229 2241 var found = false;
2230 2242 for (var i = 0; i < this.checkpoints.length; i++) {
2231 2243 var existing = this.checkpoints[i];
2232 2244 if (existing.id == checkpoint.id) {
2233 2245 found = true;
2234 2246 this.checkpoints[i] = checkpoint;
2235 2247 break;
2236 2248 }
2237 2249 }
2238 2250 if (!found) {
2239 2251 this.checkpoints.push(checkpoint);
2240 2252 }
2241 2253 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2242 2254 };
2243 2255
2244 2256 /**
2245 2257 * List checkpoints for this notebook.
2246 2258 *
2247 2259 * @method list_checkpoints
2248 2260 */
2249 2261 Notebook.prototype.list_checkpoints = function () {
2250 2262 var url = utils.url_join_encode(
2251 2263 this.base_url,
2252 2264 'api/notebooks',
2253 2265 this.notebook_path,
2254 2266 this.notebook_name,
2255 2267 'checkpoints'
2256 2268 );
2257 2269 $.get(url).done(
2258 2270 $.proxy(this.list_checkpoints_success, this)
2259 2271 ).fail(
2260 2272 $.proxy(this.list_checkpoints_error, this)
2261 2273 );
2262 2274 };
2263 2275
2264 2276 /**
2265 2277 * Success callback for listing checkpoints.
2266 2278 *
2267 2279 * @method list_checkpoint_success
2268 2280 * @param {Object} data JSON representation of a checkpoint
2269 2281 * @param {String} status Description of response status
2270 2282 * @param {jqXHR} xhr jQuery Ajax object
2271 2283 */
2272 2284 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2273 2285 data = $.parseJSON(data);
2274 2286 this.checkpoints = data;
2275 2287 if (data.length) {
2276 2288 this.last_checkpoint = data[data.length - 1];
2277 2289 } else {
2278 2290 this.last_checkpoint = null;
2279 2291 }
2280 2292 this.events.trigger('checkpoints_listed.Notebook', [data]);
2281 2293 };
2282 2294
2283 2295 /**
2284 2296 * Failure callback for listing a checkpoint.
2285 2297 *
2286 2298 * @method list_checkpoint_error
2287 2299 * @param {jqXHR} xhr jQuery Ajax object
2288 2300 * @param {String} status Description of response status
2289 2301 * @param {String} error_msg HTTP error message
2290 2302 */
2291 2303 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2292 2304 this.events.trigger('list_checkpoints_failed.Notebook');
2293 2305 };
2294 2306
2295 2307 /**
2296 2308 * Create a checkpoint of this notebook on the server from the most recent save.
2297 2309 *
2298 2310 * @method create_checkpoint
2299 2311 */
2300 2312 Notebook.prototype.create_checkpoint = function () {
2301 2313 var url = utils.url_join_encode(
2302 2314 this.base_url,
2303 2315 'api/notebooks',
2304 2316 this.notebook_path,
2305 2317 this.notebook_name,
2306 2318 'checkpoints'
2307 2319 );
2308 2320 $.post(url).done(
2309 2321 $.proxy(this.create_checkpoint_success, this)
2310 2322 ).fail(
2311 2323 $.proxy(this.create_checkpoint_error, this)
2312 2324 );
2313 2325 };
2314 2326
2315 2327 /**
2316 2328 * Success callback for creating a checkpoint.
2317 2329 *
2318 2330 * @method create_checkpoint_success
2319 2331 * @param {Object} data JSON representation of a checkpoint
2320 2332 * @param {String} status Description of response status
2321 2333 * @param {jqXHR} xhr jQuery Ajax object
2322 2334 */
2323 2335 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2324 2336 data = $.parseJSON(data);
2325 2337 this.add_checkpoint(data);
2326 2338 this.events.trigger('checkpoint_created.Notebook', data);
2327 2339 };
2328 2340
2329 2341 /**
2330 2342 * Failure callback for creating a checkpoint.
2331 2343 *
2332 2344 * @method create_checkpoint_error
2333 2345 * @param {jqXHR} xhr jQuery Ajax object
2334 2346 * @param {String} status Description of response status
2335 2347 * @param {String} error_msg HTTP error message
2336 2348 */
2337 2349 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2338 2350 this.events.trigger('checkpoint_failed.Notebook');
2339 2351 };
2340 2352
2341 2353 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2342 2354 var that = this;
2343 2355 checkpoint = checkpoint || this.last_checkpoint;
2344 2356 if ( ! checkpoint ) {
2345 2357 console.log("restore dialog, but no checkpoint to restore to!");
2346 2358 return;
2347 2359 }
2348 2360 var body = $('<div/>').append(
2349 2361 $('<p/>').addClass("p-space").text(
2350 2362 "Are you sure you want to revert the notebook to " +
2351 2363 "the latest checkpoint?"
2352 2364 ).append(
2353 2365 $("<strong/>").text(
2354 2366 " This cannot be undone."
2355 2367 )
2356 2368 )
2357 2369 ).append(
2358 2370 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2359 2371 ).append(
2360 2372 $('<p/>').addClass("p-space").text(
2361 2373 Date(checkpoint.last_modified)
2362 2374 ).css("text-align", "center")
2363 2375 );
2364 2376
2365 2377 dialog.modal({
2366 2378 title : "Revert notebook to checkpoint",
2367 2379 body : body,
2368 2380 buttons : {
2369 2381 Revert : {
2370 2382 class : "btn-danger",
2371 2383 click : function () {
2372 2384 that.restore_checkpoint(checkpoint.id);
2373 2385 }
2374 2386 },
2375 2387 Cancel : {}
2376 2388 }
2377 2389 });
2378 2390 };
2379 2391
2380 2392 /**
2381 2393 * Restore the notebook to a checkpoint state.
2382 2394 *
2383 2395 * @method restore_checkpoint
2384 2396 * @param {String} checkpoint ID
2385 2397 */
2386 2398 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2387 2399 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2388 2400 var url = utils.url_join_encode(
2389 2401 this.base_url,
2390 2402 'api/notebooks',
2391 2403 this.notebook_path,
2392 2404 this.notebook_name,
2393 2405 'checkpoints',
2394 2406 checkpoint
2395 2407 );
2396 2408 $.post(url).done(
2397 2409 $.proxy(this.restore_checkpoint_success, this)
2398 2410 ).fail(
2399 2411 $.proxy(this.restore_checkpoint_error, this)
2400 2412 );
2401 2413 };
2402 2414
2403 2415 /**
2404 2416 * Success callback for restoring a notebook to a checkpoint.
2405 2417 *
2406 2418 * @method restore_checkpoint_success
2407 2419 * @param {Object} data (ignored, should be empty)
2408 2420 * @param {String} status Description of response status
2409 2421 * @param {jqXHR} xhr jQuery Ajax object
2410 2422 */
2411 2423 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2412 2424 this.events.trigger('checkpoint_restored.Notebook');
2413 2425 this.load_notebook(this.notebook_name, this.notebook_path);
2414 2426 };
2415 2427
2416 2428 /**
2417 2429 * Failure callback for restoring a notebook to a checkpoint.
2418 2430 *
2419 2431 * @method restore_checkpoint_error
2420 2432 * @param {jqXHR} xhr jQuery Ajax object
2421 2433 * @param {String} status Description of response status
2422 2434 * @param {String} error_msg HTTP error message
2423 2435 */
2424 2436 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2425 2437 this.events.trigger('checkpoint_restore_failed.Notebook');
2426 2438 };
2427 2439
2428 2440 /**
2429 2441 * Delete a notebook checkpoint.
2430 2442 *
2431 2443 * @method delete_checkpoint
2432 2444 * @param {String} checkpoint ID
2433 2445 */
2434 2446 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2435 2447 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2436 2448 var url = utils.url_join_encode(
2437 2449 this.base_url,
2438 2450 'api/notebooks',
2439 2451 this.notebook_path,
2440 2452 this.notebook_name,
2441 2453 'checkpoints',
2442 2454 checkpoint
2443 2455 );
2444 2456 $.ajax(url, {
2445 2457 type: 'DELETE',
2446 2458 success: $.proxy(this.delete_checkpoint_success, this),
2447 2459 error: $.proxy(this.delete_checkpoint_error, this)
2448 2460 });
2449 2461 };
2450 2462
2451 2463 /**
2452 2464 * Success callback for deleting a notebook checkpoint
2453 2465 *
2454 2466 * @method delete_checkpoint_success
2455 2467 * @param {Object} data (ignored, should be empty)
2456 2468 * @param {String} status Description of response status
2457 2469 * @param {jqXHR} xhr jQuery Ajax object
2458 2470 */
2459 2471 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2460 2472 this.events.trigger('checkpoint_deleted.Notebook', data);
2461 2473 this.load_notebook(this.notebook_name, this.notebook_path);
2462 2474 };
2463 2475
2464 2476 /**
2465 2477 * Failure callback for deleting a notebook checkpoint.
2466 2478 *
2467 2479 * @method delete_checkpoint_error
2468 2480 * @param {jqXHR} xhr jQuery Ajax object
2469 2481 * @param {String} status Description of response status
2470 2482 * @param {String} error_msg HTTP error message
2471 2483 */
2472 2484 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2473 2485 this.events.trigger('checkpoint_delete_failed.Notebook');
2474 2486 };
2475 2487
2476 2488
2477 2489 // For backwards compatability.
2478 2490 IPython.Notebook = Notebook;
2479 2491
2480 2492 return {'Notebook': Notebook};
2481 2493 });
@@ -1,233 +1,233 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 'notebook/js/notificationwidget',
10 10 ], function(IPython, $, utils, dialog, notificationwidget) {
11 11 "use strict";
12 12
13 var NotificationArea = function (selector, events, save_widget, notebook) {
13 var NotificationArea = function (selector, options) {
14 14 this.selector = selector;
15 this.events = events;
16 this.save_widget = save_widget;
17 this.notebook = notebook;
15 this.events = options.events;
16 this.save_widget = options.save_widget;
17 this.notebook = options.notebook;
18 18 if (this.selector !== undefined) {
19 19 this.element = $(selector);
20 20 }
21 21 this.widget_dict = {};
22 22 };
23 23
24 24 NotificationArea.prototype.temp_message = function (msg, timeout, css_class) {
25 25 var uuid = utils.uuid();
26 26 if( css_class == 'danger') {css_class = 'ui-state-error';}
27 27 if( css_class == 'warning') {css_class = 'ui-state-highlight';}
28 28 var tdiv = $('<div>')
29 29 .attr('id',uuid)
30 30 .addClass('notification_widget ui-widget ui-widget-content ui-corner-all')
31 31 .addClass('border-box-sizing')
32 32 .addClass(css_class)
33 33 .hide()
34 34 .text(msg);
35 35
36 36 $(this.selector).append(tdiv);
37 37 var tmout = Math.max(1500,(timeout||1500));
38 38 tdiv.fadeIn(100);
39 39
40 40 setTimeout(function () {
41 41 tdiv.fadeOut(100, function () {tdiv.remove();});
42 42 }, tmout);
43 43 };
44 44
45 45 NotificationArea.prototype.widget = function(name) {
46 46 if(this.widget_dict[name] === undefined) {
47 47 return this.new_notification_widget(name);
48 48 }
49 49 return this.get_widget(name);
50 50 };
51 51
52 52 NotificationArea.prototype.get_widget = function(name) {
53 53 if(this.widget_dict[name] === undefined) {
54 54 throw('no widgets with this name');
55 55 }
56 56 return this.widget_dict[name];
57 57 };
58 58
59 59 NotificationArea.prototype.new_notification_widget = function(name) {
60 60 if(this.widget_dict[name] !== undefined) {
61 61 throw('widget with that name already exists ! ');
62 62 }
63 63 var div = $('<div/>').attr('id','notification_'+name);
64 64 $(this.selector).append(div);
65 65 this.widget_dict[name] = new notificationwidget.NotificationWidget('#notification_'+name);
66 66 return this.widget_dict[name];
67 67 };
68 68
69 69 NotificationArea.prototype.init_notification_widgets = function() {
70 70 var that = this;
71 71 var knw = this.new_notification_widget('kernel');
72 72 var $kernel_ind_icon = $("#kernel_indicator_icon");
73 73 var $modal_ind_icon = $("#modal_indicator_icon");
74 74
75 75 // Command/Edit mode
76 76 this.events.on('edit_mode.Notebook',function () {
77 77 that.save_widget.update_document_title();
78 78 $modal_ind_icon.attr('class','edit_mode_icon').attr('title','Edit Mode');
79 79 });
80 80
81 81 this.events.on('command_mode.Notebook',function () {
82 82 that.save_widget.update_document_title();
83 83 $modal_ind_icon.attr('class','command_mode_icon').attr('title','Command Mode');
84 84 });
85 85
86 86 // Implicitly start off in Command mode, switching to Edit mode will trigger event
87 87 $modal_ind_icon.attr('class','command-mode_icon').attr('title','Command Mode');
88 88
89 89 // Kernel events
90 90 this.events.on('status_idle.Kernel',function () {
91 91 that.save_widget.update_document_title();
92 92 $kernel_ind_icon.attr('class','kernel_idle_icon').attr('title','Kernel Idle');
93 93 });
94 94
95 95 this.events.on('status_busy.Kernel',function () {
96 96 window.document.title='(Busy) '+window.document.title;
97 97 $kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
98 98 });
99 99
100 100 this.events.on('status_restarting.Kernel',function () {
101 101 that.save_widget.update_document_title();
102 102 knw.set_message("Restarting kernel", 2000);
103 103 });
104 104
105 105 this.events.on('status_interrupting.Kernel',function () {
106 106 knw.set_message("Interrupting kernel", 2000);
107 107 });
108 108
109 109 // Start the kernel indicator in the busy state, and send a kernel_info request.
110 110 // When the kernel_info reply arrives, the kernel is idle.
111 111 $kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
112 112
113 113 this.events.on('status_started.Kernel', function (evt, data) {
114 114 data.kernel.kernel_info(function () {
115 115 that.events.trigger('status_idle.Kernel');
116 116 });
117 117 });
118 118
119 119 this.events.on('status_dead.Kernel',function () {
120 120 var msg = 'The kernel has died, and the automatic restart has failed.' +
121 121 ' It is possible the kernel cannot be restarted.' +
122 122 ' If you are not able to restart the kernel, you will still be able to save' +
123 123 ' the notebook, but running code will no longer work until the notebook' +
124 124 ' is reopened.';
125 125
126 126 dialog.modal({
127 127 title: "Dead kernel",
128 128 body : msg,
129 129 buttons : {
130 130 "Manual Restart": {
131 131 class: "btn-danger",
132 132 click: function () {
133 133 that.events.trigger('status_restarting.Kernel');
134 134 that.notebook.start_kernel();
135 135 }
136 136 },
137 137 "Don't restart": {}
138 138 }
139 139 });
140 140 });
141 141
142 142 this.events.on('websocket_closed.Kernel', function (event, data) {
143 143 var kernel = data.kernel;
144 144 var ws_url = data.ws_url;
145 145 var early = data.early;
146 146 var msg;
147 147 if (!early) {
148 148 knw.set_message('Reconnecting WebSockets', 1000);
149 149 setTimeout(function () {
150 150 kernel.start_channels();
151 151 }, 5000);
152 152 return;
153 153 }
154 154 console.log('WebSocket connection failed: ', ws_url);
155 155 msg = "A WebSocket connection could not be established." +
156 156 " You will NOT be able to run code. Check your" +
157 157 " network connection or notebook server configuration.";
158 158 dialog.modal({
159 159 title: "WebSocket connection failed",
160 160 body: msg,
161 161 buttons : {
162 162 "OK": {},
163 163 "Reconnect": {
164 164 click: function () {
165 165 knw.set_message('Reconnecting WebSockets', 1000);
166 166 setTimeout(function () {
167 167 kernel.start_channels();
168 168 }, 5000);
169 169 }
170 170 }
171 171 }
172 172 });
173 173 });
174 174
175 175
176 176 var nnw = this.new_notification_widget('notebook');
177 177
178 178 // Notebook events
179 179 this.events.on('notebook_loading.Notebook', function () {
180 180 nnw.set_message("Loading notebook",500);
181 181 });
182 182 this.events.on('notebook_loaded.Notebook', function () {
183 183 nnw.set_message("Notebook loaded",500);
184 184 });
185 185 this.events.on('notebook_saving.Notebook', function () {
186 186 nnw.set_message("Saving notebook",500);
187 187 });
188 188 this.events.on('notebook_saved.Notebook', function () {
189 189 nnw.set_message("Notebook saved",2000);
190 190 });
191 191 this.events.on('notebook_save_failed.Notebook', function (evt, xhr, status, data) {
192 192 nnw.set_message(data || "Notebook save failed");
193 193 });
194 194
195 195 // Checkpoint events
196 196 this.events.on('checkpoint_created.Notebook', function (evt, data) {
197 197 var msg = "Checkpoint created";
198 198 if (data.last_modified) {
199 199 var d = new Date(data.last_modified);
200 200 msg = msg + ": " + d.format("HH:MM:ss");
201 201 }
202 202 nnw.set_message(msg, 2000);
203 203 });
204 204 this.events.on('checkpoint_failed.Notebook', function () {
205 205 nnw.set_message("Checkpoint failed");
206 206 });
207 207 this.events.on('checkpoint_deleted.Notebook', function () {
208 208 nnw.set_message("Checkpoint deleted", 500);
209 209 });
210 210 this.events.on('checkpoint_delete_failed.Notebook', function () {
211 211 nnw.set_message("Checkpoint delete failed");
212 212 });
213 213 this.events.on('checkpoint_restoring.Notebook', function () {
214 214 nnw.set_message("Restoring to checkpoint...", 500);
215 215 });
216 216 this.events.on('checkpoint_restore_failed.Notebook', function () {
217 217 nnw.set_message("Checkpoint restore failed");
218 218 });
219 219
220 220 // Autosave events
221 221 this.events.on('autosave_disabled.Notebook', function () {
222 222 nnw.set_message("Autosave disabled", 2000);
223 223 });
224 224 this.events.on('autosave_enabled.Notebook', function (evt, interval) {
225 225 nnw.set_message("Saving every " + interval / 1000 + "s", 1000);
226 226 });
227 227
228 228 };
229 229
230 230 IPython.NotificationArea = NotificationArea;
231 231
232 232 return {'NotificationArea': NotificationArea};
233 233 });
@@ -1,178 +1,178 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 ], function(IPython, $, utils) {
9 9 "use strict";
10 10
11 var Pager = function (pager_selector, pager_splitter_selector, layout_manager, events) {
12 this.events = events;
11 var Pager = function (pager_selector, pager_splitter_selector, options) {
12 this.events = options.events;
13 13 this.pager_element = $(pager_selector);
14 14 this.pager_button_area = $('#pager_button_area');
15 15 var that = this;
16 16 this.percentage_height = 0.40;
17 layout_manager.pager = this;
17 options.layout_manager.pager = this;
18 18 this.pager_splitter_element = $(pager_splitter_selector)
19 19 .draggable({
20 20 containment: 'window',
21 21 axis:'y',
22 22 helper: null ,
23 23 drag: function(event, ui) {
24 24 // recalculate the amount of space the pager should take
25 25 var pheight = ($(document.body).height()-event.clientY-4);
26 var downprct = pheight/layout_manager.app_height();
26 var downprct = pheight/options.layout_manager.app_height();
27 27 downprct = Math.min(0.9, downprct);
28 28 if (downprct < 0.1) {
29 29 that.percentage_height = 0.1;
30 30 that.collapse({'duration':0});
31 31 } else if (downprct > 0.2) {
32 32 that.percentage_height = downprct;
33 33 that.expand({'duration':0});
34 34 }
35 layout_manager.do_resize();
35 options.layout_manager.do_resize();
36 36 }
37 37 });
38 38 this.expanded = false;
39 39 this.style();
40 40 this.create_button_area();
41 41 this.bind_events();
42 42 };
43 43
44 44 Pager.prototype.create_button_area = function(){
45 45 var that = this;
46 46 this.pager_button_area.append(
47 47 $('<a>').attr('role', "button")
48 48 .attr('title',"Open the pager in an external window")
49 49 .addClass('ui-button')
50 50 .click(function(){that.detach();})
51 51 .attr('style','position: absolute; right: 20px;')
52 52 .append(
53 53 $('<span>').addClass("ui-icon ui-icon-extlink")
54 54 )
55 55 );
56 56 this.pager_button_area.append(
57 57 $('<a>').attr('role', "button")
58 58 .attr('title',"Close the pager")
59 59 .addClass('ui-button')
60 60 .click(function(){that.collapse();})
61 61 .attr('style','position: absolute; right: 5px;')
62 62 .append(
63 63 $('<span>').addClass("ui-icon ui-icon-close")
64 64 )
65 65 );
66 66 };
67 67
68 68 Pager.prototype.style = function () {
69 69 this.pager_splitter_element.addClass('border-box-sizing ui-widget ui-state-default');
70 70 this.pager_element.addClass('border-box-sizing');
71 71 this.pager_element.find(".container").addClass('border-box-sizing');
72 72 this.pager_splitter_element.attr('title', 'Click to Show/Hide pager area, drag to Resize');
73 73 };
74 74
75 75
76 76 Pager.prototype.bind_events = function () {
77 77 var that = this;
78 78
79 79 this.pager_element.bind('collapse_pager', function (event, extrap) {
80 80 var time = 'fast';
81 81 if (extrap && extrap.duration) {
82 82 time = extrap.duration;
83 83 }
84 84 that.pager_element.hide(time);
85 85 });
86 86
87 87 this.pager_element.bind('expand_pager', function (event, extrap) {
88 88 var time = 'fast';
89 89 if (extrap && extrap.duration) {
90 90 time = extrap.duration;
91 91 }
92 92 that.pager_element.show(time);
93 93 });
94 94
95 95 this.pager_splitter_element.hover(
96 96 function () {
97 97 that.pager_splitter_element.addClass('ui-state-hover');
98 98 },
99 99 function () {
100 100 that.pager_splitter_element.removeClass('ui-state-hover');
101 101 }
102 102 );
103 103
104 104 this.pager_splitter_element.click(function () {
105 105 that.toggle();
106 106 });
107 107
108 108 this.events.on('open_with_text.Pager', function (event, payload) {
109 109 // FIXME: support other mime types
110 110 if (payload.data['text/plain'] && payload.data['text/plain'] !== "") {
111 111 that.clear();
112 112 that.expand();
113 113 that.append_text(payload.data['text/plain']);
114 114 }
115 115 });
116 116 };
117 117
118 118
119 119 Pager.prototype.collapse = function (extrap) {
120 120 if (this.expanded === true) {
121 121 this.expanded = false;
122 122 this.pager_element.add($('div#notebook')).trigger('collapse_pager', extrap);
123 123 }
124 124 };
125 125
126 126
127 127 Pager.prototype.expand = function (extrap) {
128 128 if (this.expanded !== true) {
129 129 this.expanded = true;
130 130 this.pager_element.add($('div#notebook')).trigger('expand_pager', extrap);
131 131 }
132 132 };
133 133
134 134
135 135 Pager.prototype.toggle = function () {
136 136 if (this.expanded === true) {
137 137 this.collapse();
138 138 } else {
139 139 this.expand();
140 140 }
141 141 };
142 142
143 143
144 144 Pager.prototype.clear = function (text) {
145 145 this.pager_element.find(".container").empty();
146 146 };
147 147
148 148 Pager.prototype.detach = function(){
149 149 var w = window.open("","_blank");
150 150 $(w.document.head)
151 151 .append(
152 152 $('<link>')
153 153 .attr('rel',"stylesheet")
154 154 .attr('href',"/static/css/notebook.css")
155 155 .attr('type',"text/css")
156 156 )
157 157 .append(
158 158 $('<title>').text("IPython Pager")
159 159 );
160 160 var pager_body = $(w.document.body);
161 161 pager_body.css('overflow','scroll');
162 162
163 163 pager_body.append(this.pager_element.clone().children());
164 164 w.document.close();
165 165 this.collapse();
166 166 };
167 167
168 168 Pager.prototype.append_text = function (text) {
169 169 // The only user content injected with this HTML call is escaped by
170 170 // the fixConsole() method.
171 171 this.pager_element.find(".container").append($('<pre/>').html(utils.fixCarriageReturn(utils.fixConsole(text))));
172 172 };
173 173
174 174 // Backwards compatability.
175 175 IPython.Pager = Pager;
176 176
177 177 return {'Pager': Pager};
178 178 });
@@ -1,173 +1,173 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 ], function(IPython, $, utils, dialog) {
10 10 "use strict";
11 11 var platform = utils.platform;
12 12
13 var QuickHelp = function (selector, keyboard_manager, events) {
14 this.keyboard_manager = keyboard_manager;
15 keyboard_manager.quick_help = this;
16 this.events = events;
13 var QuickHelp = function (options) {
14 this.keyboard_manager = options.keyboard_manager;
15 this.keyboard_manager.quick_help = this;
16 this.events = options.events;
17 17 };
18 18
19 19 var cmd_ctrl = 'Ctrl-';
20 20 var platform_specific;
21 21
22 22 if (platform === 'MacOS') {
23 23 // Mac OS X specific
24 24 cmd_ctrl = 'Cmd-';
25 25 platform_specific = [
26 26 { shortcut: "Cmd-Up", help:"go to cell start" },
27 27 { shortcut: "Cmd-Down", help:"go to cell end" },
28 28 { shortcut: "Opt-Left", help:"go one word left" },
29 29 { shortcut: "Opt-Right", help:"go one word right" },
30 30 { shortcut: "Opt-Backspace", help:"del word before" },
31 31 { shortcut: "Opt-Delete", help:"del word after" },
32 32 ];
33 33 } else {
34 34 // PC specific
35 35 platform_specific = [
36 36 { shortcut: "Ctrl-Home", help:"go to cell start" },
37 37 { shortcut: "Ctrl-Up", help:"go to cell start" },
38 38 { shortcut: "Ctrl-End", help:"go to cell end" },
39 39 { shortcut: "Ctrl-Down", help:"go to cell end" },
40 40 { shortcut: "Ctrl-Left", help:"go one word left" },
41 41 { shortcut: "Ctrl-Right", help:"go one word right" },
42 42 { shortcut: "Ctrl-Backspace", help:"del word before" },
43 43 { shortcut: "Ctrl-Delete", help:"del word after" },
44 44 ];
45 45 }
46 46
47 47 var cm_shortcuts = [
48 48 { shortcut:"Tab", help:"code completion or indent" },
49 49 { shortcut:"Shift-Tab", help:"tooltip" },
50 50 { shortcut: cmd_ctrl + "]", help:"indent" },
51 51 { shortcut: cmd_ctrl + "[", help:"dedent" },
52 52 { shortcut: cmd_ctrl + "a", help:"select all" },
53 53 { shortcut: cmd_ctrl + "z", help:"undo" },
54 54 { shortcut: cmd_ctrl + "Shift-z", help:"redo" },
55 55 { shortcut: cmd_ctrl + "y", help:"redo" },
56 56 ].concat( platform_specific );
57 57
58 58
59 59
60 60
61 61
62 62
63 63 QuickHelp.prototype.show_keyboard_shortcuts = function () {
64 64 // toggles display of keyboard shortcut dialog
65 65 var that = this;
66 66 if ( this.force_rebuild ) {
67 67 this.shortcut_dialog.remove();
68 68 delete(this.shortcut_dialog);
69 69 this.force_rebuild = false;
70 70 }
71 71 if ( this.shortcut_dialog ){
72 72 // if dialog is already shown, close it
73 73 $(this.shortcut_dialog).modal("toggle");
74 74 return;
75 75 }
76 76 var command_shortcuts = this.keyboard_manager.command_shortcuts.help();
77 77 var edit_shortcuts = this.keyboard_manager.edit_shortcuts.help();
78 78 var help, shortcut;
79 79 var i, half, n;
80 80 var element = $('<div/>');
81 81
82 82 // The documentation
83 83 var doc = $('<div/>').addClass('alert alert-warning');
84 84 doc.append(
85 85 $('<button/>').addClass('close').attr('data-dismiss','alert').html('&times;')
86 86 ).append(
87 87 'The IPython Notebook has two different keyboard input modes. <b>Edit mode</b> '+
88 88 'allows you to type code/text into a cell and is indicated by a green cell '+
89 89 'border. <b>Command mode</b> binds the keyboard to notebook level actions '+
90 90 'and is indicated by a grey cell border.'
91 91 );
92 92 element.append(doc);
93 93
94 94 // Command mode
95 95 var cmd_div = this.build_command_help();
96 96 element.append(cmd_div);
97 97
98 98 // Edit mode
99 99 var edit_div = this.build_edit_help(cm_shortcuts);
100 100 element.append(edit_div);
101 101
102 102 this.shortcut_dialog = dialog.modal({
103 103 title : "Keyboard shortcuts",
104 104 body : element,
105 105 destroy : false,
106 106 buttons : {
107 107 Close : {}
108 108 }
109 109 });
110 110 this.shortcut_dialog.addClass("modal_stretch");
111 111
112 112 this.events.on('rebuild.QuickHelp', function() { that.force_rebuild = true;});
113 113 };
114 114
115 115 QuickHelp.prototype.build_command_help = function () {
116 116 var command_shortcuts = this.keyboard_manager.command_shortcuts.help();
117 117 return build_div('<h4>Command Mode (press <code>Esc</code> to enable)</h4>', command_shortcuts);
118 118 };
119 119
120 120 var special_case = { pageup: "PageUp", pagedown: "Page Down", 'minus': '-' };
121 121 var prettify = function (s) {
122 122 s = s.replace(/-$/, 'minus'); // catch shortcuts using '-' key
123 123 var keys = s.split('-');
124 124 var k, i;
125 125 for (i=0; i < keys.length; i++) {
126 126 k = keys[i];
127 127 if ( k.length == 1 ) {
128 128 keys[i] = "<code><strong>" + k + "</strong></code>";
129 129 continue; // leave individual keys lower-cased
130 130 }
131 131 keys[i] = ( special_case[k] ? special_case[k] : k.charAt(0).toUpperCase() + k.slice(1) );
132 132 keys[i] = "<code><strong>" + keys[i] + "</strong></code>";
133 133 }
134 134 return keys.join('-');
135 135
136 136
137 137 };
138 138
139 139 QuickHelp.prototype.build_edit_help = function (cm_shortcuts) {
140 140 var edit_shortcuts = this.keyboard_manager.edit_shortcuts.help();
141 141 jQuery.merge(cm_shortcuts, edit_shortcuts);
142 142 return build_div('<h4>Edit Mode (press <code>Enter</code> to enable)</h4>', cm_shortcuts);
143 143 };
144 144
145 145 var build_one = function (s) {
146 146 var help = s.help;
147 147 var shortcut = prettify(s.shortcut);
148 148 return $('<div>').addClass('quickhelp').
149 149 append($('<span/>').addClass('shortcut_key').append($(shortcut))).
150 150 append($('<span/>').addClass('shortcut_descr').text(' : ' + help));
151 151
152 152 };
153 153
154 154 var build_div = function (title, shortcuts) {
155 155 var i, half, n;
156 156 var div = $('<div/>').append($(title));
157 157 var sub_div = $('<div/>').addClass('hbox');
158 158 var col1 = $('<div/>').addClass('box-flex1');
159 159 var col2 = $('<div/>').addClass('box-flex1');
160 160 n = shortcuts.length;
161 161 half = ~~(n/2); // Truncate :)
162 162 for (i=0; i<half; i++) { col1.append( build_one(shortcuts[i]) ); }
163 163 for (i=half; i<n; i++) { col2.append( build_one(shortcuts[i]) ); }
164 164 sub_div.append(col1).append(col2);
165 165 div.append(sub_div);
166 166 return div;
167 167 };
168 168
169 169 // Backwards compatability.
170 170 IPython.QuickHelp = QuickHelp;
171 171
172 172 return {'QuickHelp': QuickHelp};
173 173 });
@@ -1,454 +1,453 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'notebook/js/cell',
8 8 'base/js/security',
9 9 'notebook/js/mathjaxutils',
10 10 'notebook/js/celltoolbar',
11 11 'components/marked/lib/marked',
12 12 ], function(IPython, $, cell, security, mathjaxutils, celltoolbar, marked) {
13 13 "use strict";
14 14 var Cell = cell.Cell;
15 15
16 16 /**
17 17 * Construct a new TextCell, codemirror mode is by default 'htmlmixed', and cell type is 'text'
18 18 * cell start as not redered.
19 19 *
20 20 * @class TextCell
21 21 * @constructor TextCell
22 22 * @extend Cell
23 23 * @param {object|undefined} [options]
24 24 * @param [options.cm_config] {object} config to pass to CodeMirror, will extend/overwrite default config
25 25 * @param [options.placeholder] {string} default string to use when souce in empty for rendering (only use in some TextCell subclass)
26 26 */
27 var TextCell = function (options, events, config, keyboard_manager, notebook) {
27 var TextCell = function (options) {
28 options = options || {};
28 29 // in all TextCell/Cell subclasses
29 30 // do not assign most of members here, just pass it down
30 31 // in the options dict potentially overwriting what you wish.
31 32 // they will be assigned in the base class.
32 this.notebook = notebook;
33 this.events = events;
34 this.config = config;
33 this.notebook = options.notebook;
34 this.events = options.events;
35 this.config = options.config;
35 36
36 37 // we cannot put this as a class key as it has handle to "this".
37 38 var cm_overwrite_options = {
38 39 onKeyEvent: $.proxy(this.handle_keyevent,this)
39 40 };
40
41 options = this.mergeopt(TextCell,options,{cm_config:cm_overwrite_options});
41 var config = this.mergeopt(TextCell, this.config, {cm_config:cm_overwrite_options});
42 Cell.apply(this, [{
43 config: config,
44 keyboard_manager: options.keyboard_manager,
45 events: events}]);
42 46
43 47 this.cell_type = this.cell_type || 'text';
44 48 mathjaxutils = mathjaxutils;
45
46 Cell.apply(this, [options, keyboard_manager, events]);
47
48 49 this.rendered = false;
49 50 };
50 51
51 52 TextCell.prototype = new Cell();
52 53
53 54 TextCell.options_default = {
54 55 cm_config : {
55 56 extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
56 57 mode: 'htmlmixed',
57 58 lineWrapping : true,
58 59 }
59 60 };
60 61
61 62
62 63 /**
63 64 * Create the DOM element of the TextCell
64 65 * @method create_element
65 66 * @private
66 67 */
67 68 TextCell.prototype.create_element = function () {
68 69 Cell.prototype.create_element.apply(this, arguments);
69 70
70 71 var cell = $("<div>").addClass('cell text_cell border-box-sizing');
71 72 cell.attr('tabindex','2');
72 73
73 74 var prompt = $('<div/>').addClass('prompt input_prompt');
74 75 cell.append(prompt);
75 76 var inner_cell = $('<div/>').addClass('inner_cell');
76 77 this.celltoolbar = new celltoolbar.CellToolbar(this, this.events, this.notebook);
77 78 inner_cell.append(this.celltoolbar.element);
78 79 var input_area = $('<div/>').addClass('input_area');
79 80 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
80 81 // The tabindex=-1 makes this div focusable.
81 82 var render_area = $('<div/>').addClass('text_cell_render border-box-sizing').
82 83 addClass('rendered_html').attr('tabindex','-1');
83 84 inner_cell.append(input_area).append(render_area);
84 85 cell.append(inner_cell);
85 86 this.element = cell;
86 87 };
87 88
88 89
89 90 /**
90 91 * Bind the DOM evet to cell actions
91 92 * Need to be called after TextCell.create_element
92 93 * @private
93 94 * @method bind_event
94 95 */
95 96 TextCell.prototype.bind_events = function () {
96 97 Cell.prototype.bind_events.apply(this);
97 98 var that = this;
98 99
99 100 this.element.dblclick(function () {
100 101 if (that.selected === false) {
101 102 this.events.trigger('select.Cell', {'cell':that});
102 103 }
103 104 var cont = that.unrender();
104 105 if (cont) {
105 106 that.focus_editor();
106 107 }
107 108 });
108 109 };
109 110
110 111 // Cell level actions
111 112
112 113 TextCell.prototype.select = function () {
113 114 var cont = Cell.prototype.select.apply(this);
114 115 if (cont) {
115 116 if (this.mode === 'edit') {
116 117 this.code_mirror.refresh();
117 118 }
118 119 }
119 120 return cont;
120 121 };
121 122
122 123 TextCell.prototype.unrender = function () {
123 124 if (this.read_only) return;
124 125 var cont = Cell.prototype.unrender.apply(this);
125 126 if (cont) {
126 127 var text_cell = this.element;
127 128 var output = text_cell.find("div.text_cell_render");
128 129 output.hide();
129 130 text_cell.find('div.input_area').show();
130 131 if (this.get_text() === this.placeholder) {
131 132 this.set_text('');
132 133 }
133 134 this.refresh();
134 135 }
135 136 if (this.celltoolbar.ui_controls_list.length) {
136 137 this.celltoolbar.show();
137 138 }
138 139 return cont;
139 140 };
140 141
141 142 TextCell.prototype.execute = function () {
142 143 this.render();
143 144 };
144 145
145 146 /**
146 147 * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
147 148 * @method get_text
148 149 * @retrun {string} CodeMirror current text value
149 150 */
150 151 TextCell.prototype.get_text = function() {
151 152 return this.code_mirror.getValue();
152 153 };
153 154
154 155 /**
155 156 * @param {string} text - Codemiror text value
156 157 * @see TextCell#get_text
157 158 * @method set_text
158 159 * */
159 160 TextCell.prototype.set_text = function(text) {
160 161 this.code_mirror.setValue(text);
161 162 this.code_mirror.refresh();
162 163 };
163 164
164 165 /**
165 166 * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
166 167 * @method get_rendered
167 168 * */
168 169 TextCell.prototype.get_rendered = function() {
169 170 return this.element.find('div.text_cell_render').html();
170 171 };
171 172
172 173 /**
173 174 * @method set_rendered
174 175 */
175 176 TextCell.prototype.set_rendered = function(text) {
176 177 this.element.find('div.text_cell_render').html(text);
177 178 this.celltoolbar.hide();
178 179 };
179 180
180 181
181 182 /**
182 183 * Create Text cell from JSON
183 184 * @param {json} data - JSON serialized text-cell
184 185 * @method fromJSON
185 186 */
186 187 TextCell.prototype.fromJSON = function (data) {
187 188 Cell.prototype.fromJSON.apply(this, arguments);
188 189 if (data.cell_type === this.cell_type) {
189 190 if (data.source !== undefined) {
190 191 this.set_text(data.source);
191 192 // make this value the starting point, so that we can only undo
192 193 // to this state, instead of a blank cell
193 194 this.code_mirror.clearHistory();
194 195 // TODO: This HTML needs to be treated as potentially dangerous
195 196 // user input and should be handled before set_rendered.
196 197 this.set_rendered(data.rendered || '');
197 198 this.rendered = false;
198 199 this.render();
199 200 }
200 201 }
201 202 };
202 203
203 204 /** Generate JSON from cell
204 205 * @return {object} cell data serialised to json
205 206 */
206 207 TextCell.prototype.toJSON = function () {
207 208 var data = Cell.prototype.toJSON.apply(this);
208 209 data.source = this.get_text();
209 210 if (data.source == this.placeholder) {
210 211 data.source = "";
211 212 }
212 213 return data;
213 214 };
214 215
215 216
216 217 /**
217 218 * @class MarkdownCell
218 219 * @constructor MarkdownCell
219 220 * @extends IPython.HTMLCell
220 221 */
221 var MarkdownCell = function (options, events, config, keyboard_manager) {
222 options = this.mergeopt(MarkdownCell, options);
222 var MarkdownCell = function (options) {
223 options = options || {};
224 var config = this.mergeopt(MarkdownCell, options.config);
225 TextCell.apply(this, [$.extend({}, options, {config: config})]);
223 226
224 227 this.cell_type = 'markdown';
225 TextCell.apply(this, [options, events, config, keyboard_manager]);
226 228 };
227 229
228 230 MarkdownCell.options_default = {
229 231 cm_config: {
230 232 mode: 'ipythongfm'
231 233 },
232 234 placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
233 235 };
234 236
235 237 MarkdownCell.prototype = new TextCell();
236 238
237 239 /**
238 240 * @method render
239 241 */
240 242 MarkdownCell.prototype.render = function () {
241 243 var cont = TextCell.prototype.render.apply(this);
242 244 if (cont) {
243 245 var text = this.get_text();
244 246 var math = null;
245 247 if (text === "") { text = this.placeholder; }
246 248 var text_and_math = mathjaxutils.remove_math(text);
247 249 text = text_and_math[0];
248 250 math = text_and_math[1];
249 251 var html = marked.parser(marked.lexer(text));
250 252 html = mathjaxutils.replace_math(html, math);
251 253 html = security.sanitize_html(html);
252 254 html = $($.parseHTML(html));
253 255 // links in markdown cells should open in new tabs
254 256 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
255 257 this.set_rendered(html);
256 258 this.element.find('div.input_area').hide();
257 259 this.element.find("div.text_cell_render").show();
258 260 this.typeset();
259 261 }
260 262 return cont;
261 263 };
262 264
263 265
264 266 // RawCell
265 267
266 268 /**
267 269 * @class RawCell
268 270 * @constructor RawCell
269 271 * @extends TextCell
270 272 */
271 var RawCell = function (options, events, config, keyboard_manager) {
273 var RawCell = function (options) {
274 options = options || {};
275 var config = this.mergeopt(RawCell, options.config);
276 TextCell.apply(this, [$.extend({}, options, {config: config})]);
272 277
273 options = this.mergeopt(RawCell,options);
274 TextCell.apply(this, [options, events, config, keyboard_manager]);
275 this.cell_type = 'raw';
276 278 // RawCell should always hide its rendered div
277 279 this.element.find('div.text_cell_render').hide();
280 this.cell_type = 'raw';
278 281 };
279 282
280 283 RawCell.options_default = {
281 284 placeholder : "Write raw LaTeX or other formats here, for use with nbconvert. " +
282 285 "It will not be rendered in the notebook. " +
283 286 "When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
284 287 };
285 288
286 289 RawCell.prototype = new TextCell();
287 290
288 291 /** @method bind_events **/
289 292 RawCell.prototype.bind_events = function () {
290 293 TextCell.prototype.bind_events.apply(this);
291 294 var that = this;
292 295 this.element.focusout(function() {
293 296 that.auto_highlight();
294 297 that.render();
295 298 });
296 299
297 300 this.code_mirror.on('focus', function() { that.unrender(); });
298 301 };
299 302
300 303 /**
301 304 * Trigger autodetection of highlight scheme for current cell
302 305 * @method auto_highlight
303 306 */
304 307 RawCell.prototype.auto_highlight = function () {
305 308 this._auto_highlight(this.config.raw_cell_highlight);
306 309 };
307 310
308 311 /** @method render **/
309 312 RawCell.prototype.render = function () {
310 313 var cont = TextCell.prototype.render.apply(this);
311 314 if (cont){
312 315 var text = this.get_text();
313 316 if (text === "") { text = this.placeholder; }
314 317 this.set_text(text);
315 318 this.element.removeClass('rendered');
316 319 }
317 320 return cont;
318 321 };
319 322
320 323
321 324 /**
322 325 * @class HeadingCell
323 326 * @extends TextCell
324 327 */
325 328
326 329 /**
327 330 * @constructor HeadingCell
328 331 * @extends TextCell
329 332 */
330 var HeadingCell = function (options, events, config, keyboard_manager) {
331 options = this.mergeopt(HeadingCell, options);
333 var HeadingCell = function (options) {
334 options = options || {};
335 var config = this.mergeopt(HeadingCell, options.config);
336 TextCell.apply(this, [$.extend({}, options, {config: config})]);
332 337
333 338 this.level = 1;
334 339 this.cell_type = 'heading';
335 TextCell.apply(this, [options, events, config, keyboard_manager]);
336
337 /**
338 * heading level of the cell, use getter and setter to access
339 * @property level
340 */
341 340 };
342 341
343 342 HeadingCell.options_default = {
344 343 placeholder: "Type Heading Here"
345 344 };
346 345
347 346 HeadingCell.prototype = new TextCell();
348 347
349 348 /** @method fromJSON */
350 349 HeadingCell.prototype.fromJSON = function (data) {
351 350 if (data.level !== undefined){
352 351 this.level = data.level;
353 352 }
354 353 TextCell.prototype.fromJSON.apply(this, arguments);
355 354 };
356 355
357 356
358 357 /** @method toJSON */
359 358 HeadingCell.prototype.toJSON = function () {
360 359 var data = TextCell.prototype.toJSON.apply(this);
361 360 data.level = this.get_level();
362 361 return data;
363 362 };
364 363
365 364 /**
366 365 * can the cell be split into two cells
367 366 * @method is_splittable
368 367 **/
369 368 HeadingCell.prototype.is_splittable = function () {
370 369 return false;
371 370 };
372 371
373 372
374 373 /**
375 374 * can the cell be merged with other cells
376 375 * @method is_mergeable
377 376 **/
378 377 HeadingCell.prototype.is_mergeable = function () {
379 378 return false;
380 379 };
381 380
382 381 /**
383 382 * Change heading level of cell, and re-render
384 383 * @method set_level
385 384 */
386 385 HeadingCell.prototype.set_level = function (level) {
387 386 this.level = level;
388 387 if (this.rendered) {
389 388 this.rendered = false;
390 389 this.render();
391 390 }
392 391 };
393 392
394 393 /** The depth of header cell, based on html (h1 to h6)
395 394 * @method get_level
396 395 * @return {integer} level - for 1 to 6
397 396 */
398 397 HeadingCell.prototype.get_level = function () {
399 398 return this.level;
400 399 };
401 400
402 401
403 402 HeadingCell.prototype.get_rendered = function () {
404 403 var r = this.element.find("div.text_cell_render");
405 404 return r.children().first().html();
406 405 };
407 406
408 407 HeadingCell.prototype.render = function () {
409 408 var cont = TextCell.prototype.render.apply(this);
410 409 if (cont) {
411 410 var text = this.get_text();
412 411 var math = null;
413 412 // Markdown headings must be a single line
414 413 text = text.replace(/\n/g, ' ');
415 414 if (text === "") { text = this.placeholder; }
416 415 text = new Array(this.level + 1).join("#") + " " + text;
417 416 var text_and_math = mathjaxutils.remove_math(text);
418 417 text = text_and_math[0];
419 418 math = text_and_math[1];
420 419 var html = marked.parser(marked.lexer(text));
421 420 html = mathjaxutils.replace_math(html, math);
422 421 html = security.sanitize_html(html);
423 422 var h = $($.parseHTML(html));
424 423 // add id and linkback anchor
425 424 var hash = h.text().replace(/ /g, '-');
426 425 h.attr('id', hash);
427 426 h.append(
428 427 $('<a/>')
429 428 .addClass('anchor-link')
430 429 .attr('href', '#' + hash)
431 430 .text('ΒΆ')
432 431 );
433 432 this.set_rendered(h);
434 433 this.element.find('div.input_area').hide();
435 434 this.element.find("div.text_cell_render").show();
436 435 this.typeset();
437 436 }
438 437 return cont;
439 438 };
440 439
441 440 // Backwards compatability.
442 441 IPython.TextCell = TextCell;
443 442 IPython.MarkdownCell = MarkdownCell;
444 443 IPython.RawCell = RawCell;
445 444 IPython.HeadingCell = HeadingCell;
446 445
447 446 var Cells = {
448 447 'TextCell': TextCell,
449 448 'MarkdownCell': MarkdownCell,
450 449 'RawCell': RawCell,
451 450 'HeadingCell': HeadingCell,
452 451 };
453 452 return Cells;
454 453 });
@@ -1,116 +1,116 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'services/kernels/js/kernel',
9 9 ], function(IPython, $, utils, kernel) {
10 10 "use strict";
11 11
12 var Session = function(notebook, options){
12 var Session = function(options){
13 13 this.kernel = null;
14 14 this.id = null;
15 this.notebook = notebook;
15 this.notebook = options.notebook;
16 16 this.name = notebook.notebook_name;
17 17 this.path = notebook.notebook_path;
18 18 this.base_url = notebook.base_url;
19 19 };
20 20
21 21 Session.prototype.start = function(callback) {
22 22 var that = this;
23 23 var model = {
24 24 notebook : {
25 25 name : this.name,
26 26 path : this.path
27 27 }
28 28 };
29 29 var settings = {
30 30 processData : false,
31 31 cache : false,
32 32 type : "POST",
33 33 data: JSON.stringify(model),
34 34 dataType : "json",
35 35 success : function (data, status, xhr) {
36 36 that._handle_start_success(data);
37 37 if (callback) {
38 38 callback(data, status, xhr);
39 39 }
40 40 },
41 41 error : utils.log_ajax_error,
42 42 };
43 43 var url = utils.url_join_encode(this.base_url, 'api/sessions');
44 44 $.ajax(url, settings);
45 45 };
46 46
47 47 Session.prototype.rename_notebook = function (name, path) {
48 48 this.name = name;
49 49 this.path = path;
50 50 var model = {
51 51 notebook : {
52 52 name : this.name,
53 53 path : this.path
54 54 }
55 55 };
56 56 var settings = {
57 57 processData : false,
58 58 cache : false,
59 59 type : "PATCH",
60 60 data: JSON.stringify(model),
61 61 dataType : "json",
62 62 error : utils.log_ajax_error,
63 63 };
64 64 var url = utils.url_join_encode(this.base_url, 'api/sessions', this.id);
65 65 $.ajax(url, settings);
66 66 };
67 67
68 68 Session.prototype.delete = function() {
69 69 var settings = {
70 70 processData : false,
71 71 cache : false,
72 72 type : "DELETE",
73 73 dataType : "json",
74 74 error : utils.log_ajax_error,
75 75 };
76 76 this.kernel.running = false;
77 77 var url = utils.url_join_encode(this.base_url, 'api/sessions', this.id);
78 78 $.ajax(url, settings);
79 79 };
80 80
81 81 // Kernel related things
82 82 /**
83 83 * Create the Kernel object associated with this Session.
84 84 *
85 85 * @method _handle_start_success
86 86 */
87 87 Session.prototype._handle_start_success = function (data, status, xhr) {
88 88 this.id = data.id;
89 89 var kernel_service_url = utils.url_path_join(this.base_url, "api/kernels");
90 90 this.kernel = new kernel.Kernel(kernel_service_url, this.notebook);
91 91 this.kernel._kernel_started(data.kernel);
92 92 };
93 93
94 94 /**
95 95 * Prompt the user to restart the IPython kernel.
96 96 *
97 97 * @method restart_kernel
98 98 */
99 99 Session.prototype.restart_kernel = function () {
100 100 this.kernel.restart();
101 101 };
102 102
103 103 Session.prototype.interrupt_kernel = function() {
104 104 this.kernel.interrupt();
105 105 };
106 106
107 107
108 108 Session.prototype.kill_kernel = function() {
109 109 this.kernel.kill();
110 110 };
111 111
112 112 // For backwards compatability.
113 113 IPython.Session = Session;
114 114
115 115 return {'Session': Session};
116 116 });
@@ -1,34 +1,36 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'tree/js/notebooklist',
8 8 ], function(IPython, $, notebooklist) {
9 9 "use strict";
10 10
11 var KernelList = function (selector, options, session_list) {
12 notebooklist.NotebookList.call(this, selector, options, 'running', session_list);
11 var KernelList = function (selector, options) {
12 notebooklist.NotebookList.call(this, selector, $.extend({
13 element_name: 'running'},
14 options));
13 15 };
14 16
15 17 KernelList.prototype = Object.create(notebooklist.NotebookList.prototype);
16 18
17 19 KernelList.prototype.sessions_loaded = function (d) {
18 20 this.sessions = d;
19 21 this.clear_list();
20 22 var item;
21 23 for (var path in d) {
22 24 item = this.new_notebook_item(-1);
23 25 this.add_link('', path, item);
24 26 this.add_shutdown_button(item, this.sessions[path]);
25 27 }
26 28
27 29 $('#running_list_header').toggle($.isEmptyObject(d));
28 30 };
29 31
30 32 // Backwards compatability.
31 33 IPython.KernelList = KernelList;
32 34
33 35 return {'KernelList': KernelList};
34 36 });
@@ -1,114 +1,119 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 var ipython = ipython || {};
5 4 require([
6 5 'base/js/namespace',
7 6 'jquery',
8 7 'base/js/events',
9 8 'base/js/page',
10 9 'base/js/utils',
11 10 'tree/js/notebooklist',
12 11 'tree/js/clusterlist',
13 12 'tree/js/sessionlist',
14 13 'tree/js/kernellist',
15 14 'auth/js/loginwidget',
16 15 'components/jquery-ui/ui/minified/jquery-ui.min',
17 16 'components/bootstrap/js/bootstrap.min',
18 17 ], function(
19 18 IPython,
20 19 $,
21 20 events,
22 21 page,
23 22 utils,
24 23 notebooklist,
25 24 clusterlist,
26 25 sesssionlist,
27 26 kernellist,
28 27 loginwidget){
29 28
30 29 page = new page.Page();
31 30
32 var opts = {
31 var common_options = {
33 32 base_url: utils.get_body_data("baseUrl"),
34 33 notebook_path: utils.get_body_data("notebookPath"),
35 34 };
36 35 events = $([new events.Events()]);
37 session_list = new sesssionlist.SesssionList(opts, events);
38 notebook_list = new notebooklist.NotebookList('#notebook_list', opts, undefined, session_list);
39 cluster_list = new clusterlist.ClusterList('#cluster_list', opts);
40 kernel_list = new kernellist.KernelList('#running_list', opts, session_list);
41 login_widget = new loginwidget.LoginWidget('#login_widget', opts);
36 session_list = new sesssionlist.SesssionList($.extend({
37 events: events},
38 common_options));
39 notebook_list = new notebooklist.NotebookList('#notebook_list', $.extend({
40 session_list: session_list},
41 common_options));
42 cluster_list = new clusterlist.ClusterList('#cluster_list', common_options);
43 kernel_list = new kernellist.KernelList('#running_list', $.extend({
44 session_list: session_list},
45 common_options));
46 login_widget = new loginwidget.LoginWidget('#login_widget', common_options);
42 47
43 48 $('#new_notebook').button().click(function (e) {
44 49 notebook_list.new_notebook();
45 50 });
46 51
47 52 var interval_id=0;
48 53 // auto refresh every xx secondes, no need to be fast,
49 54 // update is done at least when page get focus
50 55 var time_refresh = 60; // in sec
51 56
52 57 var enable_autorefresh = function(){
53 58 //refresh immediately , then start interval
54 59 if($('.upload_button').length === 0)
55 60 {
56 61 session_list.load_sessions();
57 62 cluster_list.load_list();
58 63 }
59 64 if (!interval_id){
60 65 interval_id = setInterval(function(){
61 66 if($('.upload_button').length === 0)
62 67 {
63 68 session_list.load_sessions();
64 69 cluster_list.load_list();
65 70 }
66 71 }, time_refresh*1000);
67 72 }
68 73 };
69 74
70 75 var disable_autorefresh = function(){
71 76 clearInterval(interval_id);
72 77 interval_id = 0;
73 78 };
74 79
75 80 // stop autorefresh when page lose focus
76 81 $(window).blur(function() {
77 82 disable_autorefresh();
78 83 });
79 84
80 85 //re-enable when page get focus back
81 86 $(window).focus(function() {
82 87 enable_autorefresh();
83 88 });
84 89
85 90 // finally start it, it will refresh immediately
86 91 enable_autorefresh();
87 92
88 93 page.show();
89 94 events.trigger('app_initialized.DashboardApp');
90 95
91 96 // bound the upload method to the on change of the file select list
92 97 $("#alternate_upload").change(function (event){
93 98 notebook_list.handleFilesUpload(event,'form');
94 99 });
95 100
96 101 // set hash on tab click
97 102 $("#tabs").find("a").click(function() {
98 103 window.location.hash = $(this).attr("href");
99 104 });
100 105
101 106 // load tab if url hash
102 107 if (window.location.hash) {
103 108 $("#tabs").find("a[href=" + window.location.hash + "]").click();
104 109 }
105 110
106 111 // For backwards compatability.
107 ipython.page = page;
108 ipython.notebook_list = notebook_list;
109 ipython.cluster_list = cluster_list;
110 ipython.session_list = session_list;
111 ipython.kernel_list = kernel_list;
112 ipython.login_widget = login_widget;
113 ipython.events = events;
112 IPython.page = page;
113 IPython.notebook_list = notebook_list;
114 IPython.cluster_list = cluster_list;
115 IPython.session_list = session_list;
116 IPython.kernel_list = kernel_list;
117 IPython.login_widget = login_widget;
118 IPython.events = events;
114 119 });
@@ -1,438 +1,438 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 ], function(IPython, $, utils, dialog) {
10 10 "use strict";
11 11
12 var NotebookList = function (selector, options, element_name, session_list) {
12 var NotebookList = function (selector, options) {
13 13 var that = this;
14 this.session_list = session_list;
14 this.session_list = options.session_list;
15 15 // allow code re-use by just changing element_name in kernellist.js
16 this.element_name = element_name || 'notebook';
16 this.element_name = options.element_name || 'notebook';
17 17 this.selector = selector;
18 18 if (this.selector !== undefined) {
19 19 this.element = $(selector);
20 20 this.style();
21 21 this.bind_events();
22 22 }
23 23 this.notebooks_list = [];
24 24 this.sessions = {};
25 25 this.base_url = options.base_url || utils.get_body_data("baseUrl");
26 26 this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
27 27 if (this.session_list && this.session_list.events) {
28 28 this.session_list.events.on('sessions_loaded.Dashboard',
29 29 function(e, d) { that.sessions_loaded(d); });
30 30 }
31 31 };
32 32
33 33 NotebookList.prototype.style = function () {
34 34 var prefix = '#' + this.element_name;
35 35 $(prefix + '_toolbar').addClass('list_toolbar');
36 36 $(prefix + '_list_info').addClass('toolbar_info');
37 37 $(prefix + '_buttons').addClass('toolbar_buttons');
38 38 $(prefix + '_list_header').addClass('list_header');
39 39 this.element.addClass("list_container");
40 40 };
41 41
42 42
43 43 NotebookList.prototype.bind_events = function () {
44 44 var that = this;
45 45 $('#refresh_' + this.element_name + '_list').click(function () {
46 46 that.load_sessions();
47 47 });
48 48 this.element.bind('dragover', function () {
49 49 return false;
50 50 });
51 51 this.element.bind('drop', function(event){
52 52 that.handleFilesUpload(event,'drop');
53 53 return false;
54 54 });
55 55 };
56 56
57 57 NotebookList.prototype.handleFilesUpload = function(event, dropOrForm) {
58 58 var that = this;
59 59 var files;
60 60 if(dropOrForm =='drop'){
61 61 files = event.originalEvent.dataTransfer.files;
62 62 } else
63 63 {
64 64 files = event.originalEvent.target.files;
65 65 }
66 66 for (var i = 0; i < files.length; i++) {
67 67 var f = files[i];
68 68 var reader = new FileReader();
69 69 reader.readAsText(f);
70 70 var name_and_ext = utils.splitext(f.name);
71 71 var file_ext = name_and_ext[1];
72 72 if (file_ext === '.ipynb') {
73 73 var item = that.new_notebook_item(0);
74 74 item.addClass('new-file');
75 75 that.add_name_input(f.name, item);
76 76 // Store the notebook item in the reader so we can use it later
77 77 // to know which item it belongs to.
78 78 $(reader).data('item', item);
79 79 reader.onload = function (event) {
80 80 var nbitem = $(event.target).data('item');
81 81 that.add_notebook_data(event.target.result, nbitem);
82 82 that.add_upload_button(nbitem);
83 83 };
84 84 } else {
85 85 var dialog_body = 'Uploaded notebooks must be .ipynb files';
86 86 dialog.modal({
87 87 title : 'Invalid file type',
88 88 body : dialog_body,
89 89 buttons : {'OK' : {'class' : 'btn-primary'}}
90 90 });
91 91 }
92 92 }
93 93 // Replace the file input form wth a clone of itself. This is required to
94 94 // reset the form. Otherwise, if you upload a file, delete it and try to
95 95 // upload it again, the changed event won't fire.
96 96 var form = $('input.fileinput');
97 97 form.replaceWith(form.clone(true));
98 98 return false;
99 99 };
100 100
101 101 NotebookList.prototype.clear_list = function (remove_uploads) {
102 102 // Clears the navigation tree.
103 103 //
104 104 // Parameters
105 105 // remove_uploads: bool=False
106 106 // Should upload prompts also be removed from the tree.
107 107 if (remove_uploads) {
108 108 this.element.children('.list_item').remove();
109 109 } else {
110 110 this.element.children('.list_item:not(.new-file)').remove();
111 111 }
112 112 };
113 113
114 114 NotebookList.prototype.load_sessions = function(){
115 115 this.session_list.load_sessions();
116 116 };
117 117
118 118
119 119 NotebookList.prototype.sessions_loaded = function(data){
120 120 this.sessions = data;
121 121 this.load_list();
122 122 };
123 123
124 124 NotebookList.prototype.load_list = function () {
125 125 var that = this;
126 126 var settings = {
127 127 processData : false,
128 128 cache : false,
129 129 type : "GET",
130 130 dataType : "json",
131 131 success : $.proxy(this.list_loaded, this),
132 132 error : $.proxy( function(xhr, status, error){
133 133 utils.log_ajax_error(xhr, status, error);
134 134 that.list_loaded([], null, null, {msg:"Error connecting to server."});
135 135 },this)
136 136 };
137 137
138 138 var url = utils.url_join_encode(
139 139 this.base_url,
140 140 'api',
141 141 'notebooks',
142 142 this.notebook_path
143 143 );
144 144 $.ajax(url, settings);
145 145 };
146 146
147 147
148 148 NotebookList.prototype.list_loaded = function (data, status, xhr, param) {
149 149 var message = 'Notebook list empty.';
150 150 if (param !== undefined && param.msg) {
151 151 message = param.msg;
152 152 }
153 153 var item = null;
154 154 var len = data.length;
155 155 this.clear_list();
156 156 if (len === 0) {
157 157 item = this.new_notebook_item(0);
158 158 var span12 = item.children().first();
159 159 span12.empty();
160 160 span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
161 161 }
162 162 var path = this.notebook_path;
163 163 var offset = 0;
164 164 if (path !== '') {
165 165 item = this.new_notebook_item(0);
166 166 this.add_dir(path, '..', item);
167 167 offset = 1;
168 168 }
169 169 for (var i=0; i<len; i++) {
170 170 if (data[i].type === 'directory') {
171 171 var name = data[i].name;
172 172 item = this.new_notebook_item(i+offset);
173 173 this.add_dir(path, name, item);
174 174 } else {
175 175 var name = data[i].name;
176 176 item = this.new_notebook_item(i+offset);
177 177 this.add_link(path, name, item);
178 178 name = utils.url_path_join(path, name);
179 179 if(this.sessions[name] === undefined){
180 180 this.add_delete_button(item);
181 181 } else {
182 182 this.add_shutdown_button(item,this.sessions[name]);
183 183 }
184 184 }
185 185 }
186 186 };
187 187
188 188
189 189 NotebookList.prototype.new_notebook_item = function (index) {
190 190 var item = $('<div/>').addClass("list_item").addClass("row");
191 191 // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
192 192 // item.css('border-top-style','none');
193 193 item.append($("<div/>").addClass("col-md-12").append(
194 194 $('<i/>').addClass('item_icon')
195 195 ).append(
196 196 $("<a/>").addClass("item_link").append(
197 197 $("<span/>").addClass("item_name")
198 198 )
199 199 ).append(
200 200 $('<div/>').addClass("item_buttons btn-group pull-right")
201 201 ));
202 202
203 203 if (index === -1) {
204 204 this.element.append(item);
205 205 } else {
206 206 this.element.children().eq(index).after(item);
207 207 }
208 208 return item;
209 209 };
210 210
211 211
212 212 NotebookList.prototype.add_dir = function (path, name, item) {
213 213 item.data('name', name);
214 214 item.data('path', path);
215 215 item.find(".item_name").text(name);
216 216 item.find(".item_icon").addClass('folder_icon').addClass('icon-fixed-width');
217 217 item.find("a.item_link")
218 218 .attr('href',
219 219 utils.url_join_encode(
220 220 this.base_url,
221 221 "tree",
222 222 path,
223 223 name
224 224 )
225 225 );
226 226 };
227 227
228 228
229 229 NotebookList.prototype.add_link = function (path, nbname, item) {
230 230 item.data('nbname', nbname);
231 231 item.data('path', path);
232 232 item.find(".item_name").text(nbname);
233 233 item.find(".item_icon").addClass('notebook_icon').addClass('icon-fixed-width');
234 234 item.find("a.item_link")
235 235 .attr('href',
236 236 utils.url_join_encode(
237 237 this.base_url,
238 238 "notebooks",
239 239 path,
240 240 nbname
241 241 )
242 242 ).attr('target','_blank');
243 243 };
244 244
245 245
246 246 NotebookList.prototype.add_name_input = function (nbname, item) {
247 247 item.data('nbname', nbname);
248 248 item.find(".item_icon").addClass('notebook_icon').addClass('icon-fixed-width');
249 249 item.find(".item_name").empty().append(
250 250 $('<input/>')
251 251 .addClass("nbname_input")
252 252 .attr('value', utils.splitext(nbname)[0])
253 253 .attr('size', '30')
254 254 .attr('type', 'text')
255 255 );
256 256 };
257 257
258 258
259 259 NotebookList.prototype.add_notebook_data = function (data, item) {
260 260 item.data('nbdata', data);
261 261 };
262 262
263 263
264 264 NotebookList.prototype.add_shutdown_button = function (item, session) {
265 265 var that = this;
266 266 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-xs btn-danger").
267 267 click(function (e) {
268 268 var settings = {
269 269 processData : false,
270 270 cache : false,
271 271 type : "DELETE",
272 272 dataType : "json",
273 273 success : function () {
274 274 that.load_sessions();
275 275 },
276 276 error : utils.log_ajax_error,
277 277 };
278 278 var url = utils.url_join_encode(
279 279 that.base_url,
280 280 'api/sessions',
281 281 session
282 282 );
283 283 $.ajax(url, settings);
284 284 return false;
285 285 });
286 286 // var new_buttons = item.find('a'); // shutdown_button;
287 287 item.find(".item_buttons").text("").append(shutdown_button);
288 288 };
289 289
290 290 NotebookList.prototype.add_delete_button = function (item) {
291 291 var new_buttons = $('<span/>').addClass("btn-group pull-right");
292 292 var notebooklist = this;
293 293 var delete_button = $("<button/>").text("Delete").addClass("btn btn-default btn-xs").
294 294 click(function (e) {
295 295 // $(this) is the button that was clicked.
296 296 var that = $(this);
297 297 // We use the nbname and notebook_id from the parent notebook_item element's
298 298 // data because the outer scopes values change as we iterate through the loop.
299 299 var parent_item = that.parents('div.list_item');
300 300 var nbname = parent_item.data('nbname');
301 301 var message = 'Are you sure you want to permanently delete the notebook: ' + nbname + '?';
302 302 dialog.modal({
303 303 title : "Delete notebook",
304 304 body : message,
305 305 buttons : {
306 306 Delete : {
307 307 class: "btn-danger",
308 308 click: function() {
309 309 var settings = {
310 310 processData : false,
311 311 cache : false,
312 312 type : "DELETE",
313 313 dataType : "json",
314 314 success : function (data, status, xhr) {
315 315 parent_item.remove();
316 316 },
317 317 error : utils.log_ajax_error,
318 318 };
319 319 var url = utils.url_join_encode(
320 320 notebooklist.base_url,
321 321 'api/notebooks',
322 322 notebooklist.notebook_path,
323 323 nbname
324 324 );
325 325 $.ajax(url, settings);
326 326 }
327 327 },
328 328 Cancel : {}
329 329 }
330 330 });
331 331 return false;
332 332 });
333 333 item.find(".item_buttons").text("").append(delete_button);
334 334 };
335 335
336 336
337 337 NotebookList.prototype.add_upload_button = function (item) {
338 338 var that = this;
339 339 var upload_button = $('<button/>').text("Upload")
340 340 .addClass('btn btn-primary btn-xs upload_button')
341 341 .click(function (e) {
342 342 var nbname = item.find('.item_name > input').val();
343 343 if (nbname.slice(nbname.length-6, nbname.length) != ".ipynb") {
344 344 nbname = nbname + ".ipynb";
345 345 }
346 346 var path = that.notebook_path;
347 347 var nbdata = item.data('nbdata');
348 348 var content_type = 'application/json';
349 349 var model = {
350 350 content : JSON.parse(nbdata),
351 351 };
352 352 var settings = {
353 353 processData : false,
354 354 cache : false,
355 355 type : 'PUT',
356 356 dataType : 'json',
357 357 data : JSON.stringify(model),
358 358 headers : {'Content-Type': content_type},
359 359 success : function (data, status, xhr) {
360 360 that.add_link(path, nbname, item);
361 361 that.add_delete_button(item);
362 362 },
363 363 error : utils.log_ajax_error,
364 364 };
365 365
366 366 var url = utils.url_join_encode(
367 367 that.base_url,
368 368 'api/notebooks',
369 369 that.notebook_path,
370 370 nbname
371 371 );
372 372 $.ajax(url, settings);
373 373 return false;
374 374 });
375 375 var cancel_button = $('<button/>').text("Cancel")
376 376 .addClass("btn btn-default btn-xs")
377 377 .click(function (e) {
378 378 console.log('cancel click');
379 379 item.remove();
380 380 return false;
381 381 });
382 382 item.find(".item_buttons").empty()
383 383 .append(upload_button)
384 384 .append(cancel_button);
385 385 };
386 386
387 387
388 388 NotebookList.prototype.new_notebook = function(){
389 389 var path = this.notebook_path;
390 390 var base_url = this.base_url;
391 391 var settings = {
392 392 processData : false,
393 393 cache : false,
394 394 type : "POST",
395 395 dataType : "json",
396 396 async : false,
397 397 success : function (data, status, xhr) {
398 398 var notebook_name = data.name;
399 399 window.open(
400 400 utils.url_join_encode(
401 401 base_url,
402 402 'notebooks',
403 403 path,
404 404 notebook_name),
405 405 '_blank'
406 406 );
407 407 },
408 408 error : $.proxy(this.new_notebook_failed, this),
409 409 };
410 410 var url = utils.url_join_encode(
411 411 base_url,
412 412 'api/notebooks',
413 413 path
414 414 );
415 415 $.ajax(url, settings);
416 416 };
417 417
418 418
419 419 NotebookList.prototype.new_notebook_failed = function (xhr, status, error) {
420 420 utils.log_ajax_error(xhr, status, error);
421 421 var msg;
422 422 if (xhr.responseJSON && xhr.responseJSON.message) {
423 423 msg = xhr.responseJSON.message;
424 424 } else {
425 425 msg = xhr.statusText;
426 426 }
427 427 dialog.modal({
428 428 title : 'Creating Notebook Failed',
429 429 body : "The error was: " + msg,
430 430 buttons : {'OK' : {'class' : 'btn-primary'}}
431 431 });
432 432 };
433 433
434 434 // Backwards compatability.
435 435 IPython.NotebookList = NotebookList;
436 436
437 437 return {'NotebookList': NotebookList};
438 438 });
@@ -1,49 +1,49 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 ], function(IPython, $, utils) {
9 9 "use strict";
10 10
11 11 var SesssionList = function (options, events) {
12 this.events = events;
12 this.events = options.events;
13 13 this.sessions = {};
14 14 this.base_url = options.base_url || utils.get_body_data("baseUrl");
15 15 };
16 16
17 17 SesssionList.prototype.load_sessions = function(){
18 18 var that = this;
19 19 var settings = {
20 20 processData : false,
21 21 cache : false,
22 22 type : "GET",
23 23 dataType : "json",
24 24 success : $.proxy(that.sessions_loaded, this),
25 25 error : utils.log_ajax_error,
26 26 };
27 27 var url = utils.url_join_encode(this.base_url, 'api/sessions');
28 28 $.ajax(url, settings);
29 29 };
30 30
31 31 SesssionList.prototype.sessions_loaded = function(data){
32 32 this.sessions = {};
33 33 var len = data.length;
34 34 var nb_path;
35 35 for (var i=0; i<len; i++) {
36 36 nb_path = utils.url_path_join(
37 37 data[i].notebook.path,
38 38 data[i].notebook.name
39 39 );
40 40 this.sessions[nb_path] = data[i].id;
41 41 }
42 42 this.events.trigger('sessions_loaded.Dashboard', this.sessions);
43 43 };
44 44
45 45 // Backwards compatability.
46 46 IPython.SesssionList = SesssionList;
47 47
48 48 return {'SesssionList': SesssionList};
49 49 });
General Comments 0
You need to be logged in to leave comments. Login now