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