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