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