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