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