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