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