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