##// END OF EJS Templates
Adding new logic to cells.
Brian E. Granger -
Show More
@@ -1,390 +1,468
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Cell
10 10 //============================================================================
11 11 /**
12 12 * An extendable module that provide base functionnality to create cell for notebook.
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule Cell
16 16 */
17 17
18 18 var IPython = (function (IPython) {
19 19 "use strict";
20 20
21 21 var utils = IPython.utils;
22 22
23 23 /**
24 24 * The Base `Cell` class from which to inherit
25 25 * @class Cell
26 26 **/
27 27
28 28 /*
29 29 * @constructor
30 30 *
31 31 * @param {object|undefined} [options]
32 32 * @param [options.cm_config] {object} config to pass to CodeMirror, will extend default parameters
33 33 */
34 34 var Cell = function (options) {
35 35
36 36 options = this.mergeopt(Cell, options);
37 37 // superclass default overwrite our default
38 38
39 39 this.placeholder = options.placeholder || '';
40 40 this.read_only = options.cm_config.readOnly;
41 41 this.selected = false;
42 this.focused = false;
42 this.rendered = false;
43 this.mode = 'command';
43 44 this.metadata = {};
44 45 // load this from metadata later ?
45 46 this.user_highlight = 'auto';
46 47 this.cm_config = options.cm_config;
47 48 this.cell_id = utils.uuid();
48 49 this._options = options;
49 50
50 51 // For JS VM engines optimisation, attributes should be all set (even
51 52 // to null) in the constructor, and if possible, if different subclass
52 53 // have new attributes with same name, they should be created in the
53 54 // same order. Easiest is to create and set to null in parent class.
54 55
55 56 this.element = null;
56 57 this.cell_type = this.cell_type || null;
57 58 this.code_mirror = null;
58 59
59 60
60 61 this.create_element();
61 62 if (this.element !== null) {
62 63 this.element.data("cell", this);
63 64 this.bind_events();
64 65 }
65 66 };
66 67
67 68 Cell.options_default = {
68 69 cm_config : {
69 70 indentUnit : 4,
70 71 readOnly: false,
71 72 theme: "default"
72 73 }
73 74 };
74 75
75 76 // FIXME: Workaround CM Bug #332 (Safari segfault on drag)
76 77 // by disabling drag/drop altogether on Safari
77 78 // https://github.com/marijnh/CodeMirror/issues/332
78 79
79 80 if (utils.browser[0] == "Safari") {
80 81 Cell.options_default.cm_config.dragDrop = false;
81 82 }
82 83
83 84 Cell.prototype.mergeopt = function(_class, options, overwrite){
84 85 options = options || {};
85 86 overwrite = overwrite || {};
86 87 return $.extend(true, {}, _class.options_default, options, overwrite)
87 88
88 89 }
89 90
90 91
91 92
92 93 /**
93 94 * Empty. Subclasses must implement create_element.
94 95 * This should contain all the code to create the DOM element in notebook
95 96 * and will be called by Base Class constructor.
96 97 * @method create_element
97 98 */
98 99 Cell.prototype.create_element = function () {
99 100 };
100 101
101 102
102 103 /**
103 104 * Subclasses can implement override bind_events.
104 105 * Be carefull to call the parent method when overwriting as it fires event.
105 106 * this will be triggerd after create_element in constructor.
106 107 * @method bind_events
107 108 */
108 109 Cell.prototype.bind_events = function () {
109 110 var that = this;
110 111 // We trigger events so that Cell doesn't have to depend on Notebook.
111 112 that.element.click(function (event) {
112 113 if (that.selected === false) {
113 114 $([IPython.events]).trigger('select.Cell', {'cell':that});
114 115 }
115 116 });
116 117 that.element.focusin(function (event) {
117 118 if (that.selected === false) {
118 119 $([IPython.events]).trigger('select.Cell', {'cell':that});
119 120 }
120 121 });
121 122 if (this.code_mirror) {
122 123 this.code_mirror.on("change", function(cm, change) {
123 124 $([IPython.events]).trigger("set_dirty.Notebook", {value: true});
124 125 });
125 126 }
126 127 };
127 128
128 129 /**
129 130 * Triger typsetting of math by mathjax on current cell element
130 131 * @method typeset
131 132 */
132 133 Cell.prototype.typeset = function () {
133 134 if (window.MathJax){
134 135 var cell_math = this.element.get(0);
135 136 MathJax.Hub.Queue(["Typeset", MathJax.Hub, cell_math]);
136 }
137 };
137 138 };
138 139
139 140 /**
140 141 * handle cell level logic when a cell is selected
141 142 * @method select
143 * @return is the action being taken
142 144 */
143 145 Cell.prototype.select = function () {
146 if (!this.selected) {
144 147 this.element.addClass('selected');
148 this.element.removeClass('unselected');
145 149 this.selected = true;
150 return true;
151 } else {
152 return false;
153 };
146 154 };
147 155
148 156 /**
149 157 * handle cell level logic when a cell is unselected
150 158 * @method unselect
159 * @return is the action being taken
151 160 */
152 161 Cell.prototype.unselect = function () {
162 if (this.selected) {
163 this.element.addClass('unselected');
153 164 this.element.removeClass('selected');
154 165 this.selected = false;
166 return true;
167 } else {
168 return false;
169 };
155 170 };
156 171
157 172 /**
158 * handle cell level logic when a cell is focused
159 * @method focus
173 * handle cell level logic when a cell is rendered
174 * @method render
175 * @return is the action being taken
160 176 */
161 Cell.prototype.focus = function () {
162 this.element.addClass('focused');
163 this.focused = true;
177 Cell.prototype.render = function () {
178 if (!this.rendered) {
179 this.element.addClass('rendered');
180 this.element.removeClass('unrendered');
181 this.rendered = true;
182 return true;
183 } else {
184 return false;
185 };
186 };
187
188 /**
189 * handle cell level logic when a cell is unrendered
190 * @method unrender
191 * @return is the action being taken
192 */
193 Cell.prototype.unrender = function () {
194 if (this.rendered) {
195 this.element.addClass('unrendered');
196 this.element.removeClass('rendered');
197 this.rendered = false;
198 return true;
199 } else {
200 return false;
201 };
164 202 };
165 203
166 204 /**
167 * handle cell level logic when a cell is unfocused
168 * @method unfocus
205 * enter the command mode for the cell
206 * @method command_mode
207 * @return is the action being taken
169 208 */
170 Cell.prototype.unfocus = function () {
171 this.element.removeClass('focused');
172 this.focused = false;
209 Cell.prototype.command_mode = function () {
210 if (this.mode !== 'command') {
211 this.element.addClass('command_mode');
212 this.element.removeClass('edit_mode');
213 this.mode = 'command';
214 return true;
215 } else {
216 return false;
173 217 };
218 };
219
220 /**
221 * enter the edit mode for the cell
222 * @method command_mode
223 * @return is the action being taken
224 */
225 Cell.prototype.edit_mode = function () {
226 if (this.mode !== 'edit') {
227 this.element.addClass('edit_mode');
228 this.element.removeClass('command_mode');
229 this.mode = 'edit';
230 return true;
231 } else {
232 return false;
233 };
234 }
235
236 /**
237 * Focus the cell in the DOM sense
238 * @method focus_cell
239 */
240 Cell.prototype.focus_cell = function () {
241 this.element.focus();
242 }
243
244 /**
245 * Focus the editor area so a user can type
246 * @method focus_editor
247 */
248 Cell.prototype.focus_editor = function () {
249 this.code_mirror.refresh();
250 this.code_mirror.focus();
251 }
174 252
175 253 /**
176 254 * should be overritten by subclass
177 255 * @method get_text
178 256 */
179 257 Cell.prototype.get_text = function () {
180 258 };
181 259
182 260 /**
183 261 * should be overritten by subclass
184 262 * @method set_text
185 263 * @param {string} text
186 264 */
187 265 Cell.prototype.set_text = function (text) {
188 266 };
189 267
190 268 /**
191 269 * Refresh codemirror instance
192 270 * @method refresh
193 271 */
194 272 Cell.prototype.refresh = function () {
195 273 this.code_mirror.refresh();
196 274 };
197 275
198 276
199 277 /**
200 278 * should be overritten by subclass
201 279 * @method edit
202 280 **/
203 281 Cell.prototype.edit = function () {
204 282 };
205 283
206 284
207 285 /**
208 286 * should be overritten by subclass
209 287 * @method render
210 288 **/
211 289 Cell.prototype.render = function () {
212 290 };
213 291
214 292 /**
215 293 * should be overritten by subclass
216 294 * serialise cell to json.
217 295 * @method toJSON
218 296 **/
219 297 Cell.prototype.toJSON = function () {
220 298 var data = {};
221 299 data.metadata = this.metadata;
222 300 data.cell_type = this.cell_type;
223 301 return data;
224 302 };
225 303
226 304
227 305 /**
228 306 * should be overritten by subclass
229 307 * @method fromJSON
230 308 **/
231 309 Cell.prototype.fromJSON = function (data) {
232 310 if (data.metadata !== undefined) {
233 311 this.metadata = data.metadata;
234 312 }
235 313 this.celltoolbar.rebuild();
236 314 };
237 315
238 316
239 317 /**
240 318 * can the cell be split into two cells
241 319 * @method is_splittable
242 320 **/
243 321 Cell.prototype.is_splittable = function () {
244 322 return true;
245 323 };
246 324
247 325
248 326 /**
249 327 * can the cell be merged with other cells
250 328 * @method is_mergeable
251 329 **/
252 330 Cell.prototype.is_mergeable = function () {
253 331 return true;
254 332 };
255 333
256 334
257 335 /**
258 336 * @return {String} - the text before the cursor
259 337 * @method get_pre_cursor
260 338 **/
261 339 Cell.prototype.get_pre_cursor = function () {
262 340 var cursor = this.code_mirror.getCursor();
263 341 var text = this.code_mirror.getRange({line:0, ch:0}, cursor);
264 342 text = text.replace(/^\n+/, '').replace(/\n+$/, '');
265 343 return text;
266 344 }
267 345
268 346
269 347 /**
270 348 * @return {String} - the text after the cursor
271 349 * @method get_post_cursor
272 350 **/
273 351 Cell.prototype.get_post_cursor = function () {
274 352 var cursor = this.code_mirror.getCursor();
275 353 var last_line_num = this.code_mirror.lineCount()-1;
276 354 var last_line_len = this.code_mirror.getLine(last_line_num).length;
277 355 var end = {line:last_line_num, ch:last_line_len}
278 356 var text = this.code_mirror.getRange(cursor, end);
279 357 text = text.replace(/^\n+/, '').replace(/\n+$/, '');
280 358 return text;
281 359 };
282 360
283 361 /**
284 362 * Show/Hide CodeMirror LineNumber
285 363 * @method show_line_numbers
286 364 *
287 365 * @param value {Bool} show (true), or hide (false) the line number in CodeMirror
288 366 **/
289 367 Cell.prototype.show_line_numbers = function (value) {
290 368 this.code_mirror.setOption('lineNumbers', value);
291 369 this.code_mirror.refresh();
292 370 };
293 371
294 372 /**
295 373 * Toggle CodeMirror LineNumber
296 374 * @method toggle_line_numbers
297 375 **/
298 376 Cell.prototype.toggle_line_numbers = function () {
299 377 var val = this.code_mirror.getOption('lineNumbers');
300 378 this.show_line_numbers(!val);
301 379 };
302 380
303 381 /**
304 382 * Force codemirror highlight mode
305 383 * @method force_highlight
306 384 * @param {object} - CodeMirror mode
307 385 **/
308 386 Cell.prototype.force_highlight = function(mode) {
309 387 this.user_highlight = mode;
310 388 this.auto_highlight();
311 389 };
312 390
313 391 /**
314 392 * Try to autodetect cell highlight mode, or use selected mode
315 393 * @methods _auto_highlight
316 394 * @private
317 395 * @param {String|object|undefined} - CodeMirror mode | 'auto'
318 396 **/
319 397 Cell.prototype._auto_highlight = function (modes) {
320 398 //Here we handle manually selected modes
321 399 if( this.user_highlight != undefined && this.user_highlight != 'auto' )
322 400 {
323 401 var mode = this.user_highlight;
324 402 CodeMirror.autoLoadMode(this.code_mirror, mode);
325 403 this.code_mirror.setOption('mode', mode);
326 404 return;
327 405 }
328 406 var current_mode = this.code_mirror.getOption('mode', mode);
329 407 var first_line = this.code_mirror.getLine(0);
330 408 // loop on every pairs
331 409 for( var mode in modes) {
332 410 var regs = modes[mode]['reg'];
333 411 // only one key every time but regexp can't be keys...
334 412 for(var reg in regs ) {
335 413 // here we handle non magic_modes
336 414 if(first_line.match(regs[reg]) != null) {
337 415 if(current_mode == mode){
338 416 return;
339 417 }
340 418 if (mode.search('magic_') != 0) {
341 419 this.code_mirror.setOption('mode', mode);
342 420 CodeMirror.autoLoadMode(this.code_mirror, mode);
343 421 return;
344 422 }
345 423 var open = modes[mode]['open']|| "%%";
346 424 var close = modes[mode]['close']|| "%%end";
347 425 var mmode = mode;
348 426 mode = mmode.substr(6);
349 427 if(current_mode == mode){
350 428 return;
351 429 }
352 430 CodeMirror.autoLoadMode(this.code_mirror, mode);
353 431 // create on the fly a mode that swhitch between
354 432 // plain/text and smth else otherwise `%%` is
355 433 // source of some highlight issues.
356 434 // we use patchedGetMode to circumvent a bug in CM
357 435 CodeMirror.defineMode(mmode , function(config) {
358 436 return CodeMirror.multiplexingMode(
359 437 CodeMirror.patchedGetMode(config, 'text/plain'),
360 438 // always set someting on close
361 439 {open: open, close: close,
362 440 mode: CodeMirror.patchedGetMode(config, mode),
363 441 delimStyle: "delimit"
364 442 }
365 443 );
366 444 });
367 445 this.code_mirror.setOption('mode', mmode);
368 446 return;
369 447 }
370 448 }
371 449 }
372 450 // fallback on default
373 451 var default_mode
374 452 try {
375 453 default_mode = this._options.cm_config.mode;
376 454 } catch(e) {
377 455 default_mode = 'text/plain';
378 456 }
379 457 if( current_mode === default_mode){
380 458 return
381 459 }
382 460 this.code_mirror.setOption('mode', default_mode);
383 461 };
384 462
385 463 IPython.Cell = Cell;
386 464
387 465 return IPython;
388 466
389 467 }(IPython));
390 468
@@ -1,468 +1,497
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // CodeCell
10 10 //============================================================================
11 11 /**
12 12 * An extendable module that provide base functionnality to create cell for notebook.
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule CodeCell
16 16 */
17 17
18 18
19 19 /* local util for codemirror */
20 20 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;};
21 21
22 22 /**
23 23 *
24 24 * function to delete until previous non blanking space character
25 25 * or first multiple of 4 tabstop.
26 26 * @private
27 27 */
28 28 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
29 29 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
30 30 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
31 31 var cur = cm.getCursor(), line = cm.getLine(cur.line);
32 32 var tabsize = cm.getOption('tabSize');
33 33 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
34 34 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
35 35 var select = cm.getRange(from,cur);
36 36 if( select.match(/^\ +$/) !== null){
37 37 cm.replaceRange("",from,cur);
38 38 } else {
39 39 cm.deleteH(-1,"char");
40 40 }
41 41 };
42 42
43 43
44 44 var IPython = (function (IPython) {
45 45 "use strict";
46 46
47 47 var utils = IPython.utils;
48 48 var key = IPython.utils.keycodes;
49 49
50 50 /**
51 51 * A Cell conceived to write code.
52 52 *
53 53 * The kernel doesn't have to be set at creation time, in that case
54 54 * it will be null and set_kernel has to be called later.
55 55 * @class CodeCell
56 56 * @extends IPython.Cell
57 57 *
58 58 * @constructor
59 59 * @param {Object|null} kernel
60 60 * @param {object|undefined} [options]
61 61 * @param [options.cm_config] {object} config to pass to CodeMirror
62 62 */
63 63 var CodeCell = function (kernel, options) {
64 64 this.kernel = kernel || null;
65 65 this.collapsed = false;
66 66
67 67 // create all attributed in constructor function
68 68 // even if null for V8 VM optimisation
69 69 this.input_prompt_number = null;
70 70 this.celltoolbar = null;
71 71 this.output_area = null;
72 72 this.last_msg_id = null;
73 73 this.completer = null;
74 74
75 75
76 76 var cm_overwrite_options = {
77 77 onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this)
78 78 };
79 79
80 80 options = this.mergeopt(CodeCell, options, {cm_config:cm_overwrite_options});
81 81
82 82 IPython.Cell.apply(this,[options]);
83 83
84 84 // Attributes we want to override in this subclass.
85 85 this.cell_type = "code";
86 86
87 87 var that = this;
88 88 this.element.focusout(
89 89 function() { that.auto_highlight(); }
90 90 );
91 91 };
92 92
93 93 CodeCell.options_default = {
94 94 cm_config : {
95 95 extraKeys: {
96 96 "Tab" : "indentMore",
97 97 "Shift-Tab" : "indentLess",
98 98 "Backspace" : "delSpaceToPrevTabStop",
99 99 "Cmd-/" : "toggleComment",
100 100 "Ctrl-/" : "toggleComment"
101 101 },
102 102 mode: 'ipython',
103 103 theme: 'ipython',
104 104 matchBrackets: true
105 105 }
106 106 };
107 107
108 108
109 109 CodeCell.prototype = new IPython.Cell();
110 110
111 111 /**
112 112 * @method auto_highlight
113 113 */
114 114 CodeCell.prototype.auto_highlight = function () {
115 115 this._auto_highlight(IPython.config.cell_magic_highlight);
116 116 };
117 117
118 118 /** @method create_element */
119 119 CodeCell.prototype.create_element = function () {
120 120 IPython.Cell.prototype.create_element.apply(this, arguments);
121 121
122 122 var cell = $('<div></div>').addClass('cell border-box-sizing code_cell');
123 123 cell.attr('tabindex','2');
124 124
125 125 var input = $('<div></div>').addClass('input');
126 126 var prompt = $('<div/>').addClass('prompt input_prompt');
127 127 var inner_cell = $('<div/>').addClass('inner_cell');
128 128 this.celltoolbar = new IPython.CellToolbar(this);
129 129 inner_cell.append(this.celltoolbar.element);
130 130 var input_area = $('<div/>').addClass('input_area');
131 131 this.code_mirror = CodeMirror(input_area.get(0), this.cm_config);
132 132 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
133 133 inner_cell.append(input_area);
134 134 input.append(prompt).append(inner_cell);
135 135 var output = $('<div></div>');
136 136 cell.append(input).append(output);
137 137 this.element = cell;
138 138 this.output_area = new IPython.OutputArea(output, true);
139 139 this.completer = new IPython.Completer(this);
140 140 };
141 141
142 142 /**
143 143 * This method gets called in CodeMirror's onKeyDown/onKeyPress
144 144 * handlers and is used to provide custom key handling. Its return
145 145 * value is used to determine if CodeMirror should ignore the event:
146 146 * true = ignore, false = don't ignore.
147 147 * @method handle_codemirror_keyevent
148 148 */
149 149 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
150 150
151 151 var that = this;
152
153 if (this.mode === 'command') {
154 return false
155 } else if (this.mode === 'edit') {
152 156 // whatever key is pressed, first, cancel the tooltip request before
153 157 // they are sent, and remove tooltip if any, except for tab again
154 158 if (event.type === 'keydown' && event.which != key.TAB ) {
155 159 IPython.tooltip.remove_and_cancel_tooltip();
156 }
160 };
157 161
158 162 var cur = editor.getCursor();
159 163 if (event.keyCode === key.ENTER){
160 164 this.auto_highlight();
161 165 }
162 166
163 167 if (event.keyCode === key.ENTER && (event.shiftKey || event.ctrlKey)) {
164 168 // Always ignore shift-enter in CodeMirror as we handle it.
165 169 return true;
170
166 171 } else if (event.which === 40 && event.type === 'keypress' && IPython.tooltip.time_before_tooltip >= 0) {
167 172 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
168 173 // browser and keyboard layout !
169 174 // Pressing '(' , request tooltip, don't forget to reappend it
170 175 // The second argument says to hide the tooltip if the docstring
171 176 // is actually empty
172 177 IPython.tooltip.pending(that, true);
173 178 } else if (event.which === key.UPARROW && event.type === 'keydown') {
174 179 // If we are not at the top, let CM handle the up arrow and
175 180 // prevent the global keydown handler from handling it.
176 181 if (!that.at_top()) {
177 182 event.stop();
178 183 return false;
179 184 } else {
180 185 return true;
181 }
186 };
182 187 } else if (event.which === key.ESC) {
183 return IPython.tooltip.remove_and_cancel_tooltip(true);
188 IPython.tooltip.remove_and_cancel_tooltip(true);
189 return true;
184 190 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
185 191 // If we are not at the bottom, let CM handle the down arrow and
186 192 // prevent the global keydown handler from handling it.
187 193 if (!that.at_bottom()) {
188 194 event.stop();
189 195 return false;
190 196 } else {
191 197 return true;
192 }
198 };
193 199 } else if (event.keyCode === key.TAB && event.type == 'keydown' && event.shiftKey) {
194 200 if (editor.somethingSelected()){
195 201 var anchor = editor.getCursor("anchor");
196 202 var head = editor.getCursor("head");
197 203 if( anchor.line != head.line){
198 204 return false;
199 205 }
200 206 }
207 }
201 208 IPython.tooltip.request(that);
202 209 event.stop();
203 210 return true;
204 211 } else if (event.keyCode === key.TAB && event.type == 'keydown') {
205 212 // Tab completion.
206 213 IPython.tooltip.remove_and_cancel_tooltip();
207 214 if (editor.somethingSelected()) {
208 215 return false;
209 216 }
210 217 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
211 218 if (pre_cursor.trim() === "") {
212 219 // Don't autocomplete if the part of the line before the cursor
213 220 // is empty. In this case, let CodeMirror handle indentation.
214 221 return false;
215 222 } else {
216 event.stop();
217 this.completer.startCompletion();
218 return true;
219 }
220 } else {
221 223 // keypress/keyup also trigger on TAB press, and we don't want to
222 224 // use those to disable tab completion.
223 225 return false;
226 };
227 return false;
224 228 }
225 229 return false;
226 230 };
227 231
228 232
229 233 // Kernel related calls.
230 234
231 235 CodeCell.prototype.set_kernel = function (kernel) {
232 236 this.kernel = kernel;
233 237 };
234 238
235 239 /**
236 240 * Execute current code cell to the kernel
237 241 * @method execute
238 242 */
239 243 CodeCell.prototype.execute = function () {
240 244 this.output_area.clear_output();
241 245 this.set_input_prompt('*');
242 246 this.element.addClass("running");
243 247 if (this.last_msg_id) {
244 248 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
245 249 }
246 250 var callbacks = this.get_callbacks();
247 251
248 252 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true});
249 253 };
250 254
251 255 /**
252 256 * Construct the default callbacks for
253 257 * @method get_callbacks
254 258 */
255 259 CodeCell.prototype.get_callbacks = function () {
256 260 return {
257 261 shell : {
258 262 reply : $.proxy(this._handle_execute_reply, this),
259 263 payload : {
260 264 set_next_input : $.proxy(this._handle_set_next_input, this),
261 265 page : $.proxy(this._open_with_pager, this)
262 266 }
263 267 },
264 268 iopub : {
265 269 output : $.proxy(this.output_area.handle_output, this.output_area),
266 270 clear_output : $.proxy(this.output_area.handle_clear_output, this.output_area),
267 271 },
268 272 input : $.proxy(this._handle_input_request, this)
269 273 };
270 274 };
271 275
272 276 CodeCell.prototype._open_with_pager = function (payload) {
273 277 $([IPython.events]).trigger('open_with_text.Pager', payload);
274 278 };
275 279
276 280 /**
277 281 * @method _handle_execute_reply
278 282 * @private
279 283 */
280 284 CodeCell.prototype._handle_execute_reply = function (msg) {
281 285 this.set_input_prompt(msg.content.execution_count);
282 286 this.element.removeClass("running");
283 287 $([IPython.events]).trigger('set_dirty.Notebook', {value: true});
284 288 };
285 289
286 290 /**
287 291 * @method _handle_set_next_input
288 292 * @private
289 293 */
290 294 CodeCell.prototype._handle_set_next_input = function (payload) {
291 295 var data = {'cell': this, 'text': payload.text};
292 296 $([IPython.events]).trigger('set_next_input.Notebook', data);
293 297 };
294 298
295 299 /**
296 300 * @method _handle_input_request
297 301 * @private
298 302 */
299 303 CodeCell.prototype._handle_input_request = function (msg) {
300 304 this.output_area.append_raw_input(msg);
301 305 };
302 306
303 307
304 308 // Basic cell manipulation.
305 309
306 310 CodeCell.prototype.select = function () {
307 IPython.Cell.prototype.select.apply(this);
311 var continue = IPython.Cell.prototype.select.apply(this);
312 if (continue) {
308 313 this.code_mirror.refresh();
309 314 this.auto_highlight();
310 315 };
316 return continue;
317 };
318
319 CodeCell.prototype.render = function () {
320 var continue = IPython.Cell.prototype.render.apply(this);
321 if (continue) {
322 this.execute();
323 };
324 return continue;
325 };
326
327 CodeCell.prototype.unrender = function () {
328 var continue = IPython.Cell.prototype.unrender.apply(this);
329 if (continue) {
330 this.clear_output(true, true, true);
331 };
332 return continue;
333 };
311 334
312 CodeCell.prototype.focus = function () {
313 IPython.Cell.prototype.focus.apply(this);
314 this.code_mirror.focus();
335 CodeCell.prototype.command_mode = function () {
336 var continue = IPython.Cell.prototype.command_mode.apply(this);
337 if (continue) {
338 this.focus_cell();
315 339 };
340 return continue;
341 }
316 342
317 CodeCell.prototype.unfocus = function () {
318 IPython.Cell.prototype.focus.apply(this);
319 this.code_mirror.blur();
343 CodeCell.prototype.edit_mode = function () {
344 var continue = IPython.Cell.prototype.edit_mode.apply(this);
345 if (continue) {
346 this.focus_editor();
320 347 };
348 return continue;
349 }
321 350
322 351 CodeCell.prototype.select_all = function () {
323 352 var start = {line: 0, ch: 0};
324 353 var nlines = this.code_mirror.lineCount();
325 354 var last_line = this.code_mirror.getLine(nlines-1);
326 355 var end = {line: nlines-1, ch: last_line.length};
327 356 this.code_mirror.setSelection(start, end);
328 357 };
329 358
330 359
331 360 CodeCell.prototype.collapse = function () {
332 361 this.collapsed = true;
333 362 this.output_area.collapse();
334 363 };
335 364
336 365
337 366 CodeCell.prototype.expand = function () {
338 367 this.collapsed = false;
339 368 this.output_area.expand();
340 369 };
341 370
342 371
343 372 CodeCell.prototype.toggle_output = function () {
344 373 this.collapsed = Boolean(1 - this.collapsed);
345 374 this.output_area.toggle_output();
346 375 };
347 376
348 377
349 378 CodeCell.prototype.toggle_output_scroll = function () {
350 379 this.output_area.toggle_scroll();
351 380 };
352 381
353 382
354 383 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
355 384 var ns = prompt_value || "&nbsp;";
356 385 return 'In&nbsp;[' + ns + ']:';
357 386 };
358 387
359 388 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
360 389 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
361 390 for(var i=1; i < lines_number; i++) {
362 391 html.push(['...:']);
363 392 }
364 393 return html.join('<br/>');
365 394 };
366 395
367 396 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
368 397
369 398
370 399 CodeCell.prototype.set_input_prompt = function (number) {
371 400 var nline = 1;
372 401 if (this.code_mirror !== undefined) {
373 402 nline = this.code_mirror.lineCount();
374 403 }
375 404 this.input_prompt_number = number;
376 405 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
377 406 this.element.find('div.input_prompt').html(prompt_html);
378 407 };
379 408
380 409
381 410 CodeCell.prototype.clear_input = function () {
382 411 this.code_mirror.setValue('');
383 412 };
384 413
385 414
386 415 CodeCell.prototype.get_text = function () {
387 416 return this.code_mirror.getValue();
388 417 };
389 418
390 419
391 420 CodeCell.prototype.set_text = function (code) {
392 421 return this.code_mirror.setValue(code);
393 422 };
394 423
395 424
396 425 CodeCell.prototype.at_top = function () {
397 426 var cursor = this.code_mirror.getCursor();
398 427 if (cursor.line === 0 && cursor.ch === 0) {
399 428 return true;
400 429 } else {
401 430 return false;
402 431 }
403 432 };
404 433
405 434
406 435 CodeCell.prototype.at_bottom = function () {
407 436 var cursor = this.code_mirror.getCursor();
408 437 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
409 438 return true;
410 439 } else {
411 440 return false;
412 441 }
413 442 };
414 443
415 444
416 445 CodeCell.prototype.clear_output = function (wait) {
417 446 this.output_area.clear_output(wait);
418 447 };
419 448
420 449
421 450 // JSON serialization
422 451
423 452 CodeCell.prototype.fromJSON = function (data) {
424 453 IPython.Cell.prototype.fromJSON.apply(this, arguments);
425 454 if (data.cell_type === 'code') {
426 455 if (data.input !== undefined) {
427 456 this.set_text(data.input);
428 457 // make this value the starting point, so that we can only undo
429 458 // to this state, instead of a blank cell
430 459 this.code_mirror.clearHistory();
431 460 this.auto_highlight();
432 461 }
433 462 if (data.prompt_number !== undefined) {
434 463 this.set_input_prompt(data.prompt_number);
435 464 } else {
436 465 this.set_input_prompt();
437 466 }
438 467 this.output_area.fromJSON(data.outputs);
439 468 if (data.collapsed !== undefined) {
440 469 if (data.collapsed) {
441 470 this.collapse();
442 471 } else {
443 472 this.expand();
444 473 }
445 474 }
446 475 }
447 476 };
448 477
449 478
450 479 CodeCell.prototype.toJSON = function () {
451 480 var data = IPython.Cell.prototype.toJSON.apply(this);
452 481 data.input = this.get_text();
453 482 // is finite protect against undefined and '*' value
454 483 if (isFinite(this.input_prompt_number)) {
455 484 data.prompt_number = this.input_prompt_number;
456 485 }
457 486 var outputs = this.output_area.toJSON();
458 487 data.outputs = outputs;
459 488 data.language = 'python';
460 489 data.collapsed = this.collapsed;
461 490 return data;
462 491 };
463 492
464 493
465 494 IPython.CodeCell = CodeCell;
466 495
467 496 return IPython;
468 497 }(IPython));
@@ -1,2267 +1,2267
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Notebook
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 var utils = IPython.utils;
16 16 var key = IPython.utils.keycodes;
17 17
18 18 /**
19 19 * A notebook contains and manages cells.
20 20 *
21 21 * @class Notebook
22 22 * @constructor
23 23 * @param {String} selector A jQuery selector for the notebook's DOM element
24 24 * @param {Object} [options] A config object
25 25 */
26 26 var Notebook = function (selector, options) {
27 27 var options = options || {};
28 28 this._baseProjectUrl = options.baseProjectUrl;
29 29 this.notebook_path = options.notebookPath;
30 30 this.notebook_name = options.notebookName;
31 31 this.element = $(selector);
32 32 this.element.scroll();
33 33 this.element.data("notebook", this);
34 34 this.next_prompt_number = 1;
35 35 this.session = null;
36 36 this.kernel = null;
37 37 this.clipboard = null;
38 38 this.undelete_backup = null;
39 39 this.undelete_index = null;
40 40 this.undelete_below = false;
41 41 this.paste_enabled = false;
42 42 this.set_dirty(false);
43 43 this.metadata = {};
44 44 this._checkpoint_after_save = false;
45 45 this.last_checkpoint = null;
46 46 this.checkpoints = [];
47 47 this.autosave_interval = 0;
48 48 this.autosave_timer = null;
49 49 // autosave *at most* every two minutes
50 50 this.minimum_autosave_interval = 120000;
51 51 // single worksheet for now
52 52 this.worksheet_metadata = {};
53 53 this.control_key_active = false;
54 54 this.notebook_name_blacklist_re = /[\/\\:]/;
55 55 this.nbformat = 3 // Increment this when changing the nbformat
56 56 this.nbformat_minor = 0 // Increment this when changing the nbformat
57 57 this.style();
58 58 this.create_elements();
59 59 this.bind_events();
60 60 };
61 61
62 62 /**
63 63 * Tweak the notebook's CSS style.
64 64 *
65 65 * @method style
66 66 */
67 67 Notebook.prototype.style = function () {
68 68 $('div#notebook').addClass('border-box-sizing');
69 69 };
70 70
71 71 /**
72 72 * Get the root URL of the notebook server.
73 73 *
74 74 * @method baseProjectUrl
75 75 * @return {String} The base project URL
76 76 */
77 77 Notebook.prototype.baseProjectUrl = function(){
78 78 return this._baseProjectUrl || $('body').data('baseProjectUrl');
79 79 };
80 80
81 81 Notebook.prototype.notebookName = function() {
82 82 return $('body').data('notebookName');
83 83 };
84 84
85 85 Notebook.prototype.notebookPath = function() {
86 86 return $('body').data('notebookPath');
87 87 };
88 88
89 89 /**
90 90 * Create an HTML and CSS representation of the notebook.
91 91 *
92 92 * @method create_elements
93 93 */
94 94 Notebook.prototype.create_elements = function () {
95 95 // We add this end_space div to the end of the notebook div to:
96 96 // i) provide a margin between the last cell and the end of the notebook
97 97 // ii) to prevent the div from scrolling up when the last cell is being
98 98 // edited, but is too low on the page, which browsers will do automatically.
99 99 var that = this;
100 100 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
101 101 var end_space = $('<div/>').addClass('end_space');
102 102 end_space.dblclick(function (e) {
103 103 var ncells = that.ncells();
104 104 that.insert_cell_below('code',ncells-1);
105 105 });
106 106 this.element.append(this.container);
107 107 this.container.append(end_space);
108 108 $('div#notebook').addClass('border-box-sizing');
109 109 };
110 110
111 111 /**
112 112 * Bind JavaScript events: key presses and custom IPython events.
113 113 *
114 114 * @method bind_events
115 115 */
116 116 Notebook.prototype.bind_events = function () {
117 117 var that = this;
118 118
119 119 $([IPython.events]).on('set_next_input.Notebook', function (event, data) {
120 120 var index = that.find_cell_index(data.cell);
121 121 var new_cell = that.insert_cell_below('code',index);
122 122 new_cell.set_text(data.text);
123 123 that.dirty = true;
124 124 });
125 125
126 126 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
127 127 that.dirty = data.value;
128 128 });
129 129
130 130 $([IPython.events]).on('select.Cell', function (event, data) {
131 131 var index = that.find_cell_index(data.cell);
132 132 that.select(index);
133 133 });
134 134
135 135 $([IPython.events]).on('status_autorestarting.Kernel', function () {
136 136 IPython.dialog.modal({
137 137 title: "Kernel Restarting",
138 138 body: "The kernel appears to have died. It will restart automatically.",
139 139 buttons: {
140 140 OK : {
141 141 class : "btn-primary"
142 142 }
143 143 }
144 144 });
145 145 });
146 146
147 147
148 148 $(document).keydown(function (event) {
149 149
150 150 // Save (CTRL+S) or (AppleKey+S)
151 151 //metaKey = applekey on mac
152 152 if ((event.ctrlKey || event.metaKey) && event.keyCode==83) {
153 153 that.save_checkpoint();
154 154 event.preventDefault();
155 155 return false;
156 156 } else if (event.which === key.ESC) {
157 157 // Intercept escape at highest level to avoid closing
158 158 // websocket connection with firefox
159 159 IPython.pager.collapse();
160 160 event.preventDefault();
161 161 } else if (event.which === key.SHIFT) {
162 162 // ignore shift keydown
163 163 return true;
164 164 }
165 165 if (event.which === key.UPARROW && !event.shiftKey) {
166 166 var cell = that.get_selected_cell();
167 167 if (cell && cell.at_top()) {
168 168 event.preventDefault();
169 169 that.select_prev();
170 170 };
171 171 } else if (event.which === key.DOWNARROW && !event.shiftKey) {
172 172 var cell = that.get_selected_cell();
173 173 if (cell && cell.at_bottom()) {
174 174 event.preventDefault();
175 175 that.select_next();
176 176 };
177 177 } else if (event.which === key.ENTER && event.shiftKey) {
178 178 that.execute_selected_cell();
179 179 return false;
180 180 } else if (event.which === key.ENTER && event.altKey) {
181 181 // Execute code cell, and insert new in place
182 182 that.execute_selected_cell();
183 183 // Only insert a new cell, if we ended up in an already populated cell
184 184 if (/\S/.test(that.get_selected_cell().get_text()) == true) {
185 185 that.insert_cell_above('code');
186 186 }
187 187 return false;
188 188 } else if (event.which === key.ENTER && event.ctrlKey) {
189 189 that.execute_selected_cell({terminal:true});
190 190 return false;
191 191 } else if (event.which === 77 && event.ctrlKey && that.control_key_active == false) {
192 192 that.control_key_active = true;
193 193 return false;
194 194 } else if (event.which === 88 && that.control_key_active) {
195 195 // Cut selected cell = x
196 196 that.cut_cell();
197 197 that.control_key_active = false;
198 198 return false;
199 199 } else if (event.which === 67 && that.control_key_active) {
200 200 // Copy selected cell = c
201 201 that.copy_cell();
202 202 that.control_key_active = false;
203 203 return false;
204 204 } else if (event.which === 86 && that.control_key_active) {
205 205 // Paste below selected cell = v
206 206 that.paste_cell_below();
207 207 that.control_key_active = false;
208 208 return false;
209 209 } else if (event.which === 68 && that.control_key_active) {
210 210 // Delete selected cell = d
211 211 that.delete_cell();
212 212 that.control_key_active = false;
213 213 return false;
214 214 } else if (event.which === 65 && that.control_key_active) {
215 215 // Insert code cell above selected = a
216 216 that.insert_cell_above('code');
217 217 that.control_key_active = false;
218 218 return false;
219 219 } else if (event.which === 66 && that.control_key_active) {
220 220 // Insert code cell below selected = b
221 221 that.insert_cell_below('code');
222 222 that.control_key_active = false;
223 223 return false;
224 224 } else if (event.which === 89 && that.control_key_active) {
225 225 // To code = y
226 226 that.to_code();
227 227 that.control_key_active = false;
228 228 return false;
229 229 } else if (event.which === 77 && that.control_key_active) {
230 230 // To markdown = m
231 231 that.to_markdown();
232 232 that.control_key_active = false;
233 233 return false;
234 234 } else if (event.which === 84 && that.control_key_active) {
235 235 // To Raw = t
236 236 that.to_raw();
237 237 that.control_key_active = false;
238 238 return false;
239 239 } else if (event.which === 49 && that.control_key_active) {
240 240 // To Heading 1 = 1
241 241 that.to_heading(undefined, 1);
242 242 that.control_key_active = false;
243 243 return false;
244 244 } else if (event.which === 50 && that.control_key_active) {
245 245 // To Heading 2 = 2
246 246 that.to_heading(undefined, 2);
247 247 that.control_key_active = false;
248 248 return false;
249 249 } else if (event.which === 51 && that.control_key_active) {
250 250 // To Heading 3 = 3
251 251 that.to_heading(undefined, 3);
252 252 that.control_key_active = false;
253 253 return false;
254 254 } else if (event.which === 52 && that.control_key_active) {
255 255 // To Heading 4 = 4
256 256 that.to_heading(undefined, 4);
257 257 that.control_key_active = false;
258 258 return false;
259 259 } else if (event.which === 53 && that.control_key_active) {
260 260 // To Heading 5 = 5
261 261 that.to_heading(undefined, 5);
262 262 that.control_key_active = false;
263 263 return false;
264 264 } else if (event.which === 54 && that.control_key_active) {
265 265 // To Heading 6 = 6
266 266 that.to_heading(undefined, 6);
267 267 that.control_key_active = false;
268 268 return false;
269 269 } else if (event.which === 79 && that.control_key_active) {
270 270 // Toggle output = o
271 271 if (event.shiftKey){
272 272 that.toggle_output_scroll();
273 273 } else {
274 274 that.toggle_output();
275 275 }
276 276 that.control_key_active = false;
277 277 return false;
278 278 } else if (event.which === 83 && that.control_key_active) {
279 279 // Save notebook = s
280 280 that.save_checkpoint();
281 281 that.control_key_active = false;
282 282 return false;
283 283 } else if (event.which === 74 && that.control_key_active) {
284 284 // Move cell down = j
285 285 that.move_cell_down();
286 286 that.control_key_active = false;
287 287 return false;
288 288 } else if (event.which === 75 && that.control_key_active) {
289 289 // Move cell up = k
290 290 that.move_cell_up();
291 291 that.control_key_active = false;
292 292 return false;
293 293 } else if (event.which === 80 && that.control_key_active) {
294 294 // Select previous = p
295 295 that.select_prev();
296 296 that.control_key_active = false;
297 297 return false;
298 298 } else if (event.which === 78 && that.control_key_active) {
299 299 // Select next = n
300 300 that.select_next();
301 301 that.control_key_active = false;
302 302 return false;
303 303 } else if (event.which === 76 && that.control_key_active) {
304 304 // Toggle line numbers = l
305 305 that.cell_toggle_line_numbers();
306 306 that.control_key_active = false;
307 307 return false;
308 308 } else if (event.which === 73 && that.control_key_active) {
309 309 // Interrupt kernel = i
310 310 that.session.interrupt_kernel();
311 311 that.control_key_active = false;
312 312 return false;
313 313 } else if (event.which === 190 && that.control_key_active) {
314 314 // Restart kernel = . # matches qt console
315 315 that.restart_kernel();
316 316 that.control_key_active = false;
317 317 return false;
318 318 } else if (event.which === 72 && that.control_key_active) {
319 319 // Show keyboard shortcuts = h
320 320 IPython.quick_help.show_keyboard_shortcuts();
321 321 that.control_key_active = false;
322 322 return false;
323 323 } else if (event.which === 90 && that.control_key_active) {
324 324 // Undo last cell delete = z
325 325 that.undelete();
326 326 that.control_key_active = false;
327 327 return false;
328 328 } else if ((event.which === 189 || event.which === 173) &&
329 329 that.control_key_active) {
330 330 // how fun! '-' is 189 in Chrome, but 173 in FF and Opera
331 331 // Split cell = -
332 332 that.split_cell();
333 333 that.control_key_active = false;
334 334 return false;
335 335 } else if (that.control_key_active) {
336 336 that.control_key_active = false;
337 337 return true;
338 338 }
339 339 return true;
340 340 });
341 341
342 342 var collapse_time = function(time){
343 343 var app_height = $('#ipython-main-app').height(); // content height
344 344 var splitter_height = $('div#pager_splitter').outerHeight(true);
345 345 var new_height = app_height - splitter_height;
346 346 that.element.animate({height : new_height + 'px'}, time);
347 347 }
348 348
349 349 this.element.bind('collapse_pager', function (event,extrap) {
350 350 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
351 351 collapse_time(time);
352 352 });
353 353
354 354 var expand_time = function(time) {
355 355 var app_height = $('#ipython-main-app').height(); // content height
356 356 var splitter_height = $('div#pager_splitter').outerHeight(true);
357 357 var pager_height = $('div#pager').outerHeight(true);
358 358 var new_height = app_height - pager_height - splitter_height;
359 359 that.element.animate({height : new_height + 'px'}, time);
360 360 }
361 361
362 362 this.element.bind('expand_pager', function (event, extrap) {
363 363 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
364 364 expand_time(time);
365 365 });
366 366
367 367 // Firefox 22 broke $(window).on("beforeunload")
368 368 // I'm not sure why or how.
369 369 window.onbeforeunload = function (e) {
370 370 // TODO: Make killing the kernel configurable.
371 371 var kill_kernel = false;
372 372 if (kill_kernel) {
373 373 that.session.kill_kernel();
374 374 }
375 375 // if we are autosaving, trigger an autosave on nav-away.
376 376 // still warn, because if we don't the autosave may fail.
377 377 if (that.dirty) {
378 378 if ( that.autosave_interval ) {
379 379 // schedule autosave in a timeout
380 380 // this gives you a chance to forcefully discard changes
381 381 // by reloading the page if you *really* want to.
382 382 // the timer doesn't start until you *dismiss* the dialog.
383 383 setTimeout(function () {
384 384 if (that.dirty) {
385 385 that.save_notebook();
386 386 }
387 387 }, 1000);
388 388 return "Autosave in progress, latest changes may be lost.";
389 389 } else {
390 390 return "Unsaved changes will be lost.";
391 391 }
392 392 };
393 393 // Null is the *only* return value that will make the browser not
394 394 // pop up the "don't leave" dialog.
395 395 return null;
396 396 };
397 397 };
398 398
399 399 /**
400 400 * Set the dirty flag, and trigger the set_dirty.Notebook event
401 401 *
402 402 * @method set_dirty
403 403 */
404 404 Notebook.prototype.set_dirty = function (value) {
405 405 if (value === undefined) {
406 406 value = true;
407 407 }
408 408 if (this.dirty == value) {
409 409 return;
410 410 }
411 411 $([IPython.events]).trigger('set_dirty.Notebook', {value: value});
412 412 };
413 413
414 414 /**
415 415 * Scroll the top of the page to a given cell.
416 416 *
417 417 * @method scroll_to_cell
418 418 * @param {Number} cell_number An index of the cell to view
419 419 * @param {Number} time Animation time in milliseconds
420 420 * @return {Number} Pixel offset from the top of the container
421 421 */
422 422 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
423 423 var cells = this.get_cells();
424 424 var time = time || 0;
425 425 cell_number = Math.min(cells.length-1,cell_number);
426 426 cell_number = Math.max(0 ,cell_number);
427 427 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
428 428 this.element.animate({scrollTop:scroll_value}, time);
429 429 return scroll_value;
430 430 };
431 431
432 432 /**
433 433 * Scroll to the bottom of the page.
434 434 *
435 435 * @method scroll_to_bottom
436 436 */
437 437 Notebook.prototype.scroll_to_bottom = function () {
438 438 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
439 439 };
440 440
441 441 /**
442 442 * Scroll to the top of the page.
443 443 *
444 444 * @method scroll_to_top
445 445 */
446 446 Notebook.prototype.scroll_to_top = function () {
447 447 this.element.animate({scrollTop:0}, 0);
448 448 };
449 449
450 450 // Edit Notebook metadata
451 451
452 452 Notebook.prototype.edit_metadata = function () {
453 453 var that = this;
454 454 IPython.dialog.edit_metadata(this.metadata, function (md) {
455 455 that.metadata = md;
456 456 }, 'Notebook');
457 457 };
458 458
459 459 // Cell indexing, retrieval, etc.
460 460
461 461 /**
462 462 * Get all cell elements in the notebook.
463 463 *
464 464 * @method get_cell_elements
465 465 * @return {jQuery} A selector of all cell elements
466 466 */
467 467 Notebook.prototype.get_cell_elements = function () {
468 468 return this.container.children("div.cell");
469 469 };
470 470
471 471 /**
472 472 * Get a particular cell element.
473 473 *
474 474 * @method get_cell_element
475 475 * @param {Number} index An index of a cell to select
476 476 * @return {jQuery} A selector of the given cell.
477 477 */
478 478 Notebook.prototype.get_cell_element = function (index) {
479 479 var result = null;
480 480 var e = this.get_cell_elements().eq(index);
481 481 if (e.length !== 0) {
482 482 result = e;
483 483 }
484 484 return result;
485 485 };
486 486
487 487 /**
488 488 * Count the cells in this notebook.
489 489 *
490 490 * @method ncells
491 491 * @return {Number} The number of cells in this notebook
492 492 */
493 493 Notebook.prototype.ncells = function () {
494 494 return this.get_cell_elements().length;
495 495 };
496 496
497 497 /**
498 498 * Get all Cell objects in this notebook.
499 499 *
500 500 * @method get_cells
501 501 * @return {Array} This notebook's Cell objects
502 502 */
503 503 // TODO: we are often calling cells as cells()[i], which we should optimize
504 504 // to cells(i) or a new method.
505 505 Notebook.prototype.get_cells = function () {
506 506 return this.get_cell_elements().toArray().map(function (e) {
507 507 return $(e).data("cell");
508 508 });
509 509 };
510 510
511 511 /**
512 512 * Get a Cell object from this notebook.
513 513 *
514 514 * @method get_cell
515 515 * @param {Number} index An index of a cell to retrieve
516 516 * @return {Cell} A particular cell
517 517 */
518 518 Notebook.prototype.get_cell = function (index) {
519 519 var result = null;
520 520 var ce = this.get_cell_element(index);
521 521 if (ce !== null) {
522 522 result = ce.data('cell');
523 523 }
524 524 return result;
525 525 }
526 526
527 527 /**
528 528 * Get the cell below a given cell.
529 529 *
530 530 * @method get_next_cell
531 531 * @param {Cell} cell The provided cell
532 532 * @return {Cell} The next cell
533 533 */
534 534 Notebook.prototype.get_next_cell = function (cell) {
535 535 var result = null;
536 536 var index = this.find_cell_index(cell);
537 537 if (this.is_valid_cell_index(index+1)) {
538 538 result = this.get_cell(index+1);
539 539 }
540 540 return result;
541 541 }
542 542
543 543 /**
544 544 * Get the cell above a given cell.
545 545 *
546 546 * @method get_prev_cell
547 547 * @param {Cell} cell The provided cell
548 548 * @return {Cell} The previous cell
549 549 */
550 550 Notebook.prototype.get_prev_cell = function (cell) {
551 551 // TODO: off-by-one
552 552 // nb.get_prev_cell(nb.get_cell(1)) is null
553 553 var result = null;
554 554 var index = this.find_cell_index(cell);
555 555 if (index !== null && index > 1) {
556 556 result = this.get_cell(index-1);
557 557 }
558 558 return result;
559 559 }
560 560
561 561 /**
562 562 * Get the numeric index of a given cell.
563 563 *
564 564 * @method find_cell_index
565 565 * @param {Cell} cell The provided cell
566 566 * @return {Number} The cell's numeric index
567 567 */
568 568 Notebook.prototype.find_cell_index = function (cell) {
569 569 var result = null;
570 570 this.get_cell_elements().filter(function (index) {
571 571 if ($(this).data("cell") === cell) {
572 572 result = index;
573 573 };
574 574 });
575 575 return result;
576 576 };
577 577
578 578 /**
579 579 * Get a given index , or the selected index if none is provided.
580 580 *
581 581 * @method index_or_selected
582 582 * @param {Number} index A cell's index
583 583 * @return {Number} The given index, or selected index if none is provided.
584 584 */
585 585 Notebook.prototype.index_or_selected = function (index) {
586 586 var i;
587 587 if (index === undefined || index === null) {
588 588 i = this.get_selected_index();
589 589 if (i === null) {
590 590 i = 0;
591 591 }
592 592 } else {
593 593 i = index;
594 594 }
595 595 return i;
596 596 };
597 597
598 598 /**
599 599 * Get the currently selected cell.
600 600 * @method get_selected_cell
601 601 * @return {Cell} The selected cell
602 602 */
603 603 Notebook.prototype.get_selected_cell = function () {
604 604 var index = this.get_selected_index();
605 605 return this.get_cell(index);
606 606 };
607 607
608 608 /**
609 609 * Check whether a cell index is valid.
610 610 *
611 611 * @method is_valid_cell_index
612 612 * @param {Number} index A cell index
613 613 * @return True if the index is valid, false otherwise
614 614 */
615 615 Notebook.prototype.is_valid_cell_index = function (index) {
616 616 if (index !== null && index >= 0 && index < this.ncells()) {
617 617 return true;
618 618 } else {
619 619 return false;
620 620 };
621 621 }
622 622
623 623 /**
624 624 * Get the index of the currently selected cell.
625 625
626 626 * @method get_selected_index
627 627 * @return {Number} The selected cell's numeric index
628 628 */
629 629 Notebook.prototype.get_selected_index = function () {
630 630 var result = null;
631 631 this.get_cell_elements().filter(function (index) {
632 632 if ($(this).data("cell").selected === true) {
633 633 result = index;
634 634 };
635 635 });
636 636 return result;
637 637 };
638 638
639 639
640 640 // Cell selection.
641 641
642 642 /**
643 643 * Programmatically select a cell.
644 644 *
645 645 * @method select
646 646 * @param {Number} index A cell's index
647 647 * @return {Notebook} This notebook
648 648 */
649 649 Notebook.prototype.select = function (index) {
650 650 if (this.is_valid_cell_index(index)) {
651 651 var sindex = this.get_selected_index()
652 652 if (sindex !== null && index !== sindex) {
653 653 this.get_cell(sindex).unselect();
654 654 };
655 655 var cell = this.get_cell(index);
656 656 cell.select();
657 657 if (cell.cell_type === 'heading') {
658 658 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
659 659 {'cell_type':cell.cell_type,level:cell.level}
660 660 );
661 661 } else {
662 662 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
663 663 {'cell_type':cell.cell_type}
664 664 );
665 665 };
666 666 };
667 667 return this;
668 668 };
669 669
670 670 /**
671 671 * Programmatically select the next cell.
672 672 *
673 673 * @method select_next
674 674 * @return {Notebook} This notebook
675 675 */
676 676 Notebook.prototype.select_next = function () {
677 677 var index = this.get_selected_index();
678 678 this.select(index+1);
679 679 return this;
680 680 };
681 681
682 682 /**
683 683 * Programmatically select the previous cell.
684 684 *
685 685 * @method select_prev
686 686 * @return {Notebook} This notebook
687 687 */
688 688 Notebook.prototype.select_prev = function () {
689 689 var index = this.get_selected_index();
690 690 this.select(index-1);
691 691 return this;
692 692 };
693 693
694 694
695 695 // Cell movement
696 696
697 697 /**
698 698 * Move given (or selected) cell up and select it.
699 699 *
700 700 * @method move_cell_up
701 701 * @param [index] {integer} cell index
702 702 * @return {Notebook} This notebook
703 703 **/
704 704 Notebook.prototype.move_cell_up = function (index) {
705 705 var i = this.index_or_selected(index);
706 706 if (this.is_valid_cell_index(i) && i > 0) {
707 707 var pivot = this.get_cell_element(i-1);
708 708 var tomove = this.get_cell_element(i);
709 709 if (pivot !== null && tomove !== null) {
710 710 tomove.detach();
711 711 pivot.before(tomove);
712 712 this.select(i-1);
713 713 };
714 714 this.set_dirty(true);
715 715 };
716 716 return this;
717 717 };
718 718
719 719
720 720 /**
721 721 * Move given (or selected) cell down and select it
722 722 *
723 723 * @method move_cell_down
724 724 * @param [index] {integer} cell index
725 725 * @return {Notebook} This notebook
726 726 **/
727 727 Notebook.prototype.move_cell_down = function (index) {
728 728 var i = this.index_or_selected(index);
729 729 if ( this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
730 730 var pivot = this.get_cell_element(i+1);
731 731 var tomove = this.get_cell_element(i);
732 732 if (pivot !== null && tomove !== null) {
733 733 tomove.detach();
734 734 pivot.after(tomove);
735 735 this.select(i+1);
736 736 };
737 737 };
738 738 this.set_dirty();
739 739 return this;
740 740 };
741 741
742 742
743 743 // Insertion, deletion.
744 744
745 745 /**
746 746 * Delete a cell from the notebook.
747 747 *
748 748 * @method delete_cell
749 749 * @param [index] A cell's numeric index
750 750 * @return {Notebook} This notebook
751 751 */
752 752 Notebook.prototype.delete_cell = function (index) {
753 753 var i = this.index_or_selected(index);
754 754 var cell = this.get_selected_cell();
755 755 this.undelete_backup = cell.toJSON();
756 756 $('#undelete_cell').removeClass('disabled');
757 757 if (this.is_valid_cell_index(i)) {
758 758 var ce = this.get_cell_element(i);
759 759 ce.remove();
760 760 if (i === (this.ncells())) {
761 761 this.select(i-1);
762 762 this.undelete_index = i - 1;
763 763 this.undelete_below = true;
764 764 } else {
765 765 this.select(i);
766 766 this.undelete_index = i;
767 767 this.undelete_below = false;
768 768 };
769 769 $([IPython.events]).trigger('delete.Cell', {'cell': cell, 'index': i});
770 770 this.set_dirty(true);
771 771 };
772 772 return this;
773 773 };
774 774
775 775 /**
776 776 * Insert a cell so that after insertion the cell is at given index.
777 777 *
778 778 * Similar to insert_above, but index parameter is mandatory
779 779 *
780 780 * Index will be brought back into the accissible range [0,n]
781 781 *
782 782 * @method insert_cell_at_index
783 783 * @param type {string} in ['code','markdown','heading']
784 784 * @param [index] {int} a valid index where to inser cell
785 785 *
786 786 * @return cell {cell|null} created cell or null
787 787 **/
788 788 Notebook.prototype.insert_cell_at_index = function(type, index){
789 789
790 790 var ncells = this.ncells();
791 791 var index = Math.min(index,ncells);
792 792 index = Math.max(index,0);
793 793 var cell = null;
794 794
795 795 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
796 796 if (type === 'code') {
797 797 cell = new IPython.CodeCell(this.kernel);
798 798 cell.set_input_prompt();
799 799 } else if (type === 'markdown') {
800 800 cell = new IPython.MarkdownCell();
801 801 } else if (type === 'raw') {
802 802 cell = new IPython.RawCell();
803 803 } else if (type === 'heading') {
804 804 cell = new IPython.HeadingCell();
805 805 }
806 806
807 807 if(this._insert_element_at_index(cell.element,index)){
808 808 cell.render();
809 809 this.select(this.find_cell_index(cell));
810 810 $([IPython.events]).trigger('create.Cell', {'cell': cell, 'index': index});
811 811 this.set_dirty(true);
812 812 }
813 813 }
814 814 return cell;
815 815
816 816 };
817 817
818 818 /**
819 819 * Insert an element at given cell index.
820 820 *
821 821 * @method _insert_element_at_index
822 822 * @param element {dom element} a cell element
823 823 * @param [index] {int} a valid index where to inser cell
824 824 * @private
825 825 *
826 826 * return true if everything whent fine.
827 827 **/
828 828 Notebook.prototype._insert_element_at_index = function(element, index){
829 829 if (element === undefined){
830 830 return false;
831 831 }
832 832
833 833 var ncells = this.ncells();
834 834
835 835 if (ncells === 0) {
836 836 // special case append if empty
837 837 this.element.find('div.end_space').before(element);
838 838 } else if ( ncells === index ) {
839 839 // special case append it the end, but not empty
840 840 this.get_cell_element(index-1).after(element);
841 841 } else if (this.is_valid_cell_index(index)) {
842 842 // otherwise always somewhere to append to
843 843 this.get_cell_element(index).before(element);
844 844 } else {
845 845 return false;
846 846 }
847 847
848 848 if (this.undelete_index !== null && index <= this.undelete_index) {
849 849 this.undelete_index = this.undelete_index + 1;
850 850 this.set_dirty(true);
851 851 }
852 852 return true;
853 853 };
854 854
855 855 /**
856 856 * Insert a cell of given type above given index, or at top
857 857 * of notebook if index smaller than 0.
858 858 *
859 859 * default index value is the one of currently selected cell
860 860 *
861 861 * @method insert_cell_above
862 862 * @param type {string} cell type
863 863 * @param [index] {integer}
864 864 *
865 865 * @return handle to created cell or null
866 866 **/
867 867 Notebook.prototype.insert_cell_above = function (type, index) {
868 868 index = this.index_or_selected(index);
869 869 return this.insert_cell_at_index(type, index);
870 870 };
871 871
872 872 /**
873 873 * Insert a cell of given type below given index, or at bottom
874 874 * of notebook if index greater thatn number of cell
875 875 *
876 876 * default index value is the one of currently selected cell
877 877 *
878 878 * @method insert_cell_below
879 879 * @param type {string} cell type
880 880 * @param [index] {integer}
881 881 *
882 882 * @return handle to created cell or null
883 883 *
884 884 **/
885 885 Notebook.prototype.insert_cell_below = function (type, index) {
886 886 index = this.index_or_selected(index);
887 887 return this.insert_cell_at_index(type, index+1);
888 888 };
889 889
890 890
891 891 /**
892 892 * Insert cell at end of notebook
893 893 *
894 894 * @method insert_cell_at_bottom
895 895 * @param {String} type cell type
896 896 *
897 897 * @return the added cell; or null
898 898 **/
899 899 Notebook.prototype.insert_cell_at_bottom = function (type){
900 900 var len = this.ncells();
901 901 return this.insert_cell_below(type,len-1);
902 902 };
903 903
904 904 /**
905 905 * Turn a cell into a code cell.
906 906 *
907 907 * @method to_code
908 908 * @param {Number} [index] A cell's index
909 909 */
910 910 Notebook.prototype.to_code = function (index) {
911 911 var i = this.index_or_selected(index);
912 912 if (this.is_valid_cell_index(i)) {
913 913 var source_element = this.get_cell_element(i);
914 914 var source_cell = source_element.data("cell");
915 915 if (!(source_cell instanceof IPython.CodeCell)) {
916 916 var target_cell = this.insert_cell_below('code',i);
917 917 var text = source_cell.get_text();
918 918 if (text === source_cell.placeholder) {
919 919 text = '';
920 920 }
921 921 target_cell.set_text(text);
922 922 // make this value the starting point, so that we can only undo
923 923 // to this state, instead of a blank cell
924 924 target_cell.code_mirror.clearHistory();
925 925 source_element.remove();
926 926 this.set_dirty(true);
927 927 };
928 928 };
929 929 };
930 930
931 931 /**
932 932 * Turn a cell into a Markdown cell.
933 933 *
934 934 * @method to_markdown
935 935 * @param {Number} [index] A cell's index
936 936 */
937 937 Notebook.prototype.to_markdown = function (index) {
938 938 var i = this.index_or_selected(index);
939 939 if (this.is_valid_cell_index(i)) {
940 940 var source_element = this.get_cell_element(i);
941 941 var source_cell = source_element.data("cell");
942 942 if (!(source_cell instanceof IPython.MarkdownCell)) {
943 943 var target_cell = this.insert_cell_below('markdown',i);
944 944 var text = source_cell.get_text();
945 945 if (text === source_cell.placeholder) {
946 946 text = '';
947 947 };
948 948 // The edit must come before the set_text.
949 target_cell.edit();
949 target_cell.unrender();
950 950 target_cell.set_text(text);
951 951 // make this value the starting point, so that we can only undo
952 952 // to this state, instead of a blank cell
953 953 target_cell.code_mirror.clearHistory();
954 954 source_element.remove();
955 955 this.set_dirty(true);
956 956 };
957 957 };
958 958 };
959 959
960 960 /**
961 961 * Turn a cell into a raw text cell.
962 962 *
963 963 * @method to_raw
964 964 * @param {Number} [index] A cell's index
965 965 */
966 966 Notebook.prototype.to_raw = function (index) {
967 967 var i = this.index_or_selected(index);
968 968 if (this.is_valid_cell_index(i)) {
969 969 var source_element = this.get_cell_element(i);
970 970 var source_cell = source_element.data("cell");
971 971 var target_cell = null;
972 972 if (!(source_cell instanceof IPython.RawCell)) {
973 973 target_cell = this.insert_cell_below('raw',i);
974 974 var text = source_cell.get_text();
975 975 if (text === source_cell.placeholder) {
976 976 text = '';
977 977 };
978 978 // The edit must come before the set_text.
979 target_cell.edit();
979 target_cell.unrender();
980 980 target_cell.set_text(text);
981 981 // make this value the starting point, so that we can only undo
982 982 // to this state, instead of a blank cell
983 983 target_cell.code_mirror.clearHistory();
984 984 source_element.remove();
985 985 this.set_dirty(true);
986 986 };
987 987 };
988 988 };
989 989
990 990 /**
991 991 * Turn a cell into a heading cell.
992 992 *
993 993 * @method to_heading
994 994 * @param {Number} [index] A cell's index
995 995 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
996 996 */
997 997 Notebook.prototype.to_heading = function (index, level) {
998 998 level = level || 1;
999 999 var i = this.index_or_selected(index);
1000 1000 if (this.is_valid_cell_index(i)) {
1001 1001 var source_element = this.get_cell_element(i);
1002 1002 var source_cell = source_element.data("cell");
1003 1003 var target_cell = null;
1004 1004 if (source_cell instanceof IPython.HeadingCell) {
1005 1005 source_cell.set_level(level);
1006 1006 } else {
1007 1007 target_cell = this.insert_cell_below('heading',i);
1008 1008 var text = source_cell.get_text();
1009 1009 if (text === source_cell.placeholder) {
1010 1010 text = '';
1011 1011 };
1012 1012 // The edit must come before the set_text.
1013 1013 target_cell.set_level(level);
1014 target_cell.edit();
1014 target_cell.unrender();
1015 1015 target_cell.set_text(text);
1016 1016 // make this value the starting point, so that we can only undo
1017 1017 // to this state, instead of a blank cell
1018 1018 target_cell.code_mirror.clearHistory();
1019 1019 source_element.remove();
1020 1020 this.set_dirty(true);
1021 1021 };
1022 1022 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
1023 1023 {'cell_type':'heading',level:level}
1024 1024 );
1025 1025 };
1026 1026 };
1027 1027
1028 1028
1029 1029 // Cut/Copy/Paste
1030 1030
1031 1031 /**
1032 1032 * Enable UI elements for pasting cells.
1033 1033 *
1034 1034 * @method enable_paste
1035 1035 */
1036 1036 Notebook.prototype.enable_paste = function () {
1037 1037 var that = this;
1038 1038 if (!this.paste_enabled) {
1039 1039 $('#paste_cell_replace').removeClass('disabled')
1040 1040 .on('click', function () {that.paste_cell_replace();});
1041 1041 $('#paste_cell_above').removeClass('disabled')
1042 1042 .on('click', function () {that.paste_cell_above();});
1043 1043 $('#paste_cell_below').removeClass('disabled')
1044 1044 .on('click', function () {that.paste_cell_below();});
1045 1045 this.paste_enabled = true;
1046 1046 };
1047 1047 };
1048 1048
1049 1049 /**
1050 1050 * Disable UI elements for pasting cells.
1051 1051 *
1052 1052 * @method disable_paste
1053 1053 */
1054 1054 Notebook.prototype.disable_paste = function () {
1055 1055 if (this.paste_enabled) {
1056 1056 $('#paste_cell_replace').addClass('disabled').off('click');
1057 1057 $('#paste_cell_above').addClass('disabled').off('click');
1058 1058 $('#paste_cell_below').addClass('disabled').off('click');
1059 1059 this.paste_enabled = false;
1060 1060 };
1061 1061 };
1062 1062
1063 1063 /**
1064 1064 * Cut a cell.
1065 1065 *
1066 1066 * @method cut_cell
1067 1067 */
1068 1068 Notebook.prototype.cut_cell = function () {
1069 1069 this.copy_cell();
1070 1070 this.delete_cell();
1071 1071 }
1072 1072
1073 1073 /**
1074 1074 * Copy a cell.
1075 1075 *
1076 1076 * @method copy_cell
1077 1077 */
1078 1078 Notebook.prototype.copy_cell = function () {
1079 1079 var cell = this.get_selected_cell();
1080 1080 this.clipboard = cell.toJSON();
1081 1081 this.enable_paste();
1082 1082 };
1083 1083
1084 1084 /**
1085 1085 * Replace the selected cell with a cell in the clipboard.
1086 1086 *
1087 1087 * @method paste_cell_replace
1088 1088 */
1089 1089 Notebook.prototype.paste_cell_replace = function () {
1090 1090 if (this.clipboard !== null && this.paste_enabled) {
1091 1091 var cell_data = this.clipboard;
1092 1092 var new_cell = this.insert_cell_above(cell_data.cell_type);
1093 1093 new_cell.fromJSON(cell_data);
1094 1094 var old_cell = this.get_next_cell(new_cell);
1095 1095 this.delete_cell(this.find_cell_index(old_cell));
1096 1096 this.select(this.find_cell_index(new_cell));
1097 1097 };
1098 1098 };
1099 1099
1100 1100 /**
1101 1101 * Paste a cell from the clipboard above the selected cell.
1102 1102 *
1103 1103 * @method paste_cell_above
1104 1104 */
1105 1105 Notebook.prototype.paste_cell_above = function () {
1106 1106 if (this.clipboard !== null && this.paste_enabled) {
1107 1107 var cell_data = this.clipboard;
1108 1108 var new_cell = this.insert_cell_above(cell_data.cell_type);
1109 1109 new_cell.fromJSON(cell_data);
1110 1110 };
1111 1111 };
1112 1112
1113 1113 /**
1114 1114 * Paste a cell from the clipboard below the selected cell.
1115 1115 *
1116 1116 * @method paste_cell_below
1117 1117 */
1118 1118 Notebook.prototype.paste_cell_below = function () {
1119 1119 if (this.clipboard !== null && this.paste_enabled) {
1120 1120 var cell_data = this.clipboard;
1121 1121 var new_cell = this.insert_cell_below(cell_data.cell_type);
1122 1122 new_cell.fromJSON(cell_data);
1123 1123 };
1124 1124 };
1125 1125
1126 1126 // Cell undelete
1127 1127
1128 1128 /**
1129 1129 * Restore the most recently deleted cell.
1130 1130 *
1131 1131 * @method undelete
1132 1132 */
1133 1133 Notebook.prototype.undelete = function() {
1134 1134 if (this.undelete_backup !== null && this.undelete_index !== null) {
1135 1135 var current_index = this.get_selected_index();
1136 1136 if (this.undelete_index < current_index) {
1137 1137 current_index = current_index + 1;
1138 1138 }
1139 1139 if (this.undelete_index >= this.ncells()) {
1140 1140 this.select(this.ncells() - 1);
1141 1141 }
1142 1142 else {
1143 1143 this.select(this.undelete_index);
1144 1144 }
1145 1145 var cell_data = this.undelete_backup;
1146 1146 var new_cell = null;
1147 1147 if (this.undelete_below) {
1148 1148 new_cell = this.insert_cell_below(cell_data.cell_type);
1149 1149 } else {
1150 1150 new_cell = this.insert_cell_above(cell_data.cell_type);
1151 1151 }
1152 1152 new_cell.fromJSON(cell_data);
1153 1153 this.select(current_index);
1154 1154 this.undelete_backup = null;
1155 1155 this.undelete_index = null;
1156 1156 }
1157 1157 $('#undelete_cell').addClass('disabled');
1158 1158 }
1159 1159
1160 1160 // Split/merge
1161 1161
1162 1162 /**
1163 1163 * Split the selected cell into two, at the cursor.
1164 1164 *
1165 1165 * @method split_cell
1166 1166 */
1167 1167 Notebook.prototype.split_cell = function () {
1168 1168 // Todo: implement spliting for other cell types.
1169 1169 var cell = this.get_selected_cell();
1170 1170 if (cell.is_splittable()) {
1171 1171 var texta = cell.get_pre_cursor();
1172 1172 var textb = cell.get_post_cursor();
1173 1173 if (cell instanceof IPython.CodeCell) {
1174 1174 cell.set_text(textb);
1175 1175 var new_cell = this.insert_cell_above('code');
1176 1176 new_cell.set_text(texta);
1177 1177 this.select_next();
1178 1178 } else if (cell instanceof IPython.MarkdownCell) {
1179 1179 cell.set_text(textb);
1180 1180 cell.render();
1181 1181 var new_cell = this.insert_cell_above('markdown');
1182 new_cell.edit(); // editor must be visible to call set_text
1182 new_cell.unrender(); // editor must be visible to call set_text
1183 1183 new_cell.set_text(texta);
1184 1184 new_cell.render();
1185 1185 this.select_next();
1186 1186 }
1187 1187 };
1188 1188 };
1189 1189
1190 1190 /**
1191 1191 * Combine the selected cell into the cell above it.
1192 1192 *
1193 1193 * @method merge_cell_above
1194 1194 */
1195 1195 Notebook.prototype.merge_cell_above = function () {
1196 1196 var index = this.get_selected_index();
1197 1197 var cell = this.get_cell(index);
1198 1198 if (!cell.is_mergeable()) {
1199 1199 return;
1200 1200 }
1201 1201 if (index > 0) {
1202 1202 var upper_cell = this.get_cell(index-1);
1203 1203 if (!upper_cell.is_mergeable()) {
1204 1204 return;
1205 1205 }
1206 1206 var upper_text = upper_cell.get_text();
1207 1207 var text = cell.get_text();
1208 1208 if (cell instanceof IPython.CodeCell) {
1209 1209 cell.set_text(upper_text+'\n'+text);
1210 1210 } else if (cell instanceof IPython.MarkdownCell) {
1211 cell.edit();
1211 cell.unrender();
1212 1212 cell.set_text(upper_text+'\n'+text);
1213 1213 cell.render();
1214 1214 };
1215 1215 this.delete_cell(index-1);
1216 1216 this.select(this.find_cell_index(cell));
1217 1217 };
1218 1218 };
1219 1219
1220 1220 /**
1221 1221 * Combine the selected cell into the cell below it.
1222 1222 *
1223 1223 * @method merge_cell_below
1224 1224 */
1225 1225 Notebook.prototype.merge_cell_below = function () {
1226 1226 var index = this.get_selected_index();
1227 1227 var cell = this.get_cell(index);
1228 1228 if (!cell.is_mergeable()) {
1229 1229 return;
1230 1230 }
1231 1231 if (index < this.ncells()-1) {
1232 1232 var lower_cell = this.get_cell(index+1);
1233 1233 if (!lower_cell.is_mergeable()) {
1234 1234 return;
1235 1235 }
1236 1236 var lower_text = lower_cell.get_text();
1237 1237 var text = cell.get_text();
1238 1238 if (cell instanceof IPython.CodeCell) {
1239 1239 cell.set_text(text+'\n'+lower_text);
1240 1240 } else if (cell instanceof IPython.MarkdownCell) {
1241 cell.edit();
1241 cell.unrender();
1242 1242 cell.set_text(text+'\n'+lower_text);
1243 1243 cell.render();
1244 1244 };
1245 1245 this.delete_cell(index+1);
1246 1246 this.select(this.find_cell_index(cell));
1247 1247 };
1248 1248 };
1249 1249
1250 1250
1251 1251 // Cell collapsing and output clearing
1252 1252
1253 1253 /**
1254 1254 * Hide a cell's output.
1255 1255 *
1256 1256 * @method collapse
1257 1257 * @param {Number} index A cell's numeric index
1258 1258 */
1259 1259 Notebook.prototype.collapse = function (index) {
1260 1260 var i = this.index_or_selected(index);
1261 1261 this.get_cell(i).collapse();
1262 1262 this.set_dirty(true);
1263 1263 };
1264 1264
1265 1265 /**
1266 1266 * Show a cell's output.
1267 1267 *
1268 1268 * @method expand
1269 1269 * @param {Number} index A cell's numeric index
1270 1270 */
1271 1271 Notebook.prototype.expand = function (index) {
1272 1272 var i = this.index_or_selected(index);
1273 1273 this.get_cell(i).expand();
1274 1274 this.set_dirty(true);
1275 1275 };
1276 1276
1277 1277 /** Toggle whether a cell's output is collapsed or expanded.
1278 1278 *
1279 1279 * @method toggle_output
1280 1280 * @param {Number} index A cell's numeric index
1281 1281 */
1282 1282 Notebook.prototype.toggle_output = function (index) {
1283 1283 var i = this.index_or_selected(index);
1284 1284 this.get_cell(i).toggle_output();
1285 1285 this.set_dirty(true);
1286 1286 };
1287 1287
1288 1288 /**
1289 1289 * Toggle a scrollbar for long cell outputs.
1290 1290 *
1291 1291 * @method toggle_output_scroll
1292 1292 * @param {Number} index A cell's numeric index
1293 1293 */
1294 1294 Notebook.prototype.toggle_output_scroll = function (index) {
1295 1295 var i = this.index_or_selected(index);
1296 1296 this.get_cell(i).toggle_output_scroll();
1297 1297 };
1298 1298
1299 1299 /**
1300 1300 * Hide each code cell's output area.
1301 1301 *
1302 1302 * @method collapse_all_output
1303 1303 */
1304 1304 Notebook.prototype.collapse_all_output = function () {
1305 1305 var ncells = this.ncells();
1306 1306 var cells = this.get_cells();
1307 1307 for (var i=0; i<ncells; i++) {
1308 1308 if (cells[i] instanceof IPython.CodeCell) {
1309 1309 cells[i].output_area.collapse();
1310 1310 }
1311 1311 };
1312 1312 // this should not be set if the `collapse` key is removed from nbformat
1313 1313 this.set_dirty(true);
1314 1314 };
1315 1315
1316 1316 /**
1317 1317 * Expand each code cell's output area, and add a scrollbar for long output.
1318 1318 *
1319 1319 * @method scroll_all_output
1320 1320 */
1321 1321 Notebook.prototype.scroll_all_output = function () {
1322 1322 var ncells = this.ncells();
1323 1323 var cells = this.get_cells();
1324 1324 for (var i=0; i<ncells; i++) {
1325 1325 if (cells[i] instanceof IPython.CodeCell) {
1326 1326 cells[i].output_area.expand();
1327 1327 cells[i].output_area.scroll_if_long();
1328 1328 }
1329 1329 };
1330 1330 // this should not be set if the `collapse` key is removed from nbformat
1331 1331 this.set_dirty(true);
1332 1332 };
1333 1333
1334 1334 /**
1335 1335 * Expand each code cell's output area, and remove scrollbars.
1336 1336 *
1337 1337 * @method expand_all_output
1338 1338 */
1339 1339 Notebook.prototype.expand_all_output = function () {
1340 1340 var ncells = this.ncells();
1341 1341 var cells = this.get_cells();
1342 1342 for (var i=0; i<ncells; i++) {
1343 1343 if (cells[i] instanceof IPython.CodeCell) {
1344 1344 cells[i].output_area.expand();
1345 1345 cells[i].output_area.unscroll_area();
1346 1346 }
1347 1347 };
1348 1348 // this should not be set if the `collapse` key is removed from nbformat
1349 1349 this.set_dirty(true);
1350 1350 };
1351 1351
1352 1352 /**
1353 1353 * Clear each code cell's output area.
1354 1354 *
1355 1355 * @method clear_all_output
1356 1356 */
1357 1357 Notebook.prototype.clear_all_output = function () {
1358 1358 var ncells = this.ncells();
1359 1359 var cells = this.get_cells();
1360 1360 for (var i=0; i<ncells; i++) {
1361 1361 if (cells[i] instanceof IPython.CodeCell) {
1362 1362 cells[i].clear_output();
1363 1363 // Make all In[] prompts blank, as well
1364 1364 // TODO: make this configurable (via checkbox?)
1365 1365 cells[i].set_input_prompt();
1366 1366 }
1367 1367 };
1368 1368 this.set_dirty(true);
1369 1369 };
1370 1370
1371 1371
1372 1372 // Other cell functions: line numbers, ...
1373 1373
1374 1374 /**
1375 1375 * Toggle line numbers in the selected cell's input area.
1376 1376 *
1377 1377 * @method cell_toggle_line_numbers
1378 1378 */
1379 1379 Notebook.prototype.cell_toggle_line_numbers = function() {
1380 1380 this.get_selected_cell().toggle_line_numbers();
1381 1381 };
1382 1382
1383 1383 // Session related things
1384 1384
1385 1385 /**
1386 1386 * Start a new session and set it on each code cell.
1387 1387 *
1388 1388 * @method start_session
1389 1389 */
1390 1390 Notebook.prototype.start_session = function () {
1391 1391 this.session = new IPython.Session(this.notebook_name, this.notebook_path, this);
1392 1392 this.session.start($.proxy(this._session_started, this));
1393 1393 };
1394 1394
1395 1395
1396 1396 /**
1397 1397 * Once a session is started, link the code cells to the kernel
1398 1398 *
1399 1399 */
1400 1400 Notebook.prototype._session_started = function(){
1401 1401 this.kernel = this.session.kernel;
1402 1402 var ncells = this.ncells();
1403 1403 for (var i=0; i<ncells; i++) {
1404 1404 var cell = this.get_cell(i);
1405 1405 if (cell instanceof IPython.CodeCell) {
1406 1406 cell.set_kernel(this.session.kernel);
1407 1407 };
1408 1408 };
1409 1409 };
1410 1410
1411 1411 /**
1412 1412 * Prompt the user to restart the IPython kernel.
1413 1413 *
1414 1414 * @method restart_kernel
1415 1415 */
1416 1416 Notebook.prototype.restart_kernel = function () {
1417 1417 var that = this;
1418 1418 IPython.dialog.modal({
1419 1419 title : "Restart kernel or continue running?",
1420 1420 body : $("<p/>").html(
1421 1421 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1422 1422 ),
1423 1423 buttons : {
1424 1424 "Continue running" : {},
1425 1425 "Restart" : {
1426 1426 "class" : "btn-danger",
1427 1427 "click" : function() {
1428 1428 that.session.restart_kernel();
1429 1429 }
1430 1430 }
1431 1431 }
1432 1432 });
1433 1433 };
1434 1434
1435 1435 /**
1436 1436 * Run the selected cell.
1437 1437 *
1438 1438 * Execute or render cell outputs.
1439 1439 *
1440 1440 * @method execute_selected_cell
1441 1441 * @param {Object} options Customize post-execution behavior
1442 1442 */
1443 1443 Notebook.prototype.execute_selected_cell = function (options) {
1444 1444 // add_new: should a new cell be added if we are at the end of the nb
1445 1445 // terminal: execute in terminal mode, which stays in the current cell
1446 1446 var default_options = {terminal: false, add_new: true};
1447 1447 $.extend(default_options, options);
1448 1448 var that = this;
1449 1449 var cell = that.get_selected_cell();
1450 1450 var cell_index = that.find_cell_index(cell);
1451 1451 if (cell instanceof IPython.CodeCell) {
1452 1452 cell.execute();
1453 1453 }
1454 1454 if (default_options.terminal) {
1455 1455 cell.select_all();
1456 1456 } else {
1457 1457 if ((cell_index === (that.ncells()-1)) && default_options.add_new) {
1458 1458 that.insert_cell_below('code');
1459 1459 // If we are adding a new cell at the end, scroll down to show it.
1460 1460 that.scroll_to_bottom();
1461 1461 } else {
1462 1462 that.select(cell_index+1);
1463 1463 };
1464 1464 };
1465 1465 this.set_dirty(true);
1466 1466 };
1467 1467
1468 1468 /**
1469 1469 * Execute all cells below the selected cell.
1470 1470 *
1471 1471 * @method execute_cells_below
1472 1472 */
1473 1473 Notebook.prototype.execute_cells_below = function () {
1474 1474 this.execute_cell_range(this.get_selected_index(), this.ncells());
1475 1475 this.scroll_to_bottom();
1476 1476 };
1477 1477
1478 1478 /**
1479 1479 * Execute all cells above the selected cell.
1480 1480 *
1481 1481 * @method execute_cells_above
1482 1482 */
1483 1483 Notebook.prototype.execute_cells_above = function () {
1484 1484 this.execute_cell_range(0, this.get_selected_index());
1485 1485 };
1486 1486
1487 1487 /**
1488 1488 * Execute all cells.
1489 1489 *
1490 1490 * @method execute_all_cells
1491 1491 */
1492 1492 Notebook.prototype.execute_all_cells = function () {
1493 1493 this.execute_cell_range(0, this.ncells());
1494 1494 this.scroll_to_bottom();
1495 1495 };
1496 1496
1497 1497 /**
1498 1498 * Execute a contiguous range of cells.
1499 1499 *
1500 1500 * @method execute_cell_range
1501 1501 * @param {Number} start Index of the first cell to execute (inclusive)
1502 1502 * @param {Number} end Index of the last cell to execute (exclusive)
1503 1503 */
1504 1504 Notebook.prototype.execute_cell_range = function (start, end) {
1505 1505 for (var i=start; i<end; i++) {
1506 1506 this.select(i);
1507 1507 this.execute_selected_cell({add_new:false});
1508 1508 };
1509 1509 };
1510 1510
1511 1511 // Persistance and loading
1512 1512
1513 1513 /**
1514 1514 * Getter method for this notebook's name.
1515 1515 *
1516 1516 * @method get_notebook_name
1517 1517 * @return {String} This notebook's name
1518 1518 */
1519 1519 Notebook.prototype.get_notebook_name = function () {
1520 1520 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1521 1521 return nbname;
1522 1522 };
1523 1523
1524 1524 /**
1525 1525 * Setter method for this notebook's name.
1526 1526 *
1527 1527 * @method set_notebook_name
1528 1528 * @param {String} name A new name for this notebook
1529 1529 */
1530 1530 Notebook.prototype.set_notebook_name = function (name) {
1531 1531 this.notebook_name = name;
1532 1532 };
1533 1533
1534 1534 /**
1535 1535 * Check that a notebook's name is valid.
1536 1536 *
1537 1537 * @method test_notebook_name
1538 1538 * @param {String} nbname A name for this notebook
1539 1539 * @return {Boolean} True if the name is valid, false if invalid
1540 1540 */
1541 1541 Notebook.prototype.test_notebook_name = function (nbname) {
1542 1542 nbname = nbname || '';
1543 1543 if (this.notebook_name_blacklist_re.test(nbname) == false && nbname.length>0) {
1544 1544 return true;
1545 1545 } else {
1546 1546 return false;
1547 1547 };
1548 1548 };
1549 1549
1550 1550 /**
1551 1551 * Load a notebook from JSON (.ipynb).
1552 1552 *
1553 1553 * This currently handles one worksheet: others are deleted.
1554 1554 *
1555 1555 * @method fromJSON
1556 1556 * @param {Object} data JSON representation of a notebook
1557 1557 */
1558 1558 Notebook.prototype.fromJSON = function (data) {
1559 1559 var content = data.content;
1560 1560 var ncells = this.ncells();
1561 1561 var i;
1562 1562 for (i=0; i<ncells; i++) {
1563 1563 // Always delete cell 0 as they get renumbered as they are deleted.
1564 1564 this.delete_cell(0);
1565 1565 };
1566 1566 // Save the metadata and name.
1567 1567 this.metadata = content.metadata;
1568 1568 this.notebook_name = data.name;
1569 1569 // Only handle 1 worksheet for now.
1570 1570 var worksheet = content.worksheets[0];
1571 1571 if (worksheet !== undefined) {
1572 1572 if (worksheet.metadata) {
1573 1573 this.worksheet_metadata = worksheet.metadata;
1574 1574 }
1575 1575 var new_cells = worksheet.cells;
1576 1576 ncells = new_cells.length;
1577 1577 var cell_data = null;
1578 1578 var new_cell = null;
1579 1579 for (i=0; i<ncells; i++) {
1580 1580 cell_data = new_cells[i];
1581 1581 // VERSIONHACK: plaintext -> raw
1582 1582 // handle never-released plaintext name for raw cells
1583 1583 if (cell_data.cell_type === 'plaintext'){
1584 1584 cell_data.cell_type = 'raw';
1585 1585 }
1586 1586
1587 1587 new_cell = this.insert_cell_at_bottom(cell_data.cell_type);
1588 1588 new_cell.fromJSON(cell_data);
1589 1589 };
1590 1590 };
1591 1591 if (content.worksheets.length > 1) {
1592 1592 IPython.dialog.modal({
1593 1593 title : "Multiple worksheets",
1594 1594 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1595 1595 "but this version of IPython can only handle the first. " +
1596 1596 "If you save this notebook, worksheets after the first will be lost.",
1597 1597 buttons : {
1598 1598 OK : {
1599 1599 class : "btn-danger"
1600 1600 }
1601 1601 }
1602 1602 });
1603 1603 }
1604 1604 };
1605 1605
1606 1606 /**
1607 1607 * Dump this notebook into a JSON-friendly object.
1608 1608 *
1609 1609 * @method toJSON
1610 1610 * @return {Object} A JSON-friendly representation of this notebook.
1611 1611 */
1612 1612 Notebook.prototype.toJSON = function () {
1613 1613 var cells = this.get_cells();
1614 1614 var ncells = cells.length;
1615 1615 var cell_array = new Array(ncells);
1616 1616 for (var i=0; i<ncells; i++) {
1617 1617 cell_array[i] = cells[i].toJSON();
1618 1618 };
1619 1619 var data = {
1620 1620 // Only handle 1 worksheet for now.
1621 1621 worksheets : [{
1622 1622 cells: cell_array,
1623 1623 metadata: this.worksheet_metadata
1624 1624 }],
1625 1625 metadata : this.metadata
1626 1626 };
1627 1627 return data;
1628 1628 };
1629 1629
1630 1630 /**
1631 1631 * Start an autosave timer, for periodically saving the notebook.
1632 1632 *
1633 1633 * @method set_autosave_interval
1634 1634 * @param {Integer} interval the autosave interval in milliseconds
1635 1635 */
1636 1636 Notebook.prototype.set_autosave_interval = function (interval) {
1637 1637 var that = this;
1638 1638 // clear previous interval, so we don't get simultaneous timers
1639 1639 if (this.autosave_timer) {
1640 1640 clearInterval(this.autosave_timer);
1641 1641 }
1642 1642
1643 1643 this.autosave_interval = this.minimum_autosave_interval = interval;
1644 1644 if (interval) {
1645 1645 this.autosave_timer = setInterval(function() {
1646 1646 if (that.dirty) {
1647 1647 that.save_notebook();
1648 1648 }
1649 1649 }, interval);
1650 1650 $([IPython.events]).trigger("autosave_enabled.Notebook", interval);
1651 1651 } else {
1652 1652 this.autosave_timer = null;
1653 1653 $([IPython.events]).trigger("autosave_disabled.Notebook");
1654 1654 };
1655 1655 };
1656 1656
1657 1657 /**
1658 1658 * Save this notebook on the server.
1659 1659 *
1660 1660 * @method save_notebook
1661 1661 */
1662 1662 Notebook.prototype.save_notebook = function (extra_settings) {
1663 1663 // Create a JSON model to be sent to the server.
1664 1664 var model = {};
1665 1665 model.name = this.notebook_name;
1666 1666 model.path = this.notebook_path;
1667 1667 model.content = this.toJSON();
1668 1668 model.content.nbformat = this.nbformat;
1669 1669 model.content.nbformat_minor = this.nbformat_minor;
1670 1670 // time the ajax call for autosave tuning purposes.
1671 1671 var start = new Date().getTime();
1672 1672 // We do the call with settings so we can set cache to false.
1673 1673 var settings = {
1674 1674 processData : false,
1675 1675 cache : false,
1676 1676 type : "PUT",
1677 1677 data : JSON.stringify(model),
1678 1678 headers : {'Content-Type': 'application/json'},
1679 1679 success : $.proxy(this.save_notebook_success, this, start),
1680 1680 error : $.proxy(this.save_notebook_error, this)
1681 1681 };
1682 1682 if (extra_settings) {
1683 1683 for (var key in extra_settings) {
1684 1684 settings[key] = extra_settings[key];
1685 1685 }
1686 1686 }
1687 1687 $([IPython.events]).trigger('notebook_saving.Notebook');
1688 1688 var url = utils.url_join_encode(
1689 1689 this._baseProjectUrl,
1690 1690 'api/notebooks',
1691 1691 this.notebook_path,
1692 1692 this.notebook_name
1693 1693 );
1694 1694 $.ajax(url, settings);
1695 1695 };
1696 1696
1697 1697 /**
1698 1698 * Success callback for saving a notebook.
1699 1699 *
1700 1700 * @method save_notebook_success
1701 1701 * @param {Integer} start the time when the save request started
1702 1702 * @param {Object} data JSON representation of a notebook
1703 1703 * @param {String} status Description of response status
1704 1704 * @param {jqXHR} xhr jQuery Ajax object
1705 1705 */
1706 1706 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1707 1707 this.set_dirty(false);
1708 1708 $([IPython.events]).trigger('notebook_saved.Notebook');
1709 1709 this._update_autosave_interval(start);
1710 1710 if (this._checkpoint_after_save) {
1711 1711 this.create_checkpoint();
1712 1712 this._checkpoint_after_save = false;
1713 1713 };
1714 1714 };
1715 1715
1716 1716 /**
1717 1717 * update the autosave interval based on how long the last save took
1718 1718 *
1719 1719 * @method _update_autosave_interval
1720 1720 * @param {Integer} timestamp when the save request started
1721 1721 */
1722 1722 Notebook.prototype._update_autosave_interval = function (start) {
1723 1723 var duration = (new Date().getTime() - start);
1724 1724 if (this.autosave_interval) {
1725 1725 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1726 1726 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1727 1727 // round to 10 seconds, otherwise we will be setting a new interval too often
1728 1728 interval = 10000 * Math.round(interval / 10000);
1729 1729 // set new interval, if it's changed
1730 1730 if (interval != this.autosave_interval) {
1731 1731 this.set_autosave_interval(interval);
1732 1732 }
1733 1733 }
1734 1734 };
1735 1735
1736 1736 /**
1737 1737 * Failure callback for saving a notebook.
1738 1738 *
1739 1739 * @method save_notebook_error
1740 1740 * @param {jqXHR} xhr jQuery Ajax object
1741 1741 * @param {String} status Description of response status
1742 1742 * @param {String} error HTTP error message
1743 1743 */
1744 1744 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1745 1745 $([IPython.events]).trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1746 1746 };
1747 1747
1748 1748 Notebook.prototype.new_notebook = function(){
1749 1749 var path = this.notebook_path;
1750 1750 var base_project_url = this._baseProjectUrl;
1751 1751 var settings = {
1752 1752 processData : false,
1753 1753 cache : false,
1754 1754 type : "POST",
1755 1755 dataType : "json",
1756 1756 async : false,
1757 1757 success : function (data, status, xhr){
1758 1758 var notebook_name = data.name;
1759 1759 window.open(
1760 1760 utils.url_join_encode(
1761 1761 base_project_url,
1762 1762 'notebooks',
1763 1763 path,
1764 1764 notebook_name
1765 1765 ),
1766 1766 '_blank'
1767 1767 );
1768 1768 }
1769 1769 };
1770 1770 var url = utils.url_join_encode(
1771 1771 base_project_url,
1772 1772 'api/notebooks',
1773 1773 path
1774 1774 );
1775 1775 $.ajax(url,settings);
1776 1776 };
1777 1777
1778 1778
1779 1779 Notebook.prototype.copy_notebook = function(){
1780 1780 var path = this.notebook_path;
1781 1781 var base_project_url = this._baseProjectUrl;
1782 1782 var settings = {
1783 1783 processData : false,
1784 1784 cache : false,
1785 1785 type : "POST",
1786 1786 dataType : "json",
1787 1787 data : JSON.stringify({copy_from : this.notebook_name}),
1788 1788 async : false,
1789 1789 success : function (data, status, xhr) {
1790 1790 window.open(utils.url_join_encode(
1791 1791 base_project_url,
1792 1792 'notebooks',
1793 1793 data.path,
1794 1794 data.name
1795 1795 ), '_blank');
1796 1796 }
1797 1797 };
1798 1798 var url = utils.url_join_encode(
1799 1799 base_project_url,
1800 1800 'api/notebooks',
1801 1801 path
1802 1802 );
1803 1803 $.ajax(url,settings);
1804 1804 };
1805 1805
1806 1806 Notebook.prototype.rename = function (nbname) {
1807 1807 var that = this;
1808 1808 var data = {name: nbname + '.ipynb'};
1809 1809 var settings = {
1810 1810 processData : false,
1811 1811 cache : false,
1812 1812 type : "PATCH",
1813 1813 data : JSON.stringify(data),
1814 1814 dataType: "json",
1815 1815 headers : {'Content-Type': 'application/json'},
1816 1816 success : $.proxy(that.rename_success, this),
1817 1817 error : $.proxy(that.rename_error, this)
1818 1818 };
1819 1819 $([IPython.events]).trigger('rename_notebook.Notebook', data);
1820 1820 var url = utils.url_join_encode(
1821 1821 this._baseProjectUrl,
1822 1822 'api/notebooks',
1823 1823 this.notebook_path,
1824 1824 this.notebook_name
1825 1825 );
1826 1826 $.ajax(url, settings);
1827 1827 };
1828 1828
1829 1829
1830 1830 Notebook.prototype.rename_success = function (json, status, xhr) {
1831 1831 this.notebook_name = json.name;
1832 1832 var name = this.notebook_name;
1833 1833 var path = json.path;
1834 1834 this.session.rename_notebook(name, path);
1835 1835 $([IPython.events]).trigger('notebook_renamed.Notebook', json);
1836 1836 }
1837 1837
1838 1838 Notebook.prototype.rename_error = function (xhr, status, error) {
1839 1839 var that = this;
1840 1840 var dialog = $('<div/>').append(
1841 1841 $("<p/>").addClass("rename-message")
1842 1842 .html('This notebook name already exists.')
1843 1843 )
1844 1844 $([IPython.events]).trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
1845 1845 IPython.dialog.modal({
1846 1846 title: "Notebook Rename Error!",
1847 1847 body: dialog,
1848 1848 buttons : {
1849 1849 "Cancel": {},
1850 1850 "OK": {
1851 1851 class: "btn-primary",
1852 1852 click: function () {
1853 1853 IPython.save_widget.rename_notebook();
1854 1854 }}
1855 1855 },
1856 1856 open : function (event, ui) {
1857 1857 var that = $(this);
1858 1858 // Upon ENTER, click the OK button.
1859 1859 that.find('input[type="text"]').keydown(function (event, ui) {
1860 1860 if (event.which === utils.keycodes.ENTER) {
1861 1861 that.find('.btn-primary').first().click();
1862 1862 }
1863 1863 });
1864 1864 that.find('input[type="text"]').focus();
1865 1865 }
1866 1866 });
1867 1867 }
1868 1868
1869 1869 /**
1870 1870 * Request a notebook's data from the server.
1871 1871 *
1872 1872 * @method load_notebook
1873 1873 * @param {String} notebook_name and path A notebook to load
1874 1874 */
1875 1875 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
1876 1876 var that = this;
1877 1877 this.notebook_name = notebook_name;
1878 1878 this.notebook_path = notebook_path;
1879 1879 // We do the call with settings so we can set cache to false.
1880 1880 var settings = {
1881 1881 processData : false,
1882 1882 cache : false,
1883 1883 type : "GET",
1884 1884 dataType : "json",
1885 1885 success : $.proxy(this.load_notebook_success,this),
1886 1886 error : $.proxy(this.load_notebook_error,this),
1887 1887 };
1888 1888 $([IPython.events]).trigger('notebook_loading.Notebook');
1889 1889 var url = utils.url_join_encode(
1890 1890 this._baseProjectUrl,
1891 1891 'api/notebooks',
1892 1892 this.notebook_path,
1893 1893 this.notebook_name
1894 1894 );
1895 1895 $.ajax(url, settings);
1896 1896 };
1897 1897
1898 1898 /**
1899 1899 * Success callback for loading a notebook from the server.
1900 1900 *
1901 1901 * Load notebook data from the JSON response.
1902 1902 *
1903 1903 * @method load_notebook_success
1904 1904 * @param {Object} data JSON representation of a notebook
1905 1905 * @param {String} status Description of response status
1906 1906 * @param {jqXHR} xhr jQuery Ajax object
1907 1907 */
1908 1908 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
1909 1909 this.fromJSON(data);
1910 1910 if (this.ncells() === 0) {
1911 1911 this.insert_cell_below('code');
1912 1912 };
1913 1913 this.set_dirty(false);
1914 1914 this.select(0);
1915 1915 this.scroll_to_top();
1916 1916 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
1917 1917 var msg = "This notebook has been converted from an older " +
1918 1918 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
1919 1919 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
1920 1920 "newer notebook format will be used and older versions of IPython " +
1921 1921 "may not be able to read it. To keep the older version, close the " +
1922 1922 "notebook without saving it.";
1923 1923 IPython.dialog.modal({
1924 1924 title : "Notebook converted",
1925 1925 body : msg,
1926 1926 buttons : {
1927 1927 OK : {
1928 1928 class : "btn-primary"
1929 1929 }
1930 1930 }
1931 1931 });
1932 1932 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
1933 1933 var that = this;
1934 1934 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
1935 1935 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
1936 1936 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
1937 1937 this_vs + ". You can still work with this notebook, but some features " +
1938 1938 "introduced in later notebook versions may not be available."
1939 1939
1940 1940 IPython.dialog.modal({
1941 1941 title : "Newer Notebook",
1942 1942 body : msg,
1943 1943 buttons : {
1944 1944 OK : {
1945 1945 class : "btn-danger"
1946 1946 }
1947 1947 }
1948 1948 });
1949 1949
1950 1950 }
1951 1951
1952 1952 // Create the session after the notebook is completely loaded to prevent
1953 1953 // code execution upon loading, which is a security risk.
1954 1954 if (this.session == null) {
1955 1955 this.start_session();
1956 1956 }
1957 1957 // load our checkpoint list
1958 1958 this.list_checkpoints();
1959 1959
1960 1960 // load toolbar state
1961 1961 if (this.metadata.celltoolbar) {
1962 1962 IPython.CellToolbar.global_show();
1963 1963 IPython.CellToolbar.activate_preset(this.metadata.celltoolbar);
1964 1964 }
1965 1965
1966 1966 $([IPython.events]).trigger('notebook_loaded.Notebook');
1967 1967 };
1968 1968
1969 1969 /**
1970 1970 * Failure callback for loading a notebook from the server.
1971 1971 *
1972 1972 * @method load_notebook_error
1973 1973 * @param {jqXHR} xhr jQuery Ajax object
1974 1974 * @param {String} status Description of response status
1975 1975 * @param {String} error HTTP error message
1976 1976 */
1977 1977 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
1978 1978 $([IPython.events]).trigger('notebook_load_failed.Notebook', [xhr, status, error]);
1979 1979 if (xhr.status === 400) {
1980 1980 var msg = error;
1981 1981 } else if (xhr.status === 500) {
1982 1982 var msg = "An unknown error occurred while loading this notebook. " +
1983 1983 "This version can load notebook formats " +
1984 1984 "v" + this.nbformat + " or earlier.";
1985 1985 }
1986 1986 IPython.dialog.modal({
1987 1987 title: "Error loading notebook",
1988 1988 body : msg,
1989 1989 buttons : {
1990 1990 "OK": {}
1991 1991 }
1992 1992 });
1993 1993 }
1994 1994
1995 1995 /********************* checkpoint-related *********************/
1996 1996
1997 1997 /**
1998 1998 * Save the notebook then immediately create a checkpoint.
1999 1999 *
2000 2000 * @method save_checkpoint
2001 2001 */
2002 2002 Notebook.prototype.save_checkpoint = function () {
2003 2003 this._checkpoint_after_save = true;
2004 2004 this.save_notebook();
2005 2005 };
2006 2006
2007 2007 /**
2008 2008 * Add a checkpoint for this notebook.
2009 2009 * for use as a callback from checkpoint creation.
2010 2010 *
2011 2011 * @method add_checkpoint
2012 2012 */
2013 2013 Notebook.prototype.add_checkpoint = function (checkpoint) {
2014 2014 var found = false;
2015 2015 for (var i = 0; i < this.checkpoints.length; i++) {
2016 2016 var existing = this.checkpoints[i];
2017 2017 if (existing.id == checkpoint.id) {
2018 2018 found = true;
2019 2019 this.checkpoints[i] = checkpoint;
2020 2020 break;
2021 2021 }
2022 2022 }
2023 2023 if (!found) {
2024 2024 this.checkpoints.push(checkpoint);
2025 2025 }
2026 2026 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2027 2027 };
2028 2028
2029 2029 /**
2030 2030 * List checkpoints for this notebook.
2031 2031 *
2032 2032 * @method list_checkpoints
2033 2033 */
2034 2034 Notebook.prototype.list_checkpoints = function () {
2035 2035 var url = utils.url_join_encode(
2036 2036 this._baseProjectUrl,
2037 2037 'api/notebooks',
2038 2038 this.notebook_path,
2039 2039 this.notebook_name,
2040 2040 'checkpoints'
2041 2041 );
2042 2042 $.get(url).done(
2043 2043 $.proxy(this.list_checkpoints_success, this)
2044 2044 ).fail(
2045 2045 $.proxy(this.list_checkpoints_error, this)
2046 2046 );
2047 2047 };
2048 2048
2049 2049 /**
2050 2050 * Success callback for listing checkpoints.
2051 2051 *
2052 2052 * @method list_checkpoint_success
2053 2053 * @param {Object} data JSON representation of a checkpoint
2054 2054 * @param {String} status Description of response status
2055 2055 * @param {jqXHR} xhr jQuery Ajax object
2056 2056 */
2057 2057 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2058 2058 var data = $.parseJSON(data);
2059 2059 this.checkpoints = data;
2060 2060 if (data.length) {
2061 2061 this.last_checkpoint = data[data.length - 1];
2062 2062 } else {
2063 2063 this.last_checkpoint = null;
2064 2064 }
2065 2065 $([IPython.events]).trigger('checkpoints_listed.Notebook', [data]);
2066 2066 };
2067 2067
2068 2068 /**
2069 2069 * Failure callback for listing a checkpoint.
2070 2070 *
2071 2071 * @method list_checkpoint_error
2072 2072 * @param {jqXHR} xhr jQuery Ajax object
2073 2073 * @param {String} status Description of response status
2074 2074 * @param {String} error_msg HTTP error message
2075 2075 */
2076 2076 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2077 2077 $([IPython.events]).trigger('list_checkpoints_failed.Notebook');
2078 2078 };
2079 2079
2080 2080 /**
2081 2081 * Create a checkpoint of this notebook on the server from the most recent save.
2082 2082 *
2083 2083 * @method create_checkpoint
2084 2084 */
2085 2085 Notebook.prototype.create_checkpoint = function () {
2086 2086 var url = utils.url_join_encode(
2087 2087 this._baseProjectUrl,
2088 2088 'api/notebooks',
2089 2089 this.notebookPath(),
2090 2090 this.notebook_name,
2091 2091 'checkpoints'
2092 2092 );
2093 2093 $.post(url).done(
2094 2094 $.proxy(this.create_checkpoint_success, this)
2095 2095 ).fail(
2096 2096 $.proxy(this.create_checkpoint_error, this)
2097 2097 );
2098 2098 };
2099 2099
2100 2100 /**
2101 2101 * Success callback for creating a checkpoint.
2102 2102 *
2103 2103 * @method create_checkpoint_success
2104 2104 * @param {Object} data JSON representation of a checkpoint
2105 2105 * @param {String} status Description of response status
2106 2106 * @param {jqXHR} xhr jQuery Ajax object
2107 2107 */
2108 2108 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2109 2109 var data = $.parseJSON(data);
2110 2110 this.add_checkpoint(data);
2111 2111 $([IPython.events]).trigger('checkpoint_created.Notebook', data);
2112 2112 };
2113 2113
2114 2114 /**
2115 2115 * Failure callback for creating a checkpoint.
2116 2116 *
2117 2117 * @method create_checkpoint_error
2118 2118 * @param {jqXHR} xhr jQuery Ajax object
2119 2119 * @param {String} status Description of response status
2120 2120 * @param {String} error_msg HTTP error message
2121 2121 */
2122 2122 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2123 2123 $([IPython.events]).trigger('checkpoint_failed.Notebook');
2124 2124 };
2125 2125
2126 2126 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2127 2127 var that = this;
2128 2128 var checkpoint = checkpoint || this.last_checkpoint;
2129 2129 if ( ! checkpoint ) {
2130 2130 console.log("restore dialog, but no checkpoint to restore to!");
2131 2131 return;
2132 2132 }
2133 2133 var body = $('<div/>').append(
2134 2134 $('<p/>').addClass("p-space").text(
2135 2135 "Are you sure you want to revert the notebook to " +
2136 2136 "the latest checkpoint?"
2137 2137 ).append(
2138 2138 $("<strong/>").text(
2139 2139 " This cannot be undone."
2140 2140 )
2141 2141 )
2142 2142 ).append(
2143 2143 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2144 2144 ).append(
2145 2145 $('<p/>').addClass("p-space").text(
2146 2146 Date(checkpoint.last_modified)
2147 2147 ).css("text-align", "center")
2148 2148 );
2149 2149
2150 2150 IPython.dialog.modal({
2151 2151 title : "Revert notebook to checkpoint",
2152 2152 body : body,
2153 2153 buttons : {
2154 2154 Revert : {
2155 2155 class : "btn-danger",
2156 2156 click : function () {
2157 2157 that.restore_checkpoint(checkpoint.id);
2158 2158 }
2159 2159 },
2160 2160 Cancel : {}
2161 2161 }
2162 2162 });
2163 2163 }
2164 2164
2165 2165 /**
2166 2166 * Restore the notebook to a checkpoint state.
2167 2167 *
2168 2168 * @method restore_checkpoint
2169 2169 * @param {String} checkpoint ID
2170 2170 */
2171 2171 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2172 2172 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2173 2173 var url = utils.url_join_encode(
2174 2174 this._baseProjectUrl,
2175 2175 'api/notebooks',
2176 2176 this.notebookPath(),
2177 2177 this.notebook_name,
2178 2178 'checkpoints',
2179 2179 checkpoint
2180 2180 );
2181 2181 $.post(url).done(
2182 2182 $.proxy(this.restore_checkpoint_success, this)
2183 2183 ).fail(
2184 2184 $.proxy(this.restore_checkpoint_error, this)
2185 2185 );
2186 2186 };
2187 2187
2188 2188 /**
2189 2189 * Success callback for restoring a notebook to a checkpoint.
2190 2190 *
2191 2191 * @method restore_checkpoint_success
2192 2192 * @param {Object} data (ignored, should be empty)
2193 2193 * @param {String} status Description of response status
2194 2194 * @param {jqXHR} xhr jQuery Ajax object
2195 2195 */
2196 2196 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2197 2197 $([IPython.events]).trigger('checkpoint_restored.Notebook');
2198 2198 this.load_notebook(this.notebook_name, this.notebook_path);
2199 2199 };
2200 2200
2201 2201 /**
2202 2202 * Failure callback for restoring a notebook to a checkpoint.
2203 2203 *
2204 2204 * @method restore_checkpoint_error
2205 2205 * @param {jqXHR} xhr jQuery Ajax object
2206 2206 * @param {String} status Description of response status
2207 2207 * @param {String} error_msg HTTP error message
2208 2208 */
2209 2209 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2210 2210 $([IPython.events]).trigger('checkpoint_restore_failed.Notebook');
2211 2211 };
2212 2212
2213 2213 /**
2214 2214 * Delete a notebook checkpoint.
2215 2215 *
2216 2216 * @method delete_checkpoint
2217 2217 * @param {String} checkpoint ID
2218 2218 */
2219 2219 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2220 2220 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2221 2221 var url = utils.url_join_encode(
2222 2222 this._baseProjectUrl,
2223 2223 'api/notebooks',
2224 2224 this.notebookPath(),
2225 2225 this.notebook_name,
2226 2226 'checkpoints',
2227 2227 checkpoint
2228 2228 );
2229 2229 $.ajax(url, {
2230 2230 type: 'DELETE',
2231 2231 success: $.proxy(this.delete_checkpoint_success, this),
2232 2232 error: $.proxy(this.delete_notebook_error,this)
2233 2233 });
2234 2234 };
2235 2235
2236 2236 /**
2237 2237 * Success callback for deleting a notebook checkpoint
2238 2238 *
2239 2239 * @method delete_checkpoint_success
2240 2240 * @param {Object} data (ignored, should be empty)
2241 2241 * @param {String} status Description of response status
2242 2242 * @param {jqXHR} xhr jQuery Ajax object
2243 2243 */
2244 2244 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2245 2245 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2246 2246 this.load_notebook(this.notebook_name, this.notebook_path);
2247 2247 };
2248 2248
2249 2249 /**
2250 2250 * Failure callback for deleting a notebook checkpoint.
2251 2251 *
2252 2252 * @method delete_checkpoint_error
2253 2253 * @param {jqXHR} xhr jQuery Ajax object
2254 2254 * @param {String} status Description of response status
2255 2255 * @param {String} error_msg HTTP error message
2256 2256 */
2257 2257 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2258 2258 $([IPython.events]).trigger('checkpoint_delete_failed.Notebook');
2259 2259 };
2260 2260
2261 2261
2262 2262 IPython.Notebook = Notebook;
2263 2263
2264 2264
2265 2265 return IPython;
2266 2266
2267 2267 }(IPython));
@@ -1,580 +1,583
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2012 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // TextCell
10 10 //============================================================================
11 11
12 12
13 13
14 14 /**
15 15 A module that allow to create different type of Text Cell
16 16 @module IPython
17 17 @namespace IPython
18 18 */
19 19 var IPython = (function (IPython) {
20 20 "use strict";
21 21
22 22 // TextCell base class
23 23 var key = IPython.utils.keycodes;
24 24
25 25 /**
26 26 * Construct a new TextCell, codemirror mode is by default 'htmlmixed', and cell type is 'text'
27 27 * cell start as not redered.
28 28 *
29 29 * @class TextCell
30 30 * @constructor TextCell
31 31 * @extend IPython.Cell
32 32 * @param {object|undefined} [options]
33 33 * @param [options.cm_config] {object} config to pass to CodeMirror, will extend/overwrite default config
34 34 * @param [options.placeholder] {string} default string to use when souce in empty for rendering (only use in some TextCell subclass)
35 35 */
36 36 var TextCell = function (options) {
37 37 // in all TextCell/Cell subclasses
38 38 // do not assign most of members here, just pass it down
39 39 // in the options dict potentially overwriting what you wish.
40 40 // they will be assigned in the base class.
41 41
42 42 // we cannot put this as a class key as it has handle to "this".
43 43 var cm_overwrite_options = {
44 44 onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this)
45 45 };
46 46
47 47 options = this.mergeopt(TextCell,options,{cm_config:cm_overwrite_options});
48 48
49 49 this.cell_type = this.cell_type || 'text';
50 50
51 51 IPython.Cell.apply(this, [options]);
52 52
53 53 this.rendered = false;
54 54 };
55 55
56 56 TextCell.prototype = new IPython.Cell();
57 57
58 58 TextCell.options_default = {
59 59 cm_config : {
60 60 extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
61 61 mode: 'htmlmixed',
62 62 lineWrapping : true,
63 63 }
64 64 };
65 65
66 66
67 67
68 68 /**
69 69 * Create the DOM element of the TextCell
70 70 * @method create_element
71 71 * @private
72 72 */
73 73 TextCell.prototype.create_element = function () {
74 74 IPython.Cell.prototype.create_element.apply(this, arguments);
75 75
76 76 var cell = $("<div>").addClass('cell text_cell border-box-sizing');
77 77 cell.attr('tabindex','2');
78 78
79 79 var prompt = $('<div/>').addClass('prompt input_prompt');
80 80 cell.append(prompt);
81 81 var inner_cell = $('<div/>').addClass('inner_cell');
82 82 this.celltoolbar = new IPython.CellToolbar(this);
83 83 inner_cell.append(this.celltoolbar.element);
84 84 var input_area = $('<div/>').addClass('text_cell_input border-box-sizing');
85 85 this.code_mirror = CodeMirror(input_area.get(0), this.cm_config);
86 86 // The tabindex=-1 makes this div focusable.
87 87 var render_area = $('<div/>').addClass('text_cell_render border-box-sizing').
88 88 addClass('rendered_html').attr('tabindex','-1');
89 89 inner_cell.append(input_area).append(render_area);
90 90 cell.append(inner_cell);
91 91 this.element = cell;
92 92 };
93 93
94 94
95 95 /**
96 96 * Bind the DOM evet to cell actions
97 97 * Need to be called after TextCell.create_element
98 98 * @private
99 99 * @method bind_event
100 100 */
101 101 TextCell.prototype.bind_events = function () {
102 102 IPython.Cell.prototype.bind_events.apply(this);
103 103 var that = this;
104 104 this.element.keydown(function (event) {
105 105 if (event.which === 13 && !event.shiftKey) {
106 106 if (that.rendered) {
107 that.edit();
107 that.unrender();
108 108 return false;
109 109 };
110 110 };
111 111 });
112 112 this.element.dblclick(function () {
113 that.edit();
113 that.unrender();
114 114 });
115 115 };
116 116
117 117 /**
118 118 * This method gets called in CodeMirror's onKeyDown/onKeyPress
119 119 * handlers and is used to provide custom key handling.
120 120 *
121 121 * Subclass should override this method to have custom handeling
122 122 *
123 123 * @method handle_codemirror_keyevent
124 124 * @param {CodeMirror} editor - The codemirror instance bound to the cell
125 125 * @param {event} event -
126 126 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
127 127 */
128 128 TextCell.prototype.handle_codemirror_keyevent = function (editor, event) {
129 129
130 130 if (event.keyCode === 13 && (event.shiftKey || event.ctrlKey)) {
131 131 // Always ignore shift-enter in CodeMirror as we handle it.
132 132 return true;
133 133 }
134 134 return false;
135 135 };
136 136
137 /**
138 * Select the current cell and trigger 'focus'
139 * @method select
140 */
137 // Cell level actions
138
141 139 TextCell.prototype.select = function () {
142 IPython.Cell.prototype.select.apply(this);
143 var output = this.element.find("div.text_cell_render");
144 output.trigger('focus');
140 var continue = IPython.Cell.prototype.select.apply(this);
141 if (continue) {
142 if (this.mode === 'edit') {
143 this.code_mirror.refresh();
144 }
145 };
146 return continue;
145 147 };
146 148
147 /**
148 * unselect the current cell and `render` it
149 * @method unselect
150 */
151 TextCell.prototype.unselect = function() {
152 // render on selection of another cell
153 this.render();
154 IPython.Cell.prototype.unselect.apply(this);
149 TextCell.prototype.render = function () {
150 var continue = IPython.Cell.prototype.render.apply(this);
151 if (continue) {
152 this.execute();
153 };
154 return continue;
155 155 };
156 156
157 /**
158 *
159 * put the current cell in edition mode
160 * @method edit
161 */
162 TextCell.prototype.edit = function () {
163 if (this.rendered === true) {
157 TextCell.prototype.unrender = function () {
158 if (this.read_only) return;
159 var continue = IPython.Cell.prototype.unrender.apply(this);
160 if (continue) {
164 161 var text_cell = this.element;
165 162 var output = text_cell.find("div.text_cell_render");
166 163 output.hide();
167 164 text_cell.find('div.text_cell_input').show();
168 this.code_mirror.refresh();
169 this.code_mirror.focus();
170 // We used to need an additional refresh() after the focus, but
171 // it appears that this has been fixed in CM. This bug would show
172 // up on FF when a newly loaded markdown cell was edited.
173 this.rendered = false;
165 this.focus_editor();
174 166 if (this.get_text() === this.placeholder) {
175 167 this.set_text('');
176 168 this.refresh();
177 169 }
178 }
179 };
180 170
171 };
172 return continue;
173 };
181 174
182 /**
183 * Empty, Subclasses must define render.
184 * @method render
185 */
186 TextCell.prototype.render = function () {};
175 TextCell.prototype.command_mode = function () {
176 var continue = IPython.Cell.prototype.command_mode.apply(this);
177 if (continue) {
178 this.focus_cell();
179 };
180 return continue;
181 }
187 182
183 TextCell.prototype.edit_mode = function () {
184 var continue = IPython.Cell.prototype.edit_mode.apply(this);
185 if (continue) {
186 this.focus_editor();
187 };
188 return continue;
189 }
188 190
189 191 /**
190 192 * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
191 193 * @method get_text
192 194 * @retrun {string} CodeMirror current text value
193 195 */
194 196 TextCell.prototype.get_text = function() {
195 197 return this.code_mirror.getValue();
196 198 };
197 199
198 200 /**
199 201 * @param {string} text - Codemiror text value
200 202 * @see TextCell#get_text
201 203 * @method set_text
202 204 * */
203 205 TextCell.prototype.set_text = function(text) {
204 206 this.code_mirror.setValue(text);
205 207 this.code_mirror.refresh();
206 208 };
207 209
208 210 /**
209 211 * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
210 212 * @method get_rendered
211 213 * @return {html} html of rendered element
212 214 * */
213 215 TextCell.prototype.get_rendered = function() {
214 216 return this.element.find('div.text_cell_render').html();
215 217 };
216 218
217 219 /**
218 220 * @method set_rendered
219 221 */
220 222 TextCell.prototype.set_rendered = function(text) {
221 223 this.element.find('div.text_cell_render').html(text);
222 224 };
223 225
224 226 /**
225 227 * not deprecated, but implementation wrong
226 228 * @method at_top
227 229 * @deprecated
228 230 * @return {Boolean} true is cell rendered, false otherwise
229 231 * I doubt this is what it is supposed to do
230 232 * this implementation is completly false
231 233 */
232 234 TextCell.prototype.at_top = function () {
233 235 if (this.rendered) {
234 236 return true;
235 237 } else {
236 238 return false;
237 239 }
238 240 };
239 241
240 242
241 243 /**
242 244 * not deprecated, but implementation wrong
243 245 * @method at_bottom
244 246 * @deprecated
245 247 * @return {Boolean} true is cell rendered, false otherwise
246 248 * I doubt this is what it is supposed to do
247 249 * this implementation is completly false
248 250 * */
249 251 TextCell.prototype.at_bottom = function () {
250 252 if (this.rendered) {
251 253 return true;
252 254 } else {
253 255 return false;
254 256 }
255 257 };
256 258
257 259 /**
258 260 * Create Text cell from JSON
259 261 * @param {json} data - JSON serialized text-cell
260 262 * @method fromJSON
261 263 */
262 264 TextCell.prototype.fromJSON = function (data) {
263 265 IPython.Cell.prototype.fromJSON.apply(this, arguments);
264 266 if (data.cell_type === this.cell_type) {
265 267 if (data.source !== undefined) {
266 268 this.set_text(data.source);
267 269 // make this value the starting point, so that we can only undo
268 270 // to this state, instead of a blank cell
269 271 this.code_mirror.clearHistory();
270 272 this.set_rendered(data.rendered || '');
271 273 this.rendered = false;
272 274 this.render();
273 275 }
274 276 }
275 277 };
276 278
277 279 /** Generate JSON from cell
278 280 * @return {object} cell data serialised to json
279 281 */
280 282 TextCell.prototype.toJSON = function () {
281 283 var data = IPython.Cell.prototype.toJSON.apply(this);
282 284 data.source = this.get_text();
283 285 if (data.source == this.placeholder) {
284 286 data.source = "";
285 287 }
286 288 return data;
287 289 };
288 290
289 291
290 292 /**
291 293 * @class MarkdownCell
292 294 * @constructor MarkdownCell
293 295 * @extends IPython.HTMLCell
294 296 */
295 297 var MarkdownCell = function (options) {
296 298 options = this.mergeopt(MarkdownCell, options);
297 299
298 300 this.cell_type = 'markdown';
299 301 TextCell.apply(this, [options]);
300 302 };
301 303
302 304 MarkdownCell.options_default = {
303 305 cm_config: {
304 306 mode: 'gfm'
305 307 },
306 308 placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
307 309 }
308 310
309 311
310 312
311 313
312 314 MarkdownCell.prototype = new TextCell();
313 315
314 316 /**
315 317 * @method render
316 318 */
317 319 MarkdownCell.prototype.render = function () {
318 320 if (this.rendered === false) {
319 321 var text = this.get_text();
320 322 var math = null;
321 323 if (text === "") { text = this.placeholder; }
322 324 var text_and_math = IPython.mathjaxutils.remove_math(text);
323 325 text = text_and_math[0];
324 326 math = text_and_math[1];
325 327 var html = marked.parser(marked.lexer(text));
326 328 html = $(IPython.mathjaxutils.replace_math(html, math));
327 329 // links in markdown cells should open in new tabs
328 330 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
329 331 try {
330 332 this.set_rendered(html);
331 333 } catch (e) {
332 334 console.log("Error running Javascript in Markdown:");
333 335 console.log(e);
334 336 this.set_rendered($("<div/>").addClass("js-error").html(
335 337 "Error rendering Markdown!<br/>" + e.toString())
336 338 );
337 339 }
338 340 this.element.find('div.text_cell_input').hide();
339 341 this.element.find("div.text_cell_render").show();
340 342 this.typeset();
341 343 this.rendered = true;
342 344 }
343 345 };
344 346
345 347
346 348 // RawCell
347 349
348 350 /**
349 351 * @class RawCell
350 352 * @constructor RawCell
351 353 * @extends IPython.TextCell
352 354 */
353 355 var RawCell = function (options) {
354 356 options = this.mergeopt(RawCell, options);
355 357
356 358 this.cell_type = 'raw';
357 359 TextCell.apply(this, [options]);
358 360
359 361 var that = this;
360 362 this.element.focusout(
361 363 function() { that.auto_highlight(); }
362 364 );
363 365 };
364 366
365 367 RawCell.options_default = {
366 368 placeholder : "Write raw LaTeX or other formats here, for use with nbconvert.\n" +
367 369 "It will not be rendered in the notebook.\n" +
368 370 "When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
369 371 };
370 372
371 373
372 374
373 375 RawCell.prototype = new TextCell();
374 376
375 377 /**
376 378 * Trigger autodetection of highlight scheme for current cell
377 379 * @method auto_highlight
378 380 */
379 381 RawCell.prototype.auto_highlight = function () {
380 382 this._auto_highlight(IPython.config.raw_cell_highlight);
381 383 };
382 384
383 385 /** @method render **/
384 386 RawCell.prototype.render = function () {
387
385 388 this.rendered = true;
386 389 var text = this.get_text();
387 390 if (text === "") { text = this.placeholder; }
388 console.log('rendering', text);
389 391 this.set_text(text);
392 this.unrender();
390 393 };
391 394
392 395
393 396 /** @method handle_codemirror_keyevent **/
394 397 RawCell.prototype.handle_codemirror_keyevent = function (editor, event) {
395 398
396 399 var that = this;
397 400 if (event.which === key.UPARROW && event.type === 'keydown') {
398 401 // If we are not at the top, let CM handle the up arrow and
399 402 // prevent the global keydown handler from handling it.
400 403 if (!that.at_top()) {
401 404 event.stop();
402 405 return false;
403 406 } else {
404 407 return true;
405 408 };
406 409 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
407 410 // If we are not at the bottom, let CM handle the down arrow and
408 411 // prevent the global keydown handler from handling it.
409 412 if (!that.at_bottom()) {
410 413 event.stop();
411 414 return false;
412 415 } else {
413 416 return true;
414 417 };
415 418 };
416 419 return false;
417 420 };
418 421
419 422 /** @method select **/
420 423 RawCell.prototype.select = function () {
421 424 IPython.Cell.prototype.select.apply(this);
422 425 this.edit();
423 426 };
424 427
425 428 /** @method at_top **/
426 429 RawCell.prototype.at_top = function () {
427 430 var cursor = this.code_mirror.getCursor();
428 431 if (cursor.line === 0 && cursor.ch === 0) {
429 432 return true;
430 433 } else {
431 434 return false;
432 435 }
433 436 };
434 437
435 438
436 439 /** @method at_bottom **/
437 440 RawCell.prototype.at_bottom = function () {
438 441 var cursor = this.code_mirror.getCursor();
439 442 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
440 443 return true;
441 444 } else {
442 445 return false;
443 446 }
444 447 };
445 448
446 449
447 450 /**
448 451 * @class HeadingCell
449 452 * @extends IPython.TextCell
450 453 */
451 454
452 455 /**
453 456 * @constructor HeadingCell
454 457 * @extends IPython.TextCell
455 458 */
456 459 var HeadingCell = function (options) {
457 460 options = this.mergeopt(HeadingCell, options);
458 461
459 462 this.level = 1;
460 463 this.cell_type = 'heading';
461 464 TextCell.apply(this, [options]);
462 465
463 466 /**
464 467 * heading level of the cell, use getter and setter to access
465 468 * @property level
466 469 */
467 470 };
468 471
469 472 HeadingCell.options_default = {
470 473 placeholder: "Type Heading Here"
471 474 };
472 475
473 476 HeadingCell.prototype = new TextCell();
474 477
475 478 /** @method fromJSON */
476 479 HeadingCell.prototype.fromJSON = function (data) {
477 480 if (data.level != undefined){
478 481 this.level = data.level;
479 482 }
480 483 TextCell.prototype.fromJSON.apply(this, arguments);
481 484 };
482 485
483 486
484 487 /** @method toJSON */
485 488 HeadingCell.prototype.toJSON = function () {
486 489 var data = TextCell.prototype.toJSON.apply(this);
487 490 data.level = this.get_level();
488 491 return data;
489 492 };
490 493
491 494 /**
492 495 * can the cell be split into two cells
493 496 * @method is_splittable
494 497 **/
495 498 HeadingCell.prototype.is_splittable = function () {
496 499 return false;
497 500 };
498 501
499 502
500 503 /**
501 504 * can the cell be merged with other cells
502 505 * @method is_mergeable
503 506 **/
504 507 HeadingCell.prototype.is_mergeable = function () {
505 508 return false;
506 509 };
507 510
508 511 /**
509 512 * Change heading level of cell, and re-render
510 513 * @method set_level
511 514 */
512 515 HeadingCell.prototype.set_level = function (level) {
513 516 this.level = level;
514 517 if (this.rendered) {
515 518 this.rendered = false;
516 519 this.render();
517 520 };
518 521 };
519 522
520 523 /** The depth of header cell, based on html (h1 to h6)
521 524 * @method get_level
522 525 * @return {integer} level - for 1 to 6
523 526 */
524 527 HeadingCell.prototype.get_level = function () {
525 528 return this.level;
526 529 };
527 530
528 531
529 532 HeadingCell.prototype.set_rendered = function (html) {
530 533 this.element.find("div.text_cell_render").html(html);
531 534 };
532 535
533 536
534 537 HeadingCell.prototype.get_rendered = function () {
535 538 var r = this.element.find("div.text_cell_render");
536 539 return r.children().first().html();
537 540 };
538 541
539 542
540 543 HeadingCell.prototype.render = function () {
541 544 if (this.rendered === false) {
542 545 var text = this.get_text();
543 546 var math = null;
544 547 // Markdown headings must be a single line
545 548 text = text.replace(/\n/g, ' ');
546 549 if (text === "") { text = this.placeholder; }
547 550 text = Array(this.level + 1).join("#") + " " + text;
548 551 var text_and_math = IPython.mathjaxutils.remove_math(text);
549 552 text = text_and_math[0];
550 553 math = text_and_math[1];
551 554 var html = marked.parser(marked.lexer(text));
552 555 var h = $(IPython.mathjaxutils.replace_math(html, math));
553 556 // add id and linkback anchor
554 557 var hash = h.text().replace(/ /g, '-');
555 558 h.attr('id', hash);
556 559 h.append(
557 560 $('<a/>')
558 561 .addClass('anchor-link')
559 562 .attr('href', '#' + hash)
560 563 .text('¶')
561 564 );
562 565
563 566 this.set_rendered(h);
564 567 this.typeset();
565 568 this.element.find('div.text_cell_input').hide();
566 569 this.element.find("div.text_cell_render").show();
567 570 this.rendered = true;
568 571 };
569 572 };
570 573
571 574 IPython.TextCell = TextCell;
572 575 IPython.MarkdownCell = MarkdownCell;
573 576 IPython.RawCell = RawCell;
574 577 IPython.HeadingCell = HeadingCell;
575 578
576 579
577 580 return IPython;
578 581
579 582 }(IPython));
580 583
General Comments 0
You need to be logged in to leave comments. Login now