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