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