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