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