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