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