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