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