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