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