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