##// END OF EJS Templates
Merge pull request #5175 from jdfreder/html-take2...
Brian E. Granger -
r15543:c8e370d6 merge
parent child Browse files
Show More
@@ -1,578 +1,579 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // CodeCell
10 10 //============================================================================
11 11 /**
12 12 * An extendable module that provide base functionnality to create cell for notebook.
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule CodeCell
16 16 */
17 17
18 18
19 19 /* local util for codemirror */
20 20 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;};
21 21
22 22 /**
23 23 *
24 24 * function to delete until previous non blanking space character
25 25 * or first multiple of 4 tabstop.
26 26 * @private
27 27 */
28 28 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
29 29 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
30 30 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
31 31 var cur = cm.getCursor(), line = cm.getLine(cur.line);
32 32 var tabsize = cm.getOption('tabSize');
33 33 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
34 34 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
35 35 var select = cm.getRange(from,cur);
36 36 if( select.match(/^\ +$/) !== null){
37 37 cm.replaceRange("",from,cur);
38 38 } else {
39 39 cm.deleteH(-1,"char");
40 40 }
41 41 };
42 42
43 43
44 44 var IPython = (function (IPython) {
45 45 "use strict";
46 46
47 47 var utils = IPython.utils;
48 48 var key = IPython.utils.keycodes;
49 49
50 50 /**
51 51 * A Cell conceived to write code.
52 52 *
53 53 * The kernel doesn't have to be set at creation time, in that case
54 54 * it will be null and set_kernel has to be called later.
55 55 * @class CodeCell
56 56 * @extends IPython.Cell
57 57 *
58 58 * @constructor
59 59 * @param {Object|null} kernel
60 60 * @param {object|undefined} [options]
61 61 * @param [options.cm_config] {object} config to pass to CodeMirror
62 62 */
63 63 var CodeCell = function (kernel, options) {
64 64 this.kernel = kernel || null;
65 65 this.collapsed = false;
66 66
67 67 // create all attributed in constructor function
68 68 // even if null for V8 VM optimisation
69 69 this.input_prompt_number = null;
70 70 this.celltoolbar = null;
71 71 this.output_area = null;
72 72 this.last_msg_id = null;
73 73 this.completer = null;
74 74
75 75
76 76 var cm_overwrite_options = {
77 77 onKeyEvent: $.proxy(this.handle_keyevent,this)
78 78 };
79 79
80 80 options = this.mergeopt(CodeCell, options, {cm_config:cm_overwrite_options});
81 81
82 82 IPython.Cell.apply(this,[options]);
83 83
84 84 // Attributes we want to override in this subclass.
85 85 this.cell_type = "code";
86 86
87 87 var that = this;
88 88 this.element.focusout(
89 89 function() { that.auto_highlight(); }
90 90 );
91 91 };
92 92
93 93 CodeCell.options_default = {
94 94 cm_config : {
95 95 extraKeys: {
96 96 "Tab" : "indentMore",
97 97 "Shift-Tab" : "indentLess",
98 98 "Backspace" : "delSpaceToPrevTabStop",
99 99 "Cmd-/" : "toggleComment",
100 100 "Ctrl-/" : "toggleComment"
101 101 },
102 102 mode: 'ipython',
103 103 theme: 'ipython',
104 104 matchBrackets: true,
105 105 autoCloseBrackets: true
106 106 }
107 107 };
108 108
109 109 CodeCell.msg_cells = {};
110 110
111 111 CodeCell.prototype = new IPython.Cell();
112 112
113 113 /**
114 114 * @method auto_highlight
115 115 */
116 116 CodeCell.prototype.auto_highlight = function () {
117 117 this._auto_highlight(IPython.config.cell_magic_highlight);
118 118 };
119 119
120 120 /** @method create_element */
121 121 CodeCell.prototype.create_element = function () {
122 122 IPython.Cell.prototype.create_element.apply(this, arguments);
123 123
124 124 var cell = $('<div></div>').addClass('cell border-box-sizing code_cell');
125 125 cell.attr('tabindex','2');
126 126
127 127 var input = $('<div></div>').addClass('input');
128 128 var prompt = $('<div/>').addClass('prompt input_prompt');
129 129 var inner_cell = $('<div/>').addClass('inner_cell');
130 130 this.celltoolbar = new IPython.CellToolbar(this);
131 131 inner_cell.append(this.celltoolbar.element);
132 132 var input_area = $('<div/>').addClass('input_area');
133 133 this.code_mirror = CodeMirror(input_area.get(0), this.cm_config);
134 134 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
135 135 inner_cell.append(input_area);
136 136 input.append(prompt).append(inner_cell);
137 137
138 138 var widget_area = $('<div/>')
139 139 .addClass('widget-area')
140 140 .hide();
141 141 this.widget_area = widget_area;
142 142 var widget_prompt = $('<div/>')
143 143 .addClass('prompt')
144 144 .appendTo(widget_area);
145 145 var widget_subarea = $('<div/>')
146 146 .addClass('widget-subarea')
147 147 .appendTo(widget_area);
148 148 this.widget_subarea = widget_subarea;
149 149 var widget_clear_buton = $('<button />')
150 150 .addClass('close')
151 151 .html('&times;')
152 152 .click(function() {
153 153 widget_area.slideUp('', function(){ widget_subarea.html(''); });
154 154 })
155 155 .appendTo(widget_prompt);
156 156
157 157 var output = $('<div></div>');
158 158 cell.append(input).append(widget_area).append(output);
159 159 this.element = cell;
160 160 this.output_area = new IPython.OutputArea(output, true);
161 161 this.completer = new IPython.Completer(this);
162 162 };
163 163
164 164 /** @method bind_events */
165 165 CodeCell.prototype.bind_events = function () {
166 166 IPython.Cell.prototype.bind_events.apply(this);
167 167 var that = this;
168 168
169 169 this.element.focusout(
170 170 function() { that.auto_highlight(); }
171 171 );
172 172 };
173 173
174 174 CodeCell.prototype.handle_keyevent = function (editor, event) {
175 175
176 176 // console.log('CM', this.mode, event.which, event.type)
177 177
178 178 if (this.mode === 'command') {
179 179 return true;
180 180 } else if (this.mode === 'edit') {
181 181 return this.handle_codemirror_keyevent(editor, event);
182 182 }
183 183 };
184 184
185 185 /**
186 186 * This method gets called in CodeMirror's onKeyDown/onKeyPress
187 187 * handlers and is used to provide custom key handling. Its return
188 188 * value is used to determine if CodeMirror should ignore the event:
189 189 * true = ignore, false = don't ignore.
190 190 * @method handle_codemirror_keyevent
191 191 */
192 192 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
193 193
194 194 var that = this;
195 195 // whatever key is pressed, first, cancel the tooltip request before
196 196 // they are sent, and remove tooltip if any, except for tab again
197 197 var tooltip_closed = null;
198 198 if (event.type === 'keydown' && event.which != key.TAB ) {
199 199 tooltip_closed = IPython.tooltip.remove_and_cancel_tooltip();
200 200 }
201 201
202 202 var cur = editor.getCursor();
203 203 if (event.keyCode === key.ENTER){
204 204 this.auto_highlight();
205 205 }
206 206
207 207 if (event.keyCode === key.ENTER && (event.shiftKey || event.ctrlKey || event.altKey)) {
208 208 // Always ignore shift-enter in CodeMirror as we handle it.
209 209 return true;
210 210 } else if (event.which === 40 && event.type === 'keypress' && IPython.tooltip.time_before_tooltip >= 0) {
211 211 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
212 212 // browser and keyboard layout !
213 213 // Pressing '(' , request tooltip, don't forget to reappend it
214 214 // The second argument says to hide the tooltip if the docstring
215 215 // is actually empty
216 216 IPython.tooltip.pending(that, true);
217 217 } else if (event.which === key.UPARROW && event.type === 'keydown') {
218 218 // If we are not at the top, let CM handle the up arrow and
219 219 // prevent the global keydown handler from handling it.
220 220 if (!that.at_top()) {
221 221 event.stop();
222 222 return false;
223 223 } else {
224 224 return true;
225 225 }
226 226 } else if (event.which === key.ESC && event.type === 'keydown') {
227 227 // First see if the tooltip is active and if so cancel it.
228 228 if (tooltip_closed) {
229 229 // The call to remove_and_cancel_tooltip above in L177 doesn't pass
230 230 // force=true. Because of this it won't actually close the tooltip
231 231 // if it is in sticky mode. Thus, we have to check again if it is open
232 232 // and close it with force=true.
233 233 if (!IPython.tooltip._hidden) {
234 234 IPython.tooltip.remove_and_cancel_tooltip(true);
235 235 }
236 236 // If we closed the tooltip, don't let CM or the global handlers
237 237 // handle this event.
238 238 event.stop();
239 239 return true;
240 240 }
241 241 if (that.code_mirror.options.keyMap === "vim-insert") {
242 242 // vim keyMap is active and in insert mode. In this case we leave vim
243 243 // insert mode, but remain in notebook edit mode.
244 244 // Let' CM handle this event and prevent global handling.
245 245 event.stop();
246 246 return false;
247 247 } else {
248 248 // vim keyMap is not active. Leave notebook edit mode.
249 249 // Don't let CM handle the event, defer to global handling.
250 250 return true;
251 251 }
252 252 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
253 253 // If we are not at the bottom, let CM handle the down arrow and
254 254 // prevent the global keydown handler from handling it.
255 255 if (!that.at_bottom()) {
256 256 event.stop();
257 257 return false;
258 258 } else {
259 259 return true;
260 260 }
261 261 } else if (event.keyCode === key.TAB && event.type === 'keydown' && event.shiftKey) {
262 262 if (editor.somethingSelected()){
263 263 var anchor = editor.getCursor("anchor");
264 264 var head = editor.getCursor("head");
265 265 if( anchor.line != head.line){
266 266 return false;
267 267 }
268 268 }
269 269 IPython.tooltip.request(that);
270 270 event.stop();
271 271 return true;
272 272 } else if (event.keyCode === key.TAB && event.type == 'keydown') {
273 273 // Tab completion.
274 274 IPython.tooltip.remove_and_cancel_tooltip();
275 275 if (editor.somethingSelected()) {
276 276 return false;
277 277 }
278 278 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
279 279 if (pre_cursor.trim() === "") {
280 280 // Don't autocomplete if the part of the line before the cursor
281 281 // is empty. In this case, let CodeMirror handle indentation.
282 282 return false;
283 283 } else {
284 284 event.stop();
285 285 this.completer.startCompletion();
286 286 return true;
287 287 }
288 288 } else {
289 289 // keypress/keyup also trigger on TAB press, and we don't want to
290 290 // use those to disable tab completion.
291 291 return false;
292 292 }
293 293 return false;
294 294 };
295 295
296 296 // Kernel related calls.
297 297
298 298 CodeCell.prototype.set_kernel = function (kernel) {
299 299 this.kernel = kernel;
300 300 };
301 301
302 302 /**
303 303 * Execute current code cell to the kernel
304 304 * @method execute
305 305 */
306 306 CodeCell.prototype.execute = function () {
307 307 this.output_area.clear_output();
308 308
309 309 // Clear widget area
310 310 this.widget_subarea.html('');
311 311 this.widget_subarea.height('');
312 312 this.widget_area.height('');
313 313 this.widget_area.hide();
314 314
315 315 this.set_input_prompt('*');
316 316 this.element.addClass("running");
317 317 if (this.last_msg_id) {
318 318 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
319 319 }
320 320 var callbacks = this.get_callbacks();
321 321
322 322 var old_msg_id = this.last_msg_id;
323 323 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true});
324 324 if (old_msg_id) {
325 325 delete CodeCell.msg_cells[old_msg_id];
326 326 }
327 327 CodeCell.msg_cells[this.last_msg_id] = this;
328 328 };
329 329
330 330 /**
331 331 * Construct the default callbacks for
332 332 * @method get_callbacks
333 333 */
334 334 CodeCell.prototype.get_callbacks = function () {
335 335 return {
336 336 shell : {
337 337 reply : $.proxy(this._handle_execute_reply, this),
338 338 payload : {
339 339 set_next_input : $.proxy(this._handle_set_next_input, this),
340 340 page : $.proxy(this._open_with_pager, this)
341 341 }
342 342 },
343 343 iopub : {
344 344 output : $.proxy(this.output_area.handle_output, this.output_area),
345 345 clear_output : $.proxy(this.output_area.handle_clear_output, this.output_area),
346 346 },
347 347 input : $.proxy(this._handle_input_request, this)
348 348 };
349 349 };
350 350
351 351 CodeCell.prototype._open_with_pager = function (payload) {
352 352 $([IPython.events]).trigger('open_with_text.Pager', payload);
353 353 };
354 354
355 355 /**
356 356 * @method _handle_execute_reply
357 357 * @private
358 358 */
359 359 CodeCell.prototype._handle_execute_reply = function (msg) {
360 360 this.set_input_prompt(msg.content.execution_count);
361 361 this.element.removeClass("running");
362 362 $([IPython.events]).trigger('set_dirty.Notebook', {value: true});
363 363 };
364 364
365 365 /**
366 366 * @method _handle_set_next_input
367 367 * @private
368 368 */
369 369 CodeCell.prototype._handle_set_next_input = function (payload) {
370 370 var data = {'cell': this, 'text': payload.text};
371 371 $([IPython.events]).trigger('set_next_input.Notebook', data);
372 372 };
373 373
374 374 /**
375 375 * @method _handle_input_request
376 376 * @private
377 377 */
378 378 CodeCell.prototype._handle_input_request = function (msg) {
379 379 this.output_area.append_raw_input(msg);
380 380 };
381 381
382 382
383 383 // Basic cell manipulation.
384 384
385 385 CodeCell.prototype.select = function () {
386 386 var cont = IPython.Cell.prototype.select.apply(this);
387 387 if (cont) {
388 388 this.code_mirror.refresh();
389 389 this.auto_highlight();
390 390 }
391 391 return cont;
392 392 };
393 393
394 394 CodeCell.prototype.render = function () {
395 395 var cont = IPython.Cell.prototype.render.apply(this);
396 396 // Always execute, even if we are already in the rendered state
397 397 return cont;
398 398 };
399 399
400 400 CodeCell.prototype.unrender = function () {
401 401 // CodeCell is always rendered
402 402 return false;
403 403 };
404 404
405 405 /**
406 406 * Determine whether or not the unfocus event should be aknowledged.
407 407 *
408 408 * @method should_cancel_blur
409 409 *
410 410 * @return results {bool} Whether or not to ignore the cell's blur event.
411 411 **/
412 412 CodeCell.prototype.should_cancel_blur = function () {
413 413 // Cancel this unfocus event if the base wants to cancel or the cell
414 414 // completer is open or the tooltip is open.
415 415 return IPython.Cell.prototype.should_cancel_blur.apply(this) ||
416 416 (this.completer && this.completer.is_visible()) ||
417 417 (IPython.tooltip && IPython.tooltip.is_visible());
418 418 };
419 419
420 420 CodeCell.prototype.select_all = function () {
421 421 var start = {line: 0, ch: 0};
422 422 var nlines = this.code_mirror.lineCount();
423 423 var last_line = this.code_mirror.getLine(nlines-1);
424 424 var end = {line: nlines-1, ch: last_line.length};
425 425 this.code_mirror.setSelection(start, end);
426 426 };
427 427
428 428
429 429 CodeCell.prototype.collapse_output = function () {
430 430 this.collapsed = true;
431 431 this.output_area.collapse();
432 432 };
433 433
434 434
435 435 CodeCell.prototype.expand_output = function () {
436 436 this.collapsed = false;
437 437 this.output_area.expand();
438 438 this.output_area.unscroll_area();
439 439 };
440 440
441 441 CodeCell.prototype.scroll_output = function () {
442 442 this.output_area.expand();
443 443 this.output_area.scroll_if_long();
444 444 };
445 445
446 446 CodeCell.prototype.toggle_output = function () {
447 447 this.collapsed = Boolean(1 - this.collapsed);
448 448 this.output_area.toggle_output();
449 449 };
450 450
451 451 CodeCell.prototype.toggle_output_scroll = function () {
452 452 this.output_area.toggle_scroll();
453 453 };
454 454
455 455
456 456 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
457 457 var ns;
458 458 if (prompt_value == undefined) {
459 459 ns = "&nbsp;";
460 460 } else {
461 461 ns = encodeURIComponent(prompt_value);
462 462 }
463 463 return 'In&nbsp;[' + ns + ']:';
464 464 };
465 465
466 466 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
467 467 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
468 468 for(var i=1; i < lines_number; i++) {
469 469 html.push(['...:']);
470 470 }
471 471 return html.join('<br/>');
472 472 };
473 473
474 474 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
475 475
476 476
477 477 CodeCell.prototype.set_input_prompt = function (number) {
478 478 var nline = 1;
479 479 if (this.code_mirror !== undefined) {
480 480 nline = this.code_mirror.lineCount();
481 481 }
482 482 this.input_prompt_number = number;
483 483 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
484 // This HTML call is okay because the user contents are escaped.
484 485 this.element.find('div.input_prompt').html(prompt_html);
485 486 };
486 487
487 488
488 489 CodeCell.prototype.clear_input = function () {
489 490 this.code_mirror.setValue('');
490 491 };
491 492
492 493
493 494 CodeCell.prototype.get_text = function () {
494 495 return this.code_mirror.getValue();
495 496 };
496 497
497 498
498 499 CodeCell.prototype.set_text = function (code) {
499 500 return this.code_mirror.setValue(code);
500 501 };
501 502
502 503
503 504 CodeCell.prototype.at_top = function () {
504 505 var cursor = this.code_mirror.getCursor();
505 506 if (cursor.line === 0 && cursor.ch === 0) {
506 507 return true;
507 508 } else {
508 509 return false;
509 510 }
510 511 };
511 512
512 513
513 514 CodeCell.prototype.at_bottom = function () {
514 515 var cursor = this.code_mirror.getCursor();
515 516 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
516 517 return true;
517 518 } else {
518 519 return false;
519 520 }
520 521 };
521 522
522 523
523 524 CodeCell.prototype.clear_output = function (wait) {
524 525 this.output_area.clear_output(wait);
525 526 this.set_input_prompt();
526 527 };
527 528
528 529
529 530 // JSON serialization
530 531
531 532 CodeCell.prototype.fromJSON = function (data) {
532 533 IPython.Cell.prototype.fromJSON.apply(this, arguments);
533 534 if (data.cell_type === 'code') {
534 535 if (data.input !== undefined) {
535 536 this.set_text(data.input);
536 537 // make this value the starting point, so that we can only undo
537 538 // to this state, instead of a blank cell
538 539 this.code_mirror.clearHistory();
539 540 this.auto_highlight();
540 541 }
541 542 if (data.prompt_number !== undefined) {
542 543 this.set_input_prompt(data.prompt_number);
543 544 } else {
544 545 this.set_input_prompt();
545 546 }
546 547 this.output_area.trusted = data.trusted || false;
547 548 this.output_area.fromJSON(data.outputs);
548 549 if (data.collapsed !== undefined) {
549 550 if (data.collapsed) {
550 551 this.collapse_output();
551 552 } else {
552 553 this.expand_output();
553 554 }
554 555 }
555 556 }
556 557 };
557 558
558 559
559 560 CodeCell.prototype.toJSON = function () {
560 561 var data = IPython.Cell.prototype.toJSON.apply(this);
561 562 data.input = this.get_text();
562 563 // is finite protect against undefined and '*' value
563 564 if (isFinite(this.input_prompt_number)) {
564 565 data.prompt_number = this.input_prompt_number;
565 566 }
566 567 var outputs = this.output_area.toJSON();
567 568 data.outputs = outputs;
568 569 data.language = 'python';
569 570 data.trusted = this.output_area.trusted;
570 571 data.collapsed = this.collapsed;
571 572 return data;
572 573 };
573 574
574 575
575 576 IPython.CodeCell = CodeCell;
576 577
577 578 return IPython;
578 579 }(IPython));
@@ -1,864 +1,867 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // OutputArea
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule OutputArea
16 16 */
17 17 var IPython = (function (IPython) {
18 18 "use strict";
19 19
20 20 var utils = IPython.utils;
21 21
22 22 /**
23 23 * @class OutputArea
24 24 *
25 25 * @constructor
26 26 */
27 27
28 28 var OutputArea = function (selector, prompt_area) {
29 29 this.selector = selector;
30 30 this.wrapper = $(selector);
31 31 this.outputs = [];
32 32 this.collapsed = false;
33 33 this.scrolled = false;
34 34 this.trusted = true;
35 35 this.clear_queued = null;
36 36 if (prompt_area === undefined) {
37 37 this.prompt_area = true;
38 38 } else {
39 39 this.prompt_area = prompt_area;
40 40 }
41 41 this.create_elements();
42 42 this.style();
43 43 this.bind_events();
44 44 };
45 45
46 46
47 47 /**
48 48 * Class prototypes
49 49 **/
50 50
51 51 OutputArea.prototype.create_elements = function () {
52 52 this.element = $("<div/>");
53 53 this.collapse_button = $("<div/>");
54 54 this.prompt_overlay = $("<div/>");
55 55 this.wrapper.append(this.prompt_overlay);
56 56 this.wrapper.append(this.element);
57 57 this.wrapper.append(this.collapse_button);
58 58 };
59 59
60 60
61 61 OutputArea.prototype.style = function () {
62 62 this.collapse_button.hide();
63 63 this.prompt_overlay.hide();
64 64
65 65 this.wrapper.addClass('output_wrapper');
66 66 this.element.addClass('output');
67 67
68 68 this.collapse_button.addClass("btn output_collapsed");
69 69 this.collapse_button.attr('title', 'click to expand output');
70 70 this.collapse_button.text('. . .');
71 71
72 72 this.prompt_overlay.addClass('out_prompt_overlay prompt');
73 73 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
74 74
75 75 this.collapse();
76 76 };
77 77
78 78 /**
79 79 * Should the OutputArea scroll?
80 80 * Returns whether the height (in lines) exceeds a threshold.
81 81 *
82 82 * @private
83 83 * @method _should_scroll
84 84 * @param [lines=100]{Integer}
85 85 * @return {Bool}
86 86 *
87 87 */
88 88 OutputArea.prototype._should_scroll = function (lines) {
89 89 if (lines <=0 ){ return }
90 90 if (!lines) {
91 91 lines = 100;
92 92 }
93 93 // line-height from http://stackoverflow.com/questions/1185151
94 94 var fontSize = this.element.css('font-size');
95 95 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
96 96
97 97 return (this.element.height() > lines * lineHeight);
98 98 };
99 99
100 100
101 101 OutputArea.prototype.bind_events = function () {
102 102 var that = this;
103 103 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
104 104 this.prompt_overlay.click(function () { that.toggle_scroll(); });
105 105
106 106 this.element.resize(function () {
107 107 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
108 108 if ( IPython.utils.browser[0] === "Firefox" ) {
109 109 return;
110 110 }
111 111 // maybe scroll output,
112 112 // if it's grown large enough and hasn't already been scrolled.
113 113 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
114 114 that.scroll_area();
115 115 }
116 116 });
117 117 this.collapse_button.click(function () {
118 118 that.expand();
119 119 });
120 120 };
121 121
122 122
123 123 OutputArea.prototype.collapse = function () {
124 124 if (!this.collapsed) {
125 125 this.element.hide();
126 126 this.prompt_overlay.hide();
127 127 if (this.element.html()){
128 128 this.collapse_button.show();
129 129 }
130 130 this.collapsed = true;
131 131 }
132 132 };
133 133
134 134
135 135 OutputArea.prototype.expand = function () {
136 136 if (this.collapsed) {
137 137 this.collapse_button.hide();
138 138 this.element.show();
139 139 this.prompt_overlay.show();
140 140 this.collapsed = false;
141 141 }
142 142 };
143 143
144 144
145 145 OutputArea.prototype.toggle_output = function () {
146 146 if (this.collapsed) {
147 147 this.expand();
148 148 } else {
149 149 this.collapse();
150 150 }
151 151 };
152 152
153 153
154 154 OutputArea.prototype.scroll_area = function () {
155 155 this.element.addClass('output_scroll');
156 156 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
157 157 this.scrolled = true;
158 158 };
159 159
160 160
161 161 OutputArea.prototype.unscroll_area = function () {
162 162 this.element.removeClass('output_scroll');
163 163 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
164 164 this.scrolled = false;
165 165 };
166 166
167 167 /**
168 168 *
169 169 * Scroll OutputArea if height supperior than a threshold (in lines).
170 170 *
171 171 * Threshold is a maximum number of lines. If unspecified, defaults to
172 172 * OutputArea.minimum_scroll_threshold.
173 173 *
174 174 * Negative threshold will prevent the OutputArea from ever scrolling.
175 175 *
176 176 * @method scroll_if_long
177 177 *
178 178 * @param [lines=20]{Number} Default to 20 if not set,
179 179 * behavior undefined for value of `0`.
180 180 *
181 181 **/
182 182 OutputArea.prototype.scroll_if_long = function (lines) {
183 183 var n = lines | OutputArea.minimum_scroll_threshold;
184 184 if(n <= 0){
185 185 return
186 186 }
187 187
188 188 if (this._should_scroll(n)) {
189 189 // only allow scrolling long-enough output
190 190 this.scroll_area();
191 191 }
192 192 };
193 193
194 194
195 195 OutputArea.prototype.toggle_scroll = function () {
196 196 if (this.scrolled) {
197 197 this.unscroll_area();
198 198 } else {
199 199 // only allow scrolling long-enough output
200 200 this.scroll_if_long();
201 201 }
202 202 };
203 203
204 204
205 205 // typeset with MathJax if MathJax is available
206 206 OutputArea.prototype.typeset = function () {
207 207 if (window.MathJax){
208 208 MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
209 209 }
210 210 };
211 211
212 212
213 213 OutputArea.prototype.handle_output = function (msg) {
214 214 var json = {};
215 215 var msg_type = json.output_type = msg.header.msg_type;
216 216 var content = msg.content;
217 217 if (msg_type === "stream") {
218 218 json.text = content.data;
219 219 json.stream = content.name;
220 220 } else if (msg_type === "display_data") {
221 221 json = content.data;
222 222 json.output_type = msg_type;
223 223 json.metadata = content.metadata;
224 224 } else if (msg_type === "pyout") {
225 225 json = content.data;
226 226 json.output_type = msg_type;
227 227 json.metadata = content.metadata;
228 228 json.prompt_number = content.execution_count;
229 229 } else if (msg_type === "pyerr") {
230 230 json.ename = content.ename;
231 231 json.evalue = content.evalue;
232 232 json.traceback = content.traceback;
233 233 }
234 234 this.append_output(json);
235 235 };
236 236
237 237
238 238 OutputArea.prototype.rename_keys = function (data, key_map) {
239 239 var remapped = {};
240 240 for (var key in data) {
241 241 var new_key = key_map[key] || key;
242 242 remapped[new_key] = data[key];
243 243 }
244 244 return remapped;
245 245 };
246 246
247 247
248 248 OutputArea.output_types = [
249 249 'application/javascript',
250 250 'text/html',
251 251 'text/latex',
252 252 'image/svg+xml',
253 253 'image/png',
254 254 'image/jpeg',
255 255 'application/pdf',
256 256 'text/plain'
257 257 ];
258 258
259 259 OutputArea.prototype.validate_output = function (json) {
260 260 // scrub invalid outputs
261 261 // TODO: right now everything is a string, but JSON really shouldn't be.
262 262 // nbformat 4 will fix that.
263 263 $.map(OutputArea.output_types, function(key){
264 264 if (json[key] !== undefined && typeof json[key] !== 'string') {
265 265 console.log("Invalid type for " + key, json[key]);
266 266 delete json[key];
267 267 }
268 268 });
269 269 return json;
270 270 };
271 271
272 272 OutputArea.prototype.append_output = function (json) {
273 273 this.expand();
274 274 // Clear the output if clear is queued.
275 275 var needs_height_reset = false;
276 276 if (this.clear_queued) {
277 277 this.clear_output(false);
278 278 needs_height_reset = true;
279 279 }
280 280
281 281 // validate output data types
282 282 json = this.validate_output(json);
283 283
284 284 if (json.output_type === 'pyout') {
285 285 this.append_pyout(json);
286 286 } else if (json.output_type === 'pyerr') {
287 287 this.append_pyerr(json);
288 288 } else if (json.output_type === 'display_data') {
289 289 this.append_display_data(json);
290 290 } else if (json.output_type === 'stream') {
291 291 this.append_stream(json);
292 292 }
293 293
294 294 this.outputs.push(json);
295 295
296 296 // Only reset the height to automatic if the height is currently
297 297 // fixed (done by wait=True flag on clear_output).
298 298 if (needs_height_reset) {
299 299 this.element.height('');
300 300 }
301 301
302 302 var that = this;
303 303 setTimeout(function(){that.element.trigger('resize');}, 100);
304 304 };
305 305
306 306
307 307 OutputArea.prototype.create_output_area = function () {
308 308 var oa = $("<div/>").addClass("output_area");
309 309 if (this.prompt_area) {
310 310 oa.append($('<div/>').addClass('prompt'));
311 311 }
312 312 return oa;
313 313 };
314 314
315 315
316 316 function _get_metadata_key(metadata, key, mime) {
317 317 var mime_md = metadata[mime];
318 318 // mime-specific higher priority
319 319 if (mime_md && mime_md[key] !== undefined) {
320 320 return mime_md[key];
321 321 }
322 322 // fallback on global
323 323 return metadata[key];
324 324 }
325 325
326 326 OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
327 327 var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
328 328 if (_get_metadata_key(md, 'isolated', mime)) {
329 329 // Create an iframe to isolate the subarea from the rest of the
330 330 // document
331 331 var iframe = $('<iframe/>').addClass('box-flex1');
332 332 iframe.css({'height':1, 'width':'100%', 'display':'block'});
333 333 iframe.attr('frameborder', 0);
334 334 iframe.attr('scrolling', 'auto');
335 335
336 336 // Once the iframe is loaded, the subarea is dynamically inserted
337 337 iframe.on('load', function() {
338 338 // Workaround needed by Firefox, to properly render svg inside
339 339 // iframes, see http://stackoverflow.com/questions/10177190/
340 340 // svg-dynamically-added-to-iframe-does-not-render-correctly
341 341 this.contentDocument.open();
342 342
343 343 // Insert the subarea into the iframe
344 344 // We must directly write the html. When using Jquery's append
345 345 // method, javascript is evaluated in the parent document and
346 // not in the iframe document.
346 // not in the iframe document. At this point, subarea doesn't
347 // contain any user content.
347 348 this.contentDocument.write(subarea.html());
348 349
349 350 this.contentDocument.close();
350 351
351 352 var body = this.contentDocument.body;
352 353 // Adjust the iframe height automatically
353 354 iframe.height(body.scrollHeight + 'px');
354 355 });
355 356
356 357 // Elements should be appended to the inner subarea and not to the
357 358 // iframe
358 359 iframe.append = function(that) {
359 360 subarea.append(that);
360 361 };
361 362
362 363 return iframe;
363 364 } else {
364 365 return subarea;
365 366 }
366 367 }
367 368
368 369
369 370 OutputArea.prototype._append_javascript_error = function (err, element) {
370 371 // display a message when a javascript error occurs in display output
371 372 var msg = "Javascript error adding output!"
372 373 if ( element === undefined ) return;
373 element.append(
374 $('<div/>').html(msg + "<br/>" +
375 err.toString() +
376 '<br/>See your browser Javascript console for more details.'
377 ).addClass('js-error')
378 );
374 element
375 .append($('<div/>').text(msg).addClass('js-error'))
376 .append($('<div/>').text(err.toString()).addClass('js-error'))
377 .append($('<div/>').text('See your browser Javascript console for more details.').addClass('js-error'));
379 378 };
380 379
381 380 OutputArea.prototype._safe_append = function (toinsert) {
382 381 // safely append an item to the document
383 382 // this is an object created by user code,
384 383 // and may have errors, which should not be raised
385 384 // under any circumstances.
386 385 try {
387 386 this.element.append(toinsert);
388 387 } catch(err) {
389 388 console.log(err);
390 389 // Create an actual output_area and output_subarea, which creates
391 390 // the prompt area and the proper indentation.
392 391 var toinsert = this.create_output_area();
393 392 var subarea = $('<div/>').addClass('output_subarea');
394 393 toinsert.append(subarea);
395 394 this._append_javascript_error(err, subarea);
396 395 this.element.append(toinsert);
397 396 }
398 397 };
399 398
400 399
401 400 OutputArea.prototype.append_pyout = function (json) {
402 401 var n = json.prompt_number || ' ';
403 402 var toinsert = this.create_output_area();
404 403 if (this.prompt_area) {
405 404 toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
406 405 }
407 406 this.append_mime_type(json, toinsert);
408 407 this._safe_append(toinsert);
409 408 // If we just output latex, typeset it.
410 409 if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
411 410 this.typeset();
412 411 }
413 412 };
414 413
415 414
416 415 OutputArea.prototype.append_pyerr = function (json) {
417 416 var tb = json.traceback;
418 417 if (tb !== undefined && tb.length > 0) {
419 418 var s = '';
420 419 var len = tb.length;
421 420 for (var i=0; i<len; i++) {
422 421 s = s + tb[i] + '\n';
423 422 }
424 423 s = s + '\n';
425 424 var toinsert = this.create_output_area();
426 425 this.append_text(s, {}, toinsert);
427 426 this._safe_append(toinsert);
428 427 }
429 428 };
430 429
431 430
432 431 OutputArea.prototype.append_stream = function (json) {
433 432 // temporary fix: if stream undefined (json file written prior to this patch),
434 433 // default to most likely stdout:
435 434 if (json.stream == undefined){
436 435 json.stream = 'stdout';
437 436 }
438 437 var text = json.text;
439 438 var subclass = "output_"+json.stream;
440 439 if (this.outputs.length > 0){
441 440 // have at least one output to consider
442 441 var last = this.outputs[this.outputs.length-1];
443 442 if (last.output_type == 'stream' && json.stream == last.stream){
444 443 // latest output was in the same stream,
445 444 // so append directly into its pre tag
446 445 // escape ANSI & HTML specials:
447 446 var pre = this.element.find('div.'+subclass).last().find('pre');
448 447 var html = utils.fixCarriageReturn(
449 448 pre.html() + utils.fixConsole(text));
449 // The only user content injected with this HTML call is
450 // escaped by the fixConsole() method.
450 451 pre.html(html);
451 452 return;
452 453 }
453 454 }
454 455
455 456 if (!text.replace("\r", "")) {
456 457 // text is nothing (empty string, \r, etc.)
457 458 // so don't append any elements, which might add undesirable space
458 459 return;
459 460 }
460 461
461 462 // If we got here, attach a new div
462 463 var toinsert = this.create_output_area();
463 464 this.append_text(text, {}, toinsert, "output_stream "+subclass);
464 465 this._safe_append(toinsert);
465 466 };
466 467
467 468
468 469 OutputArea.prototype.append_display_data = function (json) {
469 470 var toinsert = this.create_output_area();
470 471 if (this.append_mime_type(json, toinsert)) {
471 472 this._safe_append(toinsert);
472 473 // If we just output latex, typeset it.
473 474 if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
474 475 this.typeset();
475 476 }
476 477 }
477 478 };
478 479
479 480
480 481 OutputArea.safe_outputs = {
481 482 'text/plain' : true,
482 483 'image/png' : true,
483 484 'image/jpeg' : true
484 485 };
485 486
486 487 OutputArea.prototype.append_mime_type = function (json, element) {
487 488 for (var type_i in OutputArea.display_order) {
488 489 var type = OutputArea.display_order[type_i];
489 490 var append = OutputArea.append_map[type];
490 491 if ((json[type] !== undefined) && append) {
491 492 if (!this.trusted && !OutputArea.safe_outputs[type]) {
492 493 // not trusted show warning and do not display
493 494 var content = {
494 495 text : "Untrusted " + type + " output ignored.",
495 496 stream : "stderr"
496 497 }
497 498 this.append_stream(content);
498 499 continue;
499 500 }
500 501 var md = json.metadata || {};
501 502 var toinsert = append.apply(this, [json[type], md, element]);
502 503 $([IPython.events]).trigger('output_appended.OutputArea', [type, json[type], md, toinsert]);
503 504 return true;
504 505 }
505 506 }
506 507 return false;
507 508 };
508 509
509 510
510 511 OutputArea.prototype.append_html = function (html, md, element) {
511 512 var type = 'text/html';
512 513 var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
513 514 IPython.keyboard_manager.register_events(toinsert);
514 515 toinsert.append(html);
515 516 element.append(toinsert);
516 517 return toinsert;
517 518 };
518 519
519 520
520 521 OutputArea.prototype.append_javascript = function (js, md, element) {
521 522 // We just eval the JS code, element appears in the local scope.
522 523 var type = 'application/javascript';
523 524 var toinsert = this.create_output_subarea(md, "output_javascript", type);
524 525 IPython.keyboard_manager.register_events(toinsert);
525 526 element.append(toinsert);
526 527 // FIXME TODO : remove `container element for 3.0`
527 528 //backward compat, js should be eval'ed in a context where `container` is defined.
528 529 var container = element;
529 530 container.show = function(){console.log('Warning "container.show()" is deprecated.')};
530 531 // end backward compat
531 532 try {
532 533 eval(js);
533 534 } catch(err) {
534 535 console.log(err);
535 536 this._append_javascript_error(err, toinsert);
536 537 }
537 538 return toinsert;
538 539 };
539 540
540 541
541 542 OutputArea.prototype.append_text = function (data, md, element, extra_class) {
542 543 var type = 'text/plain';
543 544 var toinsert = this.create_output_subarea(md, "output_text", type);
544 545 // escape ANSI & HTML specials in plaintext:
545 546 data = utils.fixConsole(data);
546 547 data = utils.fixCarriageReturn(data);
547 548 data = utils.autoLinkUrls(data);
548 549 if (extra_class){
549 550 toinsert.addClass(extra_class);
550 551 }
552 // The only user content injected with this HTML call is
553 // escaped by the fixConsole() method.
551 554 toinsert.append($("<pre/>").html(data));
552 555 element.append(toinsert);
553 556 return toinsert;
554 557 };
555 558
556 559
557 560 OutputArea.prototype.append_svg = function (svg, md, element) {
558 561 var type = 'image/svg+xml';
559 562 var toinsert = this.create_output_subarea(md, "output_svg", type);
560 563 toinsert.append(svg);
561 564 element.append(toinsert);
562 565 return toinsert;
563 566 };
564 567
565 568
566 569 OutputArea.prototype._dblclick_to_reset_size = function (img) {
567 570 // schedule wrapping image in resizable after a delay,
568 571 // so we don't end up calling resize on a zero-size object
569 572 var that = this;
570 573 setTimeout(function () {
571 574 var h0 = img.height();
572 575 var w0 = img.width();
573 576 if (!(h0 && w0)) {
574 577 // zero size, schedule another timeout
575 578 that._dblclick_to_reset_size(img);
576 579 return;
577 580 }
578 581 img.resizable({
579 582 aspectRatio: true,
580 583 autoHide: true
581 584 });
582 585 img.dblclick(function () {
583 586 // resize wrapper & image together for some reason:
584 587 img.parent().height(h0);
585 588 img.height(h0);
586 589 img.parent().width(w0);
587 590 img.width(w0);
588 591 });
589 592 }, 250);
590 593 };
591 594
592 595 var set_width_height = function (img, md, mime) {
593 596 // set width and height of an img element from metadata
594 597 var height = _get_metadata_key(md, 'height', mime);
595 598 if (height !== undefined) img.attr('height', height);
596 599 var width = _get_metadata_key(md, 'width', mime);
597 600 if (width !== undefined) img.attr('width', width);
598 601 };
599 602
600 603 OutputArea.prototype.append_png = function (png, md, element) {
601 604 var type = 'image/png';
602 605 var toinsert = this.create_output_subarea(md, "output_png", type);
603 606 var img = $("<img/>").attr('src','data:image/png;base64,'+png);
604 607 set_width_height(img, md, 'image/png');
605 608 this._dblclick_to_reset_size(img);
606 609 toinsert.append(img);
607 610 element.append(toinsert);
608 611 return toinsert;
609 612 };
610 613
611 614
612 615 OutputArea.prototype.append_jpeg = function (jpeg, md, element) {
613 616 var type = 'image/jpeg';
614 617 var toinsert = this.create_output_subarea(md, "output_jpeg", type);
615 618 var img = $("<img/>").attr('src','data:image/jpeg;base64,'+jpeg);
616 619 set_width_height(img, md, 'image/jpeg');
617 620 this._dblclick_to_reset_size(img);
618 621 toinsert.append(img);
619 622 element.append(toinsert);
620 623 return toinsert;
621 624 };
622 625
623 626
624 627 OutputArea.prototype.append_pdf = function (pdf, md, element) {
625 628 var type = 'application/pdf';
626 629 var toinsert = this.create_output_subarea(md, "output_pdf", type);
627 630 var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
628 631 a.attr('target', '_blank');
629 632 a.text('View PDF')
630 633 toinsert.append(a);
631 634 element.append(toinsert);
632 635 return toinsert;
633 636 }
634 637
635 638 OutputArea.prototype.append_latex = function (latex, md, element) {
636 639 // This method cannot do the typesetting because the latex first has to
637 640 // be on the page.
638 641 var type = 'text/latex';
639 642 var toinsert = this.create_output_subarea(md, "output_latex", type);
640 643 toinsert.append(latex);
641 644 element.append(toinsert);
642 645 return toinsert;
643 646 };
644 647
645 648
646 649 OutputArea.prototype.append_raw_input = function (msg) {
647 650 var that = this;
648 651 this.expand();
649 652 var content = msg.content;
650 653 var area = this.create_output_area();
651 654
652 655 // disable any other raw_inputs, if they are left around
653 656 $("div.output_subarea.raw_input").remove();
654 657
655 658 area.append(
656 659 $("<div/>")
657 660 .addClass("box-flex1 output_subarea raw_input")
658 661 .append(
659 662 $("<span/>")
660 663 .addClass("input_prompt")
661 664 .text(content.prompt)
662 665 )
663 666 .append(
664 667 $("<input/>")
665 668 .addClass("raw_input")
666 669 .attr('type', 'text')
667 670 .attr("size", 47)
668 671 .keydown(function (event, ui) {
669 672 // make sure we submit on enter,
670 673 // and don't re-execute the *cell* on shift-enter
671 674 if (event.which === utils.keycodes.ENTER) {
672 675 that._submit_raw_input();
673 676 return false;
674 677 }
675 678 })
676 679 )
677 680 );
678 681
679 682 this.element.append(area);
680 683 var raw_input = area.find('input.raw_input');
681 684 // Register events that enable/disable the keyboard manager while raw
682 685 // input is focused.
683 686 IPython.keyboard_manager.register_events(raw_input);
684 687 // Note, the following line used to read raw_input.focus().focus().
685 688 // This seemed to be needed otherwise only the cell would be focused.
686 689 // But with the modal UI, this seems to work fine with one call to focus().
687 690 raw_input.focus();
688 691 }
689 692
690 693 OutputArea.prototype._submit_raw_input = function (evt) {
691 694 var container = this.element.find("div.raw_input");
692 695 var theprompt = container.find("span.input_prompt");
693 696 var theinput = container.find("input.raw_input");
694 697 var value = theinput.val();
695 698 var content = {
696 699 output_type : 'stream',
697 700 name : 'stdout',
698 701 text : theprompt.text() + value + '\n'
699 702 }
700 703 // remove form container
701 704 container.parent().remove();
702 705 // replace with plaintext version in stdout
703 706 this.append_output(content, false);
704 707 $([IPython.events]).trigger('send_input_reply.Kernel', value);
705 708 }
706 709
707 710
708 711 OutputArea.prototype.handle_clear_output = function (msg) {
709 712 // msg spec v4 had stdout, stderr, display keys
710 713 // v4.1 replaced these with just wait
711 714 // The default behavior is the same (stdout=stderr=display=True, wait=False),
712 715 // so v4 messages will still be properly handled,
713 716 // except for the rarely used clearing less than all output.
714 717 this.clear_output(msg.content.wait || false);
715 718 };
716 719
717 720
718 721 OutputArea.prototype.clear_output = function(wait) {
719 722 if (wait) {
720 723
721 724 // If a clear is queued, clear before adding another to the queue.
722 725 if (this.clear_queued) {
723 726 this.clear_output(false);
724 727 };
725 728
726 729 this.clear_queued = true;
727 730 } else {
728 731
729 732 // Fix the output div's height if the clear_output is waiting for
730 733 // new output (it is being used in an animation).
731 734 if (this.clear_queued) {
732 735 var height = this.element.height();
733 736 this.element.height(height);
734 737 this.clear_queued = false;
735 738 }
736 739
737 740 // clear all, no need for logic
738 741 this.element.html("");
739 742 this.outputs = [];
740 743 this.trusted = true;
741 744 this.unscroll_area();
742 745 return;
743 746 };
744 747 };
745 748
746 749
747 750 // JSON serialization
748 751
749 752 OutputArea.prototype.fromJSON = function (outputs) {
750 753 var len = outputs.length;
751 754 var data;
752 755
753 756 for (var i=0; i<len; i++) {
754 757 data = outputs[i];
755 758 var msg_type = data.output_type;
756 759 if (msg_type === "display_data" || msg_type === "pyout") {
757 760 // convert short keys to mime keys
758 761 // TODO: remove mapping of short keys when we update to nbformat 4
759 762 data = this.rename_keys(data, OutputArea.mime_map_r);
760 763 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map_r);
761 764 }
762 765
763 766 this.append_output(data);
764 767 }
765 768 };
766 769
767 770
768 771 OutputArea.prototype.toJSON = function () {
769 772 var outputs = [];
770 773 var len = this.outputs.length;
771 774 var data;
772 775 for (var i=0; i<len; i++) {
773 776 data = this.outputs[i];
774 777 var msg_type = data.output_type;
775 778 if (msg_type === "display_data" || msg_type === "pyout") {
776 779 // convert mime keys to short keys
777 780 data = this.rename_keys(data, OutputArea.mime_map);
778 781 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map);
779 782 }
780 783 outputs[i] = data;
781 784 }
782 785 return outputs;
783 786 };
784 787
785 788 /**
786 789 * Class properties
787 790 **/
788 791
789 792 /**
790 793 * Threshold to trigger autoscroll when the OutputArea is resized,
791 794 * typically when new outputs are added.
792 795 *
793 796 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
794 797 * unless it is < 0, in which case autoscroll will never be triggered
795 798 *
796 799 * @property auto_scroll_threshold
797 800 * @type Number
798 801 * @default 100
799 802 *
800 803 **/
801 804 OutputArea.auto_scroll_threshold = 100;
802 805
803 806 /**
804 807 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
805 808 * shorter than this are never scrolled.
806 809 *
807 810 * @property minimum_scroll_threshold
808 811 * @type Number
809 812 * @default 20
810 813 *
811 814 **/
812 815 OutputArea.minimum_scroll_threshold = 20;
813 816
814 817
815 818
816 819 OutputArea.mime_map = {
817 820 "text/plain" : "text",
818 821 "text/html" : "html",
819 822 "image/svg+xml" : "svg",
820 823 "image/png" : "png",
821 824 "image/jpeg" : "jpeg",
822 825 "text/latex" : "latex",
823 826 "application/json" : "json",
824 827 "application/javascript" : "javascript",
825 828 };
826 829
827 830 OutputArea.mime_map_r = {
828 831 "text" : "text/plain",
829 832 "html" : "text/html",
830 833 "svg" : "image/svg+xml",
831 834 "png" : "image/png",
832 835 "jpeg" : "image/jpeg",
833 836 "latex" : "text/latex",
834 837 "json" : "application/json",
835 838 "javascript" : "application/javascript",
836 839 };
837 840
838 841 OutputArea.display_order = [
839 842 'application/javascript',
840 843 'text/html',
841 844 'text/latex',
842 845 'image/svg+xml',
843 846 'image/png',
844 847 'image/jpeg',
845 848 'application/pdf',
846 849 'text/plain'
847 850 ];
848 851
849 852 OutputArea.append_map = {
850 853 "text/plain" : OutputArea.prototype.append_text,
851 854 "text/html" : OutputArea.prototype.append_html,
852 855 "image/svg+xml" : OutputArea.prototype.append_svg,
853 856 "image/png" : OutputArea.prototype.append_png,
854 857 "image/jpeg" : OutputArea.prototype.append_jpeg,
855 858 "text/latex" : OutputArea.prototype.append_latex,
856 859 "application/javascript" : OutputArea.prototype.append_javascript,
857 860 "application/pdf" : OutputArea.prototype.append_pdf
858 861 };
859 862
860 863 IPython.OutputArea = OutputArea;
861 864
862 865 return IPython;
863 866
864 867 }(IPython));
@@ -1,176 +1,178 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Pager
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 var utils = IPython.utils;
16 16
17 17 var Pager = function (pager_selector, pager_splitter_selector) {
18 18 this.pager_element = $(pager_selector);
19 19 this.pager_button_area = $('#pager_button_area');
20 20 var that = this;
21 21 this.percentage_height = 0.40;
22 22 this.pager_splitter_element = $(pager_splitter_selector)
23 23 .draggable({
24 24 containment: 'window',
25 25 axis:'y',
26 26 helper: null ,
27 27 drag: function(event, ui) {
28 28 // recalculate the amount of space the pager should take
29 29 var pheight = ($(document.body).height()-event.clientY-4);
30 30 var downprct = pheight/IPython.layout_manager.app_height();
31 31 downprct = Math.min(0.9, downprct);
32 32 if (downprct < 0.1) {
33 33 that.percentage_height = 0.1;
34 34 that.collapse({'duration':0});
35 35 } else if (downprct > 0.2) {
36 36 that.percentage_height = downprct;
37 37 that.expand({'duration':0});
38 38 }
39 39 IPython.layout_manager.do_resize();
40 40 }
41 41 });
42 42 this.expanded = false;
43 43 this.style();
44 44 this.create_button_area();
45 45 this.bind_events();
46 46 };
47 47
48 48 Pager.prototype.create_button_area = function(){
49 49 var that = this;
50 50 this.pager_button_area.append(
51 51 $('<a>').attr('role', "button")
52 52 .attr('title',"Open the pager in an external window")
53 53 .addClass('ui-button')
54 54 .click(function(){that.detach()})
55 55 .attr('style','position: absolute; right: 20px;')
56 56 .append(
57 57 $('<span>').addClass("ui-icon ui-icon-extlink")
58 58 )
59 59 )
60 60 this.pager_button_area.append(
61 61 $('<a>').attr('role', "button")
62 62 .attr('title',"Close the pager")
63 63 .addClass('ui-button')
64 64 .click(function(){that.collapse()})
65 65 .attr('style','position: absolute; right: 5px;')
66 66 .append(
67 67 $('<span>').addClass("ui-icon ui-icon-close")
68 68 )
69 69 )
70 70 };
71 71
72 72 Pager.prototype.style = function () {
73 73 this.pager_splitter_element.addClass('border-box-sizing ui-widget ui-state-default');
74 74 this.pager_element.addClass('border-box-sizing');
75 75 this.pager_element.find(".container").addClass('border-box-sizing');
76 76 this.pager_splitter_element.attr('title', 'Click to Show/Hide pager area, drag to Resize');
77 77 };
78 78
79 79
80 80 Pager.prototype.bind_events = function () {
81 81 var that = this;
82 82
83 83 this.pager_element.bind('collapse_pager', function (event, extrap) {
84 84 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
85 85 that.pager_element.hide(time);
86 86 });
87 87
88 88 this.pager_element.bind('expand_pager', function (event, extrap) {
89 89 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
90 90 that.pager_element.show(time);
91 91 });
92 92
93 93 this.pager_splitter_element.hover(
94 94 function () {
95 95 that.pager_splitter_element.addClass('ui-state-hover');
96 96 },
97 97 function () {
98 98 that.pager_splitter_element.removeClass('ui-state-hover');
99 99 }
100 100 );
101 101
102 102 this.pager_splitter_element.click(function () {
103 103 that.toggle();
104 104 });
105 105
106 106 $([IPython.events]).on('open_with_text.Pager', function (event, data) {
107 107 if (data.text.trim() !== '') {
108 108 that.clear();
109 109 that.expand();
110 110 that.append_text(data.text);
111 111 };
112 112 });
113 113 };
114 114
115 115
116 116 Pager.prototype.collapse = function (extrap) {
117 117 if (this.expanded === true) {
118 118 this.expanded = false;
119 119 this.pager_element.add($('div#notebook')).trigger('collapse_pager', extrap);
120 120 };
121 121 };
122 122
123 123
124 124 Pager.prototype.expand = function (extrap) {
125 125 if (this.expanded !== true) {
126 126 this.expanded = true;
127 127 this.pager_element.add($('div#notebook')).trigger('expand_pager', extrap);
128 128 };
129 129 };
130 130
131 131
132 132 Pager.prototype.toggle = function () {
133 133 if (this.expanded === true) {
134 134 this.collapse();
135 135 } else {
136 136 this.expand();
137 137 };
138 138 };
139 139
140 140
141 141 Pager.prototype.clear = function (text) {
142 142 this.pager_element.find(".container").empty();
143 143 };
144 144
145 145 Pager.prototype.detach = function(){
146 146 var w = window.open("","_blank");
147 147 $(w.document.head)
148 148 .append(
149 149 $('<link>')
150 150 .attr('rel',"stylesheet")
151 151 .attr('href',"/static/css/notebook.css")
152 152 .attr('type',"text/css")
153 153 )
154 154 .append(
155 155 $('<title>').text("IPython Pager")
156 156 );
157 157 var pager_body = $(w.document.body);
158 158 pager_body.css('overflow','scroll');
159 159
160 160 pager_body.append(this.pager_element.clone().children());
161 161 w.document.close();
162 162 this.collapse();
163 163
164 164 }
165 165
166 166 Pager.prototype.append_text = function (text) {
167 // The only user content injected with this HTML call is escaped by
168 // the fixConsole() method.
167 169 this.pager_element.find(".container").append($('<pre/>').html(utils.fixCarriageReturn(utils.fixConsole(text))));
168 170 };
169 171
170 172
171 173 IPython.Pager = Pager;
172 174
173 175 return IPython;
174 176
175 177 }(IPython));
176 178
@@ -1,553 +1,561 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2012 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // TextCell
10 10 //============================================================================
11 11
12 12
13 13
14 14 /**
15 15 A module that allow to create different type of Text Cell
16 16 @module IPython
17 17 @namespace IPython
18 18 */
19 19 var IPython = (function (IPython) {
20 20 "use strict";
21 21
22 22 // TextCell base class
23 23 var key = IPython.utils.keycodes;
24 24
25 25 /**
26 26 * Construct a new TextCell, codemirror mode is by default 'htmlmixed', and cell type is 'text'
27 27 * cell start as not redered.
28 28 *
29 29 * @class TextCell
30 30 * @constructor TextCell
31 31 * @extend IPython.Cell
32 32 * @param {object|undefined} [options]
33 33 * @param [options.cm_config] {object} config to pass to CodeMirror, will extend/overwrite default config
34 34 * @param [options.placeholder] {string} default string to use when souce in empty for rendering (only use in some TextCell subclass)
35 35 */
36 36 var TextCell = function (options) {
37 37 // in all TextCell/Cell subclasses
38 38 // do not assign most of members here, just pass it down
39 39 // in the options dict potentially overwriting what you wish.
40 40 // they will be assigned in the base class.
41 41
42 42 // we cannot put this as a class key as it has handle to "this".
43 43 var cm_overwrite_options = {
44 44 onKeyEvent: $.proxy(this.handle_keyevent,this)
45 45 };
46 46
47 47 options = this.mergeopt(TextCell,options,{cm_config:cm_overwrite_options});
48 48
49 49 this.cell_type = this.cell_type || 'text';
50 50
51 51 IPython.Cell.apply(this, [options]);
52 52
53 53 this.rendered = false;
54 54 };
55 55
56 56 TextCell.prototype = new IPython.Cell();
57 57
58 58 TextCell.options_default = {
59 59 cm_config : {
60 60 extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
61 61 mode: 'htmlmixed',
62 62 lineWrapping : true,
63 63 }
64 64 };
65 65
66 66
67 67 /**
68 68 * Create the DOM element of the TextCell
69 69 * @method create_element
70 70 * @private
71 71 */
72 72 TextCell.prototype.create_element = function () {
73 73 IPython.Cell.prototype.create_element.apply(this, arguments);
74 74
75 75 var cell = $("<div>").addClass('cell text_cell border-box-sizing');
76 76 cell.attr('tabindex','2');
77 77
78 78 var prompt = $('<div/>').addClass('prompt input_prompt');
79 79 cell.append(prompt);
80 80 var inner_cell = $('<div/>').addClass('inner_cell');
81 81 this.celltoolbar = new IPython.CellToolbar(this);
82 82 inner_cell.append(this.celltoolbar.element);
83 83 var input_area = $('<div/>').addClass('text_cell_input border-box-sizing');
84 84 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
85 85 // The tabindex=-1 makes this div focusable.
86 86 var render_area = $('<div/>').addClass('text_cell_render border-box-sizing').
87 87 addClass('rendered_html').attr('tabindex','-1');
88 88 inner_cell.append(input_area).append(render_area);
89 89 cell.append(inner_cell);
90 90 this.element = cell;
91 91 };
92 92
93 93
94 94 /**
95 95 * Bind the DOM evet to cell actions
96 96 * Need to be called after TextCell.create_element
97 97 * @private
98 98 * @method bind_event
99 99 */
100 100 TextCell.prototype.bind_events = function () {
101 101 IPython.Cell.prototype.bind_events.apply(this);
102 102 var that = this;
103 103
104 104 this.element.dblclick(function () {
105 105 if (that.selected === false) {
106 106 $([IPython.events]).trigger('select.Cell', {'cell':that});
107 107 }
108 108 var cont = that.unrender();
109 109 if (cont) {
110 110 that.focus_editor();
111 111 }
112 112 });
113 113 };
114 114
115 115 TextCell.prototype.handle_keyevent = function (editor, event) {
116 116
117 117 // console.log('CM', this.mode, event.which, event.type)
118 118
119 119 if (this.mode === 'command') {
120 120 return true;
121 121 } else if (this.mode === 'edit') {
122 122 return this.handle_codemirror_keyevent(editor, event);
123 123 }
124 124 };
125 125
126 126 /**
127 127 * This method gets called in CodeMirror's onKeyDown/onKeyPress
128 128 * handlers and is used to provide custom key handling.
129 129 *
130 130 * Subclass should override this method to have custom handeling
131 131 *
132 132 * @method handle_codemirror_keyevent
133 133 * @param {CodeMirror} editor - The codemirror instance bound to the cell
134 134 * @param {event} event -
135 135 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
136 136 */
137 137 TextCell.prototype.handle_codemirror_keyevent = function (editor, event) {
138 138 var that = this;
139 139
140 140 if (event.keyCode === 13 && (event.shiftKey || event.ctrlKey || event.altKey)) {
141 141 // Always ignore shift-enter in CodeMirror as we handle it.
142 142 return true;
143 143 } else if (event.which === key.UPARROW && event.type === 'keydown') {
144 144 // If we are not at the top, let CM handle the up arrow and
145 145 // prevent the global keydown handler from handling it.
146 146 if (!that.at_top()) {
147 147 event.stop();
148 148 return false;
149 149 } else {
150 150 return true;
151 151 }
152 152 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
153 153 // If we are not at the bottom, let CM handle the down arrow and
154 154 // prevent the global keydown handler from handling it.
155 155 if (!that.at_bottom()) {
156 156 event.stop();
157 157 return false;
158 158 } else {
159 159 return true;
160 160 }
161 161 } else if (event.which === key.ESC && event.type === 'keydown') {
162 162 if (that.code_mirror.options.keyMap === "vim-insert") {
163 163 // vim keyMap is active and in insert mode. In this case we leave vim
164 164 // insert mode, but remain in notebook edit mode.
165 165 // Let' CM handle this event and prevent global handling.
166 166 event.stop();
167 167 return false;
168 168 } else {
169 169 // vim keyMap is not active. Leave notebook edit mode.
170 170 // Don't let CM handle the event, defer to global handling.
171 171 return true;
172 172 }
173 173 }
174 174 return false;
175 175 };
176 176
177 177 // Cell level actions
178 178
179 179 TextCell.prototype.select = function () {
180 180 var cont = IPython.Cell.prototype.select.apply(this);
181 181 if (cont) {
182 182 if (this.mode === 'edit') {
183 183 this.code_mirror.refresh();
184 184 }
185 185 }
186 186 return cont;
187 187 };
188 188
189 189 TextCell.prototype.unrender = function () {
190 190 if (this.read_only) return;
191 191 var cont = IPython.Cell.prototype.unrender.apply(this);
192 192 if (cont) {
193 193 var text_cell = this.element;
194 194 var output = text_cell.find("div.text_cell_render");
195 195 output.hide();
196 196 text_cell.find('div.text_cell_input').show();
197 197 if (this.get_text() === this.placeholder) {
198 198 this.set_text('');
199 199 }
200 200 this.refresh();
201 201 }
202 202 return cont;
203 203 };
204 204
205 205 TextCell.prototype.execute = function () {
206 206 this.render();
207 207 };
208 208
209 209 /**
210 210 * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
211 211 * @method get_text
212 212 * @retrun {string} CodeMirror current text value
213 213 */
214 214 TextCell.prototype.get_text = function() {
215 215 return this.code_mirror.getValue();
216 216 };
217 217
218 218 /**
219 219 * @param {string} text - Codemiror text value
220 220 * @see TextCell#get_text
221 221 * @method set_text
222 222 * */
223 223 TextCell.prototype.set_text = function(text) {
224 224 this.code_mirror.setValue(text);
225 225 this.code_mirror.refresh();
226 226 };
227 227
228 228 /**
229 229 * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
230 230 * @method get_rendered
231 231 * @return {html} html of rendered element
232 232 * */
233 233 TextCell.prototype.get_rendered = function() {
234 234 return this.element.find('div.text_cell_render').html();
235 235 };
236 236
237 237 /**
238 238 * @method set_rendered
239 239 */
240 240 TextCell.prototype.set_rendered = function(text) {
241 241 this.element.find('div.text_cell_render').html(text);
242 242 };
243 243
244 244 /**
245 245 * @method at_top
246 246 * @return {Boolean}
247 247 */
248 248 TextCell.prototype.at_top = function () {
249 249 if (this.rendered) {
250 250 return true;
251 251 } else {
252 252 var cursor = this.code_mirror.getCursor();
253 253 if (cursor.line === 0 && cursor.ch === 0) {
254 254 return true;
255 255 } else {
256 256 return false;
257 257 }
258 258 }
259 259 };
260 260
261 261 /**
262 262 * @method at_bottom
263 263 * @return {Boolean}
264 264 * */
265 265 TextCell.prototype.at_bottom = function () {
266 266 if (this.rendered) {
267 267 return true;
268 268 } else {
269 269 var cursor = this.code_mirror.getCursor();
270 270 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
271 271 return true;
272 272 } else {
273 273 return false;
274 274 }
275 275 }
276 276 };
277 277
278 278 /**
279 279 * Create Text cell from JSON
280 280 * @param {json} data - JSON serialized text-cell
281 281 * @method fromJSON
282 282 */
283 283 TextCell.prototype.fromJSON = function (data) {
284 284 IPython.Cell.prototype.fromJSON.apply(this, arguments);
285 285 if (data.cell_type === this.cell_type) {
286 286 if (data.source !== undefined) {
287 287 this.set_text(data.source);
288 288 // make this value the starting point, so that we can only undo
289 289 // to this state, instead of a blank cell
290 290 this.code_mirror.clearHistory();
291 // TODO: This HTML needs to be treated as potentially dangerous
292 // user input and should be handled before set_rendered.
291 293 this.set_rendered(data.rendered || '');
292 294 this.rendered = false;
293 295 this.render();
294 296 }
295 297 }
296 298 };
297 299
298 300 /** Generate JSON from cell
299 301 * @return {object} cell data serialised to json
300 302 */
301 303 TextCell.prototype.toJSON = function () {
302 304 var data = IPython.Cell.prototype.toJSON.apply(this);
303 305 data.source = this.get_text();
304 306 if (data.source == this.placeholder) {
305 307 data.source = "";
306 308 }
307 309 return data;
308 310 };
309 311
310 312
311 313 /**
312 314 * @class MarkdownCell
313 315 * @constructor MarkdownCell
314 316 * @extends IPython.HTMLCell
315 317 */
316 318 var MarkdownCell = function (options) {
317 319 options = this.mergeopt(MarkdownCell, options);
318 320
319 321 this.cell_type = 'markdown';
320 322 TextCell.apply(this, [options]);
321 323 };
322 324
323 325 MarkdownCell.options_default = {
324 326 cm_config: {
325 327 mode: 'gfm'
326 328 },
327 329 placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
328 330 };
329 331
330 332 MarkdownCell.prototype = new TextCell();
331 333
332 334 /**
333 335 * @method render
334 336 */
335 337 MarkdownCell.prototype.render = function () {
336 338 var cont = IPython.TextCell.prototype.render.apply(this);
337 339 if (cont) {
338 340 var text = this.get_text();
339 341 var math = null;
340 342 if (text === "") { text = this.placeholder; }
341 343 var text_and_math = IPython.mathjaxutils.remove_math(text);
342 344 text = text_and_math[0];
343 345 math = text_and_math[1];
344 346 var html = marked.parser(marked.lexer(text));
345 347 html = $(IPython.mathjaxutils.replace_math(html, math));
346 // links in markdown cells should open in new tabs
348 // Links in markdown cells should open in new tabs.
347 349 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
348 350 try {
351 // TODO: This HTML needs to be treated as potentially dangerous
352 // user input and should be handled before set_rendered.
349 353 this.set_rendered(html);
350 354 } catch (e) {
351 355 console.log("Error running Javascript in Markdown:");
352 356 console.log(e);
353 this.set_rendered($("<div/>").addClass("js-error").html(
354 "Error rendering Markdown!<br/>" + e.toString())
357 this.set_rendered(
358 $("<div/>")
359 .append($("<div/>").text('Error rendering Markdown!').addClass("js-error"))
360 .append($("<div/>").text(e.toString()).addClass("js-error"))
361 .html()
355 362 );
356 363 }
357 364 this.element.find('div.text_cell_input').hide();
358 365 this.element.find("div.text_cell_render").show();
359 366 this.typeset();
360 367 }
361 368 return cont;
362 369 };
363 370
364 371
365 372 // RawCell
366 373
367 374 /**
368 375 * @class RawCell
369 376 * @constructor RawCell
370 377 * @extends IPython.TextCell
371 378 */
372 379 var RawCell = function (options) {
373 380
374 381 options = this.mergeopt(RawCell,options);
375 382 TextCell.apply(this, [options]);
376 383 this.cell_type = 'raw';
377 384 // RawCell should always hide its rendered div
378 385 this.element.find('div.text_cell_render').hide();
379 386 };
380 387
381 388 RawCell.options_default = {
382 389 placeholder : "Write raw LaTeX or other formats here, for use with nbconvert.\n" +
383 390 "It will not be rendered in the notebook.\n" +
384 391 "When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
385 392 };
386 393
387 394 RawCell.prototype = new TextCell();
388 395
389 396 /** @method bind_events **/
390 397 RawCell.prototype.bind_events = function () {
391 398 TextCell.prototype.bind_events.apply(this);
392 399 var that = this;
393 400 this.element.focusout(function() {
394 401 that.auto_highlight();
395 402 });
396 403 };
397 404
398 405 /**
399 406 * Trigger autodetection of highlight scheme for current cell
400 407 * @method auto_highlight
401 408 */
402 409 RawCell.prototype.auto_highlight = function () {
403 410 this._auto_highlight(IPython.config.raw_cell_highlight);
404 411 };
405 412
406 413 /** @method render **/
407 414 RawCell.prototype.render = function () {
408 415 // Make sure that this cell type can never be rendered
409 416 if (this.rendered) {
410 417 this.unrender();
411 418 }
412 419 var text = this.get_text();
413 420 if (text === "") { text = this.placeholder; }
414 421 this.set_text(text);
415 422 };
416 423
417 424
418 425 /**
419 426 * @class HeadingCell
420 427 * @extends IPython.TextCell
421 428 */
422 429
423 430 /**
424 431 * @constructor HeadingCell
425 432 * @extends IPython.TextCell
426 433 */
427 434 var HeadingCell = function (options) {
428 435 options = this.mergeopt(HeadingCell, options);
429 436
430 437 this.level = 1;
431 438 this.cell_type = 'heading';
432 439 TextCell.apply(this, [options]);
433 440
434 441 /**
435 442 * heading level of the cell, use getter and setter to access
436 443 * @property level
437 444 */
438 445 };
439 446
440 447 HeadingCell.options_default = {
441 448 placeholder: "Type Heading Here"
442 449 };
443 450
444 451 HeadingCell.prototype = new TextCell();
445 452
446 453 /** @method fromJSON */
447 454 HeadingCell.prototype.fromJSON = function (data) {
448 455 if (data.level !== undefined){
449 456 this.level = data.level;
450 457 }
451 458 TextCell.prototype.fromJSON.apply(this, arguments);
452 459 };
453 460
454 461
455 462 /** @method toJSON */
456 463 HeadingCell.prototype.toJSON = function () {
457 464 var data = TextCell.prototype.toJSON.apply(this);
458 465 data.level = this.get_level();
459 466 return data;
460 467 };
461 468
462 469 /**
463 470 * can the cell be split into two cells
464 471 * @method is_splittable
465 472 **/
466 473 HeadingCell.prototype.is_splittable = function () {
467 474 return false;
468 475 };
469 476
470 477
471 478 /**
472 479 * can the cell be merged with other cells
473 480 * @method is_mergeable
474 481 **/
475 482 HeadingCell.prototype.is_mergeable = function () {
476 483 return false;
477 484 };
478 485
479 486 /**
480 487 * Change heading level of cell, and re-render
481 488 * @method set_level
482 489 */
483 490 HeadingCell.prototype.set_level = function (level) {
484 491 this.level = level;
485 492 if (this.rendered) {
486 493 this.rendered = false;
487 494 this.render();
488 495 }
489 496 };
490 497
491 498 /** The depth of header cell, based on html (h1 to h6)
492 499 * @method get_level
493 500 * @return {integer} level - for 1 to 6
494 501 */
495 502 HeadingCell.prototype.get_level = function () {
496 503 return this.level;
497 504 };
498 505
499 506
500 507 HeadingCell.prototype.set_rendered = function (html) {
501 508 this.element.find("div.text_cell_render").html(html);
502 509 };
503 510
504 511
505 512 HeadingCell.prototype.get_rendered = function () {
506 513 var r = this.element.find("div.text_cell_render");
507 514 return r.children().first().html();
508 515 };
509 516
510 517
511 518 HeadingCell.prototype.render = function () {
512 519 var cont = IPython.TextCell.prototype.render.apply(this);
513 520 if (cont) {
514 521 var text = this.get_text();
515 522 var math = null;
516 523 // Markdown headings must be a single line
517 524 text = text.replace(/\n/g, ' ');
518 525 if (text === "") { text = this.placeholder; }
519 526 text = Array(this.level + 1).join("#") + " " + text;
520 527 var text_and_math = IPython.mathjaxutils.remove_math(text);
521 528 text = text_and_math[0];
522 529 math = text_and_math[1];
523 530 var html = marked.parser(marked.lexer(text));
524 531 var h = $(IPython.mathjaxutils.replace_math(html, math));
525 532 // add id and linkback anchor
526 533 var hash = h.text().replace(/ /g, '-');
527 534 h.attr('id', hash);
528 535 h.append(
529 536 $('<a/>')
530 537 .addClass('anchor-link')
531 538 .attr('href', '#' + hash)
532 539 .text('ΒΆ')
533 540 );
534
541 // TODO: This HTML needs to be treated as potentially dangerous
542 // user input and should be handled before set_rendered.
535 543 this.set_rendered(h);
536 544 this.typeset();
537 545 this.element.find('div.text_cell_input').hide();
538 546 this.element.find("div.text_cell_render").show();
539 547
540 548 }
541 549 return cont;
542 550 };
543 551
544 552 IPython.TextCell = TextCell;
545 553 IPython.MarkdownCell = MarkdownCell;
546 554 IPython.RawCell = RawCell;
547 555 IPython.HeadingCell = HeadingCell;
548 556
549 557
550 558 return IPython;
551 559
552 560 }(IPython));
553 561
@@ -1,390 +1,391 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7 //============================================================================
8 8 // Tooltip
9 9 //============================================================================
10 10 //
11 11 // you can set the autocall time by setting `IPython.tooltip.time_before_tooltip` in ms
12 12 //
13 13 // you can configure the differents action of pressing tab several times in a row by
14 14 // setting/appending different fonction in the array
15 15 // IPython.tooltip.tabs_functions
16 16 //
17 17 // eg :
18 18 // IPython.tooltip.tabs_functions[4] = function (){console.log('this is the action of the 4th tab pressing')}
19 19 //
20 20 var IPython = (function (IPython) {
21 21 "use strict";
22 22
23 23 var utils = IPython.utils;
24 24
25 25 // tooltip constructor
26 26 var Tooltip = function () {
27 27 var that = this;
28 28 this.time_before_tooltip = 1200;
29 29
30 30 // handle to html
31 31 this.tooltip = $('#tooltip');
32 32 this._hidden = true;
33 33
34 34 // variable for consecutive call
35 35 this._old_cell = null;
36 36 this._old_request = null;
37 37 this._consecutive_counter = 0;
38 38
39 39 // 'sticky ?'
40 40 this._sticky = false;
41 41
42 42 // display tooltip if the docstring is empty?
43 43 this._hide_if_no_docstring = false;
44 44
45 45 // contain the button in the upper right corner
46 46 this.buttons = $('<div/>').addClass('tooltipbuttons');
47 47
48 48 // will contain the docstring
49 49 this.text = $('<div/>').addClass('tooltiptext').addClass('smalltooltip');
50 50
51 51 // build the buttons menu on the upper right
52 52 // expand the tooltip to see more
53 53 var expandlink = $('<a/>').attr('href', "#").addClass("ui-corner-all") //rounded corner
54 54 .attr('role', "button").attr('id', 'expanbutton').attr('title', 'Grow the tooltip vertically (press tab 2 times)').click(function () {
55 55 that.expand();
56 56 }).append(
57 57 $('<span/>').text('Expand').addClass('ui-icon').addClass('ui-icon-plus'));
58 58
59 59 // open in pager
60 60 var morelink = $('<a/>').attr('href', "#").attr('role', "button").addClass('ui-button').attr('title', 'show the current docstring in pager (press tab 4 times)');
61 61 var morespan = $('<span/>').text('Open in Pager').addClass('ui-icon').addClass('ui-icon-arrowstop-l-n');
62 62 morelink.append(morespan);
63 63 morelink.click(function () {
64 64 that.showInPager(that._old_cell);
65 65 });
66 66
67 67 // close the tooltip
68 68 var closelink = $('<a/>').attr('href', "#").attr('role', "button").addClass('ui-button');
69 69 var closespan = $('<span/>').text('Close').addClass('ui-icon').addClass('ui-icon-close');
70 70 closelink.append(closespan);
71 71 closelink.click(function () {
72 72 that.remove_and_cancel_tooltip(true);
73 73 });
74 74
75 75 this._clocklink = $('<a/>').attr('href', "#");
76 76 this._clocklink.attr('role', "button");
77 77 this._clocklink.addClass('ui-button');
78 78 this._clocklink.attr('title', 'Tootip is not dismissed while typing for 10 seconds');
79 79 var clockspan = $('<span/>').text('Close');
80 80 clockspan.addClass('ui-icon');
81 81 clockspan.addClass('ui-icon-clock');
82 82 this._clocklink.append(clockspan);
83 83 this._clocklink.click(function () {
84 84 that.cancel_stick();
85 85 });
86 86
87 87
88 88
89 89
90 90 //construct the tooltip
91 91 // add in the reverse order you want them to appear
92 92 this.buttons.append(closelink);
93 93 this.buttons.append(expandlink);
94 94 this.buttons.append(morelink);
95 95 this.buttons.append(this._clocklink);
96 96 this._clocklink.hide();
97 97
98 98
99 99 // we need a phony element to make the small arrow
100 100 // of the tooltip in css
101 101 // we will move the arrow later
102 102 this.arrow = $('<div/>').addClass('pretooltiparrow');
103 103 this.tooltip.append(this.buttons);
104 104 this.tooltip.append(this.arrow);
105 105 this.tooltip.append(this.text);
106 106
107 107 // function that will be called if you press tab 1, 2, 3... times in a row
108 108 this.tabs_functions = [function (cell, text) {
109 109 that._request_tooltip(cell, text);
110 110 }, function () {
111 111 that.expand();
112 112 }, function () {
113 113 that.stick();
114 114 }, function (cell) {
115 115 that.cancel_stick();
116 116 that.showInPager(cell);
117 117 }];
118 118 // call after all the tabs function above have bee call to clean their effects
119 119 // if necessary
120 120 this.reset_tabs_function = function (cell, text) {
121 121 this._old_cell = (cell) ? cell : null;
122 122 this._old_request = (text) ? text : null;
123 123 this._consecutive_counter = 0;
124 124 };
125 125 };
126 126
127 127 Tooltip.prototype.is_visible = function () {
128 128 return !this._hidden;
129 129 };
130 130
131 131 Tooltip.prototype.showInPager = function (cell) {
132 132 // reexecute last call in pager by appending ? to show back in pager
133 133 var that = this;
134 134 var empty = function () {};
135 135 cell.kernel.execute(
136 136 that.name + '?', {
137 137 'execute_reply': empty,
138 138 'output': empty,
139 139 'clear_output': empty,
140 140 'cell': cell
141 141 }, {
142 142 'silent': false,
143 143 'store_history': true
144 144 });
145 145 this.remove_and_cancel_tooltip();
146 146 };
147 147
148 148 // grow the tooltip verticaly
149 149 Tooltip.prototype.expand = function () {
150 150 this.text.removeClass('smalltooltip');
151 151 this.text.addClass('bigtooltip');
152 152 $('#expanbutton').hide('slow');
153 153 };
154 154
155 155 // deal with all the logic of hiding the tooltip
156 156 // and reset it's status
157 157 Tooltip.prototype._hide = function () {
158 158 this._hidden = true;
159 159 this.tooltip.fadeOut('fast');
160 160 $('#expanbutton').show('slow');
161 161 this.text.removeClass('bigtooltip');
162 162 this.text.addClass('smalltooltip');
163 163 // keep scroll top to be sure to always see the first line
164 164 this.text.scrollTop(0);
165 165 this.code_mirror = null;
166 166 };
167 167
168 168 // return true on successfully removing a visible tooltip; otherwise return
169 169 // false.
170 170 Tooltip.prototype.remove_and_cancel_tooltip = function (force) {
171 171 // note that we don't handle closing directly inside the calltip
172 172 // as in the completer, because it is not focusable, so won't
173 173 // get the event.
174 174 this.cancel_pending();
175 175 if (!this._hidden) {
176 176 if (force || !this._sticky) {
177 177 this.cancel_stick();
178 178 this._hide();
179 179 }
180 180 this.reset_tabs_function();
181 181 return true;
182 182 } else {
183 183 return false;
184 184 }
185 185 };
186 186
187 187 // cancel autocall done after '(' for example.
188 188 Tooltip.prototype.cancel_pending = function () {
189 189 if (this._tooltip_timeout !== null) {
190 190 clearTimeout(this._tooltip_timeout);
191 191 this._tooltip_timeout = null;
192 192 }
193 193 };
194 194
195 195 // will trigger tooltip after timeout
196 196 Tooltip.prototype.pending = function (cell, hide_if_no_docstring) {
197 197 var that = this;
198 198 this._tooltip_timeout = setTimeout(function () {
199 199 that.request(cell, hide_if_no_docstring);
200 200 }, that.time_before_tooltip);
201 201 };
202 202
203 203 // easy access for julia monkey patching.
204 204 Tooltip.last_token_re = /[a-z_][0-9a-z._]*$/gi;
205 205
206 206 Tooltip.prototype.extract_oir_token = function(line){
207 207 // use internally just to make the request to the kernel
208 208 // Feel free to shorten this logic if you are better
209 209 // than me in regEx
210 210 // basicaly you shoul be able to get xxx.xxx.xxx from
211 211 // something(range(10), kwarg=smth) ; xxx.xxx.xxx( firstarg, rand(234,23), kwarg1=2,
212 212 // remove everything between matchin bracket (need to iterate)
213 213 var matchBracket = /\([^\(\)]+\)/g;
214 214 var endBracket = /\([^\(]*$/g;
215 215 var oldline = line;
216 216
217 217 line = line.replace(matchBracket, "");
218 218 while (oldline != line) {
219 219 oldline = line;
220 220 line = line.replace(matchBracket, "");
221 221 }
222 222 // remove everything after last open bracket
223 223 line = line.replace(endBracket, "");
224 224 // reset the regex object
225 225 Tooltip.last_token_re.lastIndex = 0;
226 226 return Tooltip.last_token_re.exec(line);
227 227 };
228 228
229 229 Tooltip.prototype._request_tooltip = function (cell, line) {
230 230 var callbacks = $.proxy(this._show, this);
231 231 var oir_token = this.extract_oir_token(line);
232 232 var msg_id = cell.kernel.object_info(oir_token, callbacks);
233 233 };
234 234
235 235 // make an imediate completion request
236 236 Tooltip.prototype.request = function (cell, hide_if_no_docstring) {
237 237 // request(codecell)
238 238 // Deal with extracting the text from the cell and counting
239 239 // call in a row
240 240 this.cancel_pending();
241 241 var editor = cell.code_mirror;
242 242 var cursor = editor.getCursor();
243 243 var text = editor.getRange({
244 244 line: cursor.line,
245 245 ch: 0
246 246 }, cursor).trim();
247 247
248 248 this._hide_if_no_docstring = hide_if_no_docstring;
249 249
250 250 if(editor.somethingSelected()){
251 251 text = editor.getSelection();
252 252 }
253 253
254 254 // need a permanent handel to code_mirror for future auto recall
255 255 this.code_mirror = editor;
256 256
257 257 // now we treat the different number of keypress
258 258 // first if same cell, same text, increment counter by 1
259 259 if (this._old_cell == cell && this._old_request == text && this._hidden === false) {
260 260 this._consecutive_counter++;
261 261 } else {
262 262 // else reset
263 263 this.cancel_stick();
264 264 this.reset_tabs_function (cell, text);
265 265 }
266 266
267 267 // don't do anything if line beggin with '(' or is empty
268 268 if (text === "" || text === "(") {
269 269 return;
270 270 }
271 271
272 272 this.tabs_functions[this._consecutive_counter](cell, text);
273 273
274 274 // then if we are at the end of list function, reset
275 275 if (this._consecutive_counter == this.tabs_functions.length) {
276 276 this.reset_tabs_function (cell, text);
277 277 }
278 278
279 279 return;
280 280 };
281 281
282 282 // cancel the option of having the tooltip to stick
283 283 Tooltip.prototype.cancel_stick = function () {
284 284 clearTimeout(this._stick_timeout);
285 285 this._stick_timeout = null;
286 286 this._clocklink.hide('slow');
287 287 this._sticky = false;
288 288 };
289 289
290 290 // put the tooltip in a sicky state for 10 seconds
291 291 // it won't be removed by remove_and_cancell() unless you called with
292 292 // the first parameter set to true.
293 293 // remove_and_cancell_tooltip(true)
294 294 Tooltip.prototype.stick = function (time) {
295 295 time = (time !== undefined) ? time : 10;
296 296 var that = this;
297 297 this._sticky = true;
298 298 this._clocklink.show('slow');
299 299 this._stick_timeout = setTimeout(function () {
300 300 that._sticky = false;
301 301 that._clocklink.hide('slow');
302 302 }, time * 1000);
303 303 };
304 304
305 305 // should be called with the kernel reply to actually show the tooltip
306 306 Tooltip.prototype._show = function (reply) {
307 307 // move the bubble if it is not hidden
308 308 // otherwise fade it
309 309 var content = reply.content;
310 310 if (!content.found) {
311 311 // object not found, nothing to show
312 312 return;
313 313 }
314 314 this.name = content.name;
315 315
316 316 // do some math to have the tooltip arrow on more or less on left or right
317 317 // width of the editor
318 318 var w = $(this.code_mirror.getScrollerElement()).width();
319 319 // ofset of the editor
320 320 var o = $(this.code_mirror.getScrollerElement()).offset();
321 321
322 322 // whatever anchor/head order but arrow at mid x selection
323 323 var anchor = this.code_mirror.cursorCoords(false);
324 324 var head = this.code_mirror.cursorCoords(true);
325 325 var xinit = (head.left+anchor.left)/2;
326 326 var xinter = o.left + (xinit - o.left) / w * (w - 450);
327 327 var posarrowleft = xinit - xinter;
328 328
329 329 if (this._hidden === false) {
330 330 this.tooltip.animate({
331 331 'left': xinter - 30 + 'px',
332 332 'top': (head.bottom + 10) + 'px'
333 333 });
334 334 } else {
335 335 this.tooltip.css({
336 336 'left': xinter - 30 + 'px'
337 337 });
338 338 this.tooltip.css({
339 339 'top': (head.bottom + 10) + 'px'
340 340 });
341 341 }
342 342 this.arrow.animate({
343 343 'left': posarrowleft + 'px'
344 344 });
345 345
346 346 // build docstring
347 347 var defstring = content.call_def;
348 348 if (!defstring) {
349 349 defstring = content.init_definition;
350 350 }
351 351 if (!defstring) {
352 352 defstring = content.definition;
353 353 }
354 354
355 355 var docstring = content.call_docstring;
356 356 if (!docstring) {
357 357 docstring = content.init_docstring;
358 358 }
359 359 if (!docstring) {
360 360 docstring = content.docstring;
361 361 }
362 362
363 363 if (!docstring) {
364 364 // For reals this time, no docstring
365 365 if (this._hide_if_no_docstring) {
366 366 return;
367 367 } else {
368 368 docstring = "<empty docstring>";
369 369 }
370 370 }
371 371
372 372 this._hidden = false;
373 373 this.tooltip.fadeIn('fast');
374 374 this.text.children().remove();
375 375
376 // Any HTML within the docstring is escaped by the fixConsole() method.
376 377 var pre = $('<pre/>').html(utils.fixConsole(docstring));
377 378 if (defstring) {
378 379 var defstring_html = $('<pre/>').html(utils.fixConsole(defstring));
379 380 this.text.append(defstring_html);
380 381 }
381 382 this.text.append(pre);
382 383 // keep scroll top to be sure to always see the first line
383 384 this.text.scrollTop(0);
384 385 };
385 386
386 387 IPython.Tooltip = Tooltip;
387 388
388 389 return IPython;
389 390
390 391 }(IPython));
General Comments 0
You need to be logged in to leave comments. Login now