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