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