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