##// END OF EJS Templates
moving typeset to utils, usage in cell and outputarea
Nicholas Bollweg (Nick) -
Show More
@@ -1,808 +1,836 b''
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 'codemirror/lib/codemirror',
8 8 ], function(IPython, $, CodeMirror){
9 9 "use strict";
10 10
11 11 IPython.load_extensions = function () {
12 12 // load one or more IPython notebook extensions with requirejs
13 13
14 14 var extensions = [];
15 15 var extension_names = arguments;
16 16 for (var i = 0; i < extension_names.length; i++) {
17 17 extensions.push("nbextensions/" + arguments[i]);
18 18 }
19 19
20 20 require(extensions,
21 21 function () {
22 22 for (var i = 0; i < arguments.length; i++) {
23 23 var ext = arguments[i];
24 24 var ext_name = extension_names[i];
25 25 // success callback
26 26 console.log("Loaded extension: " + ext_name);
27 27 if (ext && ext.load_ipython_extension !== undefined) {
28 28 ext.load_ipython_extension();
29 29 }
30 30 }
31 31 },
32 32 function (err) {
33 33 // failure callback
34 34 console.log("Failed to load extension(s):", err.requireModules, err);
35 35 }
36 36 );
37 37 };
38 38
39 39 //============================================================================
40 40 // Cross-browser RegEx Split
41 41 //============================================================================
42 42
43 43 // This code has been MODIFIED from the code licensed below to not replace the
44 44 // default browser split. The license is reproduced here.
45 45
46 46 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
47 47 /*!
48 48 * Cross-Browser Split 1.1.1
49 49 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
50 50 * Available under the MIT License
51 51 * ECMAScript compliant, uniform cross-browser split method
52 52 */
53 53
54 54 /**
55 55 * Splits a string into an array of strings using a regex or string
56 56 * separator. Matches of the separator are not included in the result array.
57 57 * However, if `separator` is a regex that contains capturing groups,
58 58 * backreferences are spliced into the result each time `separator` is
59 59 * matched. Fixes browser bugs compared to the native
60 60 * `String.prototype.split` and can be used reliably cross-browser.
61 61 * @param {String} str String to split.
62 62 * @param {RegExp|String} separator Regex or string to use for separating
63 63 * the string.
64 64 * @param {Number} [limit] Maximum number of items to include in the result
65 65 * array.
66 66 * @returns {Array} Array of substrings.
67 67 * @example
68 68 *
69 69 * // Basic use
70 70 * regex_split('a b c d', ' ');
71 71 * // -> ['a', 'b', 'c', 'd']
72 72 *
73 73 * // With limit
74 74 * regex_split('a b c d', ' ', 2);
75 75 * // -> ['a', 'b']
76 76 *
77 77 * // Backreferences in result array
78 78 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
79 79 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
80 80 */
81 81 var regex_split = function (str, separator, limit) {
82 82 // If `separator` is not a regex, use `split`
83 83 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
84 84 return split.call(str, separator, limit);
85 85 }
86 86 var output = [],
87 87 flags = (separator.ignoreCase ? "i" : "") +
88 88 (separator.multiline ? "m" : "") +
89 89 (separator.extended ? "x" : "") + // Proposed for ES6
90 90 (separator.sticky ? "y" : ""), // Firefox 3+
91 91 lastLastIndex = 0,
92 92 // Make `global` and avoid `lastIndex` issues by working with a copy
93 93 separator = new RegExp(separator.source, flags + "g"),
94 94 separator2, match, lastIndex, lastLength;
95 95 str += ""; // Type-convert
96 96
97 97 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
98 98 if (!compliantExecNpcg) {
99 99 // Doesn't need flags gy, but they don't hurt
100 100 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
101 101 }
102 102 /* Values for `limit`, per the spec:
103 103 * If undefined: 4294967295 // Math.pow(2, 32) - 1
104 104 * If 0, Infinity, or NaN: 0
105 105 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
106 106 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
107 107 * If other: Type-convert, then use the above rules
108 108 */
109 109 limit = typeof(limit) === "undefined" ?
110 110 -1 >>> 0 : // Math.pow(2, 32) - 1
111 111 limit >>> 0; // ToUint32(limit)
112 112 while (match = separator.exec(str)) {
113 113 // `separator.lastIndex` is not reliable cross-browser
114 114 lastIndex = match.index + match[0].length;
115 115 if (lastIndex > lastLastIndex) {
116 116 output.push(str.slice(lastLastIndex, match.index));
117 117 // Fix browsers whose `exec` methods don't consistently return `undefined` for
118 118 // nonparticipating capturing groups
119 119 if (!compliantExecNpcg && match.length > 1) {
120 120 match[0].replace(separator2, function () {
121 121 for (var i = 1; i < arguments.length - 2; i++) {
122 122 if (typeof(arguments[i]) === "undefined") {
123 123 match[i] = undefined;
124 124 }
125 125 }
126 126 });
127 127 }
128 128 if (match.length > 1 && match.index < str.length) {
129 129 Array.prototype.push.apply(output, match.slice(1));
130 130 }
131 131 lastLength = match[0].length;
132 132 lastLastIndex = lastIndex;
133 133 if (output.length >= limit) {
134 134 break;
135 135 }
136 136 }
137 137 if (separator.lastIndex === match.index) {
138 138 separator.lastIndex++; // Avoid an infinite loop
139 139 }
140 140 }
141 141 if (lastLastIndex === str.length) {
142 142 if (lastLength || !separator.test("")) {
143 143 output.push("");
144 144 }
145 145 } else {
146 146 output.push(str.slice(lastLastIndex));
147 147 }
148 148 return output.length > limit ? output.slice(0, limit) : output;
149 149 };
150 150
151 151 //============================================================================
152 152 // End contributed Cross-browser RegEx Split
153 153 //============================================================================
154 154
155 155
156 156 var uuid = function () {
157 157 /**
158 158 * http://www.ietf.org/rfc/rfc4122.txt
159 159 */
160 160 var s = [];
161 161 var hexDigits = "0123456789ABCDEF";
162 162 for (var i = 0; i < 32; i++) {
163 163 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
164 164 }
165 165 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
166 166 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
167 167
168 168 var uuid = s.join("");
169 169 return uuid;
170 170 };
171 171
172 172
173 173 //Fix raw text to parse correctly in crazy XML
174 174 function xmlencode(string) {
175 175 return string.replace(/\&/g,'&'+'amp;')
176 176 .replace(/</g,'&'+'lt;')
177 177 .replace(/>/g,'&'+'gt;')
178 178 .replace(/\'/g,'&'+'apos;')
179 179 .replace(/\"/g,'&'+'quot;')
180 180 .replace(/`/g,'&'+'#96;');
181 181 }
182 182
183 183
184 184 //Map from terminal commands to CSS classes
185 185 var ansi_colormap = {
186 186 "01":"ansibold",
187 187
188 188 "30":"ansiblack",
189 189 "31":"ansired",
190 190 "32":"ansigreen",
191 191 "33":"ansiyellow",
192 192 "34":"ansiblue",
193 193 "35":"ansipurple",
194 194 "36":"ansicyan",
195 195 "37":"ansigray",
196 196
197 197 "40":"ansibgblack",
198 198 "41":"ansibgred",
199 199 "42":"ansibggreen",
200 200 "43":"ansibgyellow",
201 201 "44":"ansibgblue",
202 202 "45":"ansibgpurple",
203 203 "46":"ansibgcyan",
204 204 "47":"ansibggray"
205 205 };
206 206
207 207 function _process_numbers(attrs, numbers) {
208 208 // process ansi escapes
209 209 var n = numbers.shift();
210 210 if (ansi_colormap[n]) {
211 211 if ( ! attrs["class"] ) {
212 212 attrs["class"] = ansi_colormap[n];
213 213 } else {
214 214 attrs["class"] += " " + ansi_colormap[n];
215 215 }
216 216 } else if (n == "38" || n == "48") {
217 217 // VT100 256 color or 24 bit RGB
218 218 if (numbers.length < 2) {
219 219 console.log("Not enough fields for VT100 color", numbers);
220 220 return;
221 221 }
222 222
223 223 var index_or_rgb = numbers.shift();
224 224 var r,g,b;
225 225 if (index_or_rgb == "5") {
226 226 // 256 color
227 227 var idx = parseInt(numbers.shift());
228 228 if (idx < 16) {
229 229 // indexed ANSI
230 230 // ignore bright / non-bright distinction
231 231 idx = idx % 8;
232 232 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
233 233 if ( ! attrs["class"] ) {
234 234 attrs["class"] = ansiclass;
235 235 } else {
236 236 attrs["class"] += " " + ansiclass;
237 237 }
238 238 return;
239 239 } else if (idx < 232) {
240 240 // 216 color 6x6x6 RGB
241 241 idx = idx - 16;
242 242 b = idx % 6;
243 243 g = Math.floor(idx / 6) % 6;
244 244 r = Math.floor(idx / 36) % 6;
245 245 // convert to rgb
246 246 r = (r * 51);
247 247 g = (g * 51);
248 248 b = (b * 51);
249 249 } else {
250 250 // grayscale
251 251 idx = idx - 231;
252 252 // it's 1-24 and should *not* include black or white,
253 253 // so a 26 point scale
254 254 r = g = b = Math.floor(idx * 256 / 26);
255 255 }
256 256 } else if (index_or_rgb == "2") {
257 257 // Simple 24 bit RGB
258 258 if (numbers.length > 3) {
259 259 console.log("Not enough fields for RGB", numbers);
260 260 return;
261 261 }
262 262 r = numbers.shift();
263 263 g = numbers.shift();
264 264 b = numbers.shift();
265 265 } else {
266 266 console.log("unrecognized control", numbers);
267 267 return;
268 268 }
269 269 if (r !== undefined) {
270 270 // apply the rgb color
271 271 var line;
272 272 if (n == "38") {
273 273 line = "color: ";
274 274 } else {
275 275 line = "background-color: ";
276 276 }
277 277 line = line + "rgb(" + r + "," + g + "," + b + ");";
278 278 if ( !attrs.style ) {
279 279 attrs.style = line;
280 280 } else {
281 281 attrs.style += " " + line;
282 282 }
283 283 }
284 284 }
285 285 }
286 286
287 287 function ansispan(str) {
288 288 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
289 289 // regular ansi escapes (using the table above)
290 290 var is_open = false;
291 291 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
292 292 if (!pattern) {
293 293 // [(01|22|39|)m close spans
294 294 if (is_open) {
295 295 is_open = false;
296 296 return "</span>";
297 297 } else {
298 298 return "";
299 299 }
300 300 } else {
301 301 is_open = true;
302 302
303 303 // consume sequence of color escapes
304 304 var numbers = pattern.match(/\d+/g);
305 305 var attrs = {};
306 306 while (numbers.length > 0) {
307 307 _process_numbers(attrs, numbers);
308 308 }
309 309
310 310 var span = "<span ";
311 311 for (var attr in attrs) {
312 312 var value = attrs[attr];
313 313 span = span + " " + attr + '="' + attrs[attr] + '"';
314 314 }
315 315 return span + ">";
316 316 }
317 317 });
318 318 }
319 319
320 320 // Transform ANSI color escape codes into HTML <span> tags with css
321 321 // classes listed in the above ansi_colormap object. The actual color used
322 322 // are set in the css file.
323 323 function fixConsole(txt) {
324 324 txt = xmlencode(txt);
325 325 var re = /\033\[([\dA-Fa-f;]*?)m/;
326 326 var opened = false;
327 327 var cmds = [];
328 328 var opener = "";
329 329 var closer = "";
330 330
331 331 // Strip all ANSI codes that are not color related. Matches
332 332 // all ANSI codes that do not end with "m".
333 333 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
334 334 txt = txt.replace(ignored_re, "");
335 335
336 336 // color ansi codes
337 337 txt = ansispan(txt);
338 338 return txt;
339 339 }
340 340
341 341 // Remove chunks that should be overridden by the effect of
342 342 // carriage return characters
343 343 function fixCarriageReturn(txt) {
344 344 var tmp = txt;
345 345 do {
346 346 txt = tmp;
347 347 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
348 348 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
349 349 } while (tmp.length < txt.length);
350 350 return txt;
351 351 }
352 352
353 353 // Locate any URLs and convert them to a anchor tag
354 354 function autoLinkUrls(txt) {
355 355 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
356 356 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
357 357 }
358 358
359 359 var points_to_pixels = function (points) {
360 360 /**
361 361 * A reasonably good way of converting between points and pixels.
362 362 */
363 363 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
364 364 $(body).append(test);
365 365 var pixel_per_point = test.width()/10000;
366 366 test.remove();
367 367 return Math.floor(points*pixel_per_point);
368 368 };
369 369
370 370 var always_new = function (constructor) {
371 371 /**
372 372 * wrapper around contructor to avoid requiring `var a = new constructor()`
373 373 * useful for passing constructors as callbacks,
374 374 * not for programmer laziness.
375 375 * from http://programmers.stackexchange.com/questions/118798
376 376 */
377 377 return function () {
378 378 var obj = Object.create(constructor.prototype);
379 379 constructor.apply(obj, arguments);
380 380 return obj;
381 381 };
382 382 };
383 383
384 384 var url_path_join = function () {
385 385 /**
386 386 * join a sequence of url components with '/'
387 387 */
388 388 var url = '';
389 389 for (var i = 0; i < arguments.length; i++) {
390 390 if (arguments[i] === '') {
391 391 continue;
392 392 }
393 393 if (url.length > 0 && url[url.length-1] != '/') {
394 394 url = url + '/' + arguments[i];
395 395 } else {
396 396 url = url + arguments[i];
397 397 }
398 398 }
399 399 url = url.replace(/\/\/+/, '/');
400 400 return url;
401 401 };
402 402
403 403 var url_path_split = function (path) {
404 404 /**
405 405 * Like os.path.split for URLs.
406 406 * Always returns two strings, the directory path and the base filename
407 407 */
408 408
409 409 var idx = path.lastIndexOf('/');
410 410 if (idx === -1) {
411 411 return ['', path];
412 412 } else {
413 413 return [ path.slice(0, idx), path.slice(idx + 1) ];
414 414 }
415 415 };
416 416
417 417 var parse_url = function (url) {
418 418 /**
419 419 * an `a` element with an href allows attr-access to the parsed segments of a URL
420 420 * a = parse_url("http://localhost:8888/path/name#hash")
421 421 * a.protocol = "http:"
422 422 * a.host = "localhost:8888"
423 423 * a.hostname = "localhost"
424 424 * a.port = 8888
425 425 * a.pathname = "/path/name"
426 426 * a.hash = "#hash"
427 427 */
428 428 var a = document.createElement("a");
429 429 a.href = url;
430 430 return a;
431 431 };
432 432
433 433 var encode_uri_components = function (uri) {
434 434 /**
435 435 * encode just the components of a multi-segment uri,
436 436 * leaving '/' separators
437 437 */
438 438 return uri.split('/').map(encodeURIComponent).join('/');
439 439 };
440 440
441 441 var url_join_encode = function () {
442 442 /**
443 443 * join a sequence of url components with '/',
444 444 * encoding each component with encodeURIComponent
445 445 */
446 446 return encode_uri_components(url_path_join.apply(null, arguments));
447 447 };
448 448
449 449
450 450 var splitext = function (filename) {
451 451 /**
452 452 * mimic Python os.path.splitext
453 453 * Returns ['base', '.ext']
454 454 */
455 455 var idx = filename.lastIndexOf('.');
456 456 if (idx > 0) {
457 457 return [filename.slice(0, idx), filename.slice(idx)];
458 458 } else {
459 459 return [filename, ''];
460 460 }
461 461 };
462 462
463 463
464 464 var escape_html = function (text) {
465 465 /**
466 466 * escape text to HTML
467 467 */
468 468 return $("<div/>").text(text).html();
469 469 };
470 470
471 471
472 472 var get_body_data = function(key) {
473 473 /**
474 474 * get a url-encoded item from body.data and decode it
475 475 * we should never have any encoded URLs anywhere else in code
476 476 * until we are building an actual request
477 477 */
478 478 return decodeURIComponent($('body').data(key));
479 479 };
480 480
481 481 var to_absolute_cursor_pos = function (cm, cursor) {
482 482 /**
483 483 * get the absolute cursor position from CodeMirror's col, ch
484 484 */
485 485 if (!cursor) {
486 486 cursor = cm.getCursor();
487 487 }
488 488 var cursor_pos = cursor.ch;
489 489 for (var i = 0; i < cursor.line; i++) {
490 490 cursor_pos += cm.getLine(i).length + 1;
491 491 }
492 492 return cursor_pos;
493 493 };
494 494
495 495 var from_absolute_cursor_pos = function (cm, cursor_pos) {
496 496 /**
497 497 * turn absolute cursor postion into CodeMirror col, ch cursor
498 498 */
499 499 var i, line;
500 500 var offset = 0;
501 501 for (i = 0, line=cm.getLine(i); line !== undefined; i++, line=cm.getLine(i)) {
502 502 if (offset + line.length < cursor_pos) {
503 503 offset += line.length + 1;
504 504 } else {
505 505 return {
506 506 line : i,
507 507 ch : cursor_pos - offset,
508 508 };
509 509 }
510 510 }
511 511 // reached end, return endpoint
512 512 return {
513 513 ch : line.length - 1,
514 514 line : i - 1,
515 515 };
516 516 };
517 517
518 518 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
519 519 var browser = (function() {
520 520 if (typeof navigator === 'undefined') {
521 521 // navigator undefined in node
522 522 return 'None';
523 523 }
524 524 var N= navigator.appName, ua= navigator.userAgent, tem;
525 525 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
526 526 if (M && (tem= ua.match(/version\/([\.\d]+)/i)) !== null) M[2]= tem[1];
527 527 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
528 528 return M;
529 529 })();
530 530
531 531 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
532 532 var platform = (function () {
533 533 if (typeof navigator === 'undefined') {
534 534 // navigator undefined in node
535 535 return 'None';
536 536 }
537 537 var OSName="None";
538 538 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
539 539 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
540 540 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
541 541 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
542 542 return OSName;
543 543 })();
544 544
545 545 var is_or_has = function (a, b) {
546 546 /**
547 547 * Is b a child of a or a itself?
548 548 */
549 549 return a.has(b).length !==0 || a.is(b);
550 550 };
551 551
552 552 var is_focused = function (e) {
553 553 /**
554 554 * Is element e, or one of its children focused?
555 555 */
556 556 e = $(e);
557 557 var target = $(document.activeElement);
558 558 if (target.length > 0) {
559 559 if (is_or_has(e, target)) {
560 560 return true;
561 561 } else {
562 562 return false;
563 563 }
564 564 } else {
565 565 return false;
566 566 }
567 567 };
568 568
569 569 var mergeopt = function(_class, options, overwrite){
570 570 options = options || {};
571 571 overwrite = overwrite || {};
572 572 return $.extend(true, {}, _class.options_default, options, overwrite);
573 573 };
574 574
575 575 var ajax_error_msg = function (jqXHR) {
576 576 /**
577 577 * Return a JSON error message if there is one,
578 578 * otherwise the basic HTTP status text.
579 579 */
580 580 if (jqXHR.responseJSON && jqXHR.responseJSON.traceback) {
581 581 return jqXHR.responseJSON.traceback;
582 582 } else if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
583 583 return jqXHR.responseJSON.message;
584 584 } else {
585 585 return jqXHR.statusText;
586 586 }
587 587 };
588 588 var log_ajax_error = function (jqXHR, status, error) {
589 589 /**
590 590 * log ajax failures with informative messages
591 591 */
592 592 var msg = "API request failed (" + jqXHR.status + "): ";
593 593 console.log(jqXHR);
594 594 msg += ajax_error_msg(jqXHR);
595 595 console.log(msg);
596 596 };
597 597
598 598 var requireCodeMirrorMode = function (mode, callback, errback) {
599 599 /**
600 600 * load a mode with requirejs
601 601 */
602 602 if (typeof mode != "string") mode = mode.name;
603 603 if (CodeMirror.modes.hasOwnProperty(mode)) {
604 604 callback(CodeMirror.modes.mode);
605 605 return;
606 606 }
607 607 require([
608 608 // might want to use CodeMirror.modeURL here
609 609 ['codemirror/mode', mode, mode].join('/'),
610 610 ], callback, errback
611 611 );
612 612 };
613 613
614 614 /** Error type for wrapped XHR errors. */
615 615 var XHR_ERROR = 'XhrError';
616 616
617 617 /**
618 618 * Wraps an AJAX error as an Error object.
619 619 */
620 620 var wrap_ajax_error = function (jqXHR, status, error) {
621 621 var wrapped_error = new Error(ajax_error_msg(jqXHR));
622 622 wrapped_error.name = XHR_ERROR;
623 623 // provide xhr response
624 624 wrapped_error.xhr = jqXHR;
625 625 wrapped_error.xhr_status = status;
626 626 wrapped_error.xhr_error = error;
627 627 return wrapped_error;
628 628 };
629 629
630 630 var promising_ajax = function(url, settings) {
631 631 /**
632 632 * Like $.ajax, but returning an ES6 promise. success and error settings
633 633 * will be ignored.
634 634 */
635 635 return new Promise(function(resolve, reject) {
636 636 settings.success = function(data, status, jqXHR) {
637 637 resolve(data);
638 638 };
639 639 settings.error = function(jqXHR, status, error) {
640 640 log_ajax_error(jqXHR, status, error);
641 641 reject(wrap_ajax_error(jqXHR, status, error));
642 642 };
643 643 $.ajax(url, settings);
644 644 });
645 645 };
646 646
647 647 var WrappedError = function(message, error){
648 648 /**
649 649 * Wrappable Error class
650 650 *
651 651 * The Error class doesn't actually act on `this`. Instead it always
652 652 * returns a new instance of Error. Here we capture that instance so we
653 653 * can apply it's properties to `this`.
654 654 */
655 655 var tmp = Error.apply(this, [message]);
656 656
657 657 // Copy the properties of the error over to this.
658 658 var properties = Object.getOwnPropertyNames(tmp);
659 659 for (var i = 0; i < properties.length; i++) {
660 660 this[properties[i]] = tmp[properties[i]];
661 661 }
662 662
663 663 // Keep a stack of the original error messages.
664 664 if (error instanceof WrappedError) {
665 665 this.error_stack = error.error_stack;
666 666 } else {
667 667 this.error_stack = [error];
668 668 }
669 669 this.error_stack.push(tmp);
670 670
671 671 return this;
672 672 };
673 673
674 674 WrappedError.prototype = Object.create(Error.prototype, {});
675 675
676 676
677 677 var load_class = function(class_name, module_name, registry) {
678 678 /**
679 679 * Tries to load a class
680 680 *
681 681 * Tries to load a class from a module using require.js, if a module
682 682 * is specified, otherwise tries to load a class from the global
683 683 * registry, if the global registry is provided.
684 684 */
685 685 return new Promise(function(resolve, reject) {
686 686
687 687 // Try loading the view module using require.js
688 688 if (module_name) {
689 689 require([module_name], function(module) {
690 690 if (module[class_name] === undefined) {
691 691 reject(new Error('Class '+class_name+' not found in module '+module_name));
692 692 } else {
693 693 resolve(module[class_name]);
694 694 }
695 695 }, reject);
696 696 } else {
697 697 if (registry && registry[class_name]) {
698 698 resolve(registry[class_name]);
699 699 } else {
700 700 reject(new Error('Class '+class_name+' not found in registry '));
701 701 }
702 702 }
703 703 });
704 704 };
705 705
706 706 var resolve_promises_dict = function(d) {
707 707 /**
708 708 * Resolve a promiseful dictionary.
709 709 * Returns a single Promise.
710 710 */
711 711 var keys = Object.keys(d);
712 712 var values = [];
713 713 keys.forEach(function(key) {
714 714 values.push(d[key]);
715 715 });
716 716 return Promise.all(values).then(function(v) {
717 717 d = {};
718 718 for(var i=0; i<keys.length; i++) {
719 719 d[keys[i]] = v[i];
720 720 }
721 721 return d;
722 722 });
723 723 };
724 724
725 725 var WrappedError = function(message, error){
726 726 /**
727 727 * Wrappable Error class
728 728 *
729 729 * The Error class doesn't actually act on `this`. Instead it always
730 730 * returns a new instance of Error. Here we capture that instance so we
731 731 * can apply it's properties to `this`.
732 732 */
733 733 var tmp = Error.apply(this, [message]);
734 734
735 735 // Copy the properties of the error over to this.
736 736 var properties = Object.getOwnPropertyNames(tmp);
737 737 for (var i = 0; i < properties.length; i++) {
738 738 this[properties[i]] = tmp[properties[i]];
739 739 }
740 740
741 741 // Keep a stack of the original error messages.
742 742 if (error instanceof WrappedError) {
743 743 this.error_stack = error.error_stack;
744 744 } else {
745 745 this.error_stack = [error];
746 746 }
747 747 this.error_stack.push(tmp);
748 748
749 749 return this;
750 750 };
751 751
752 752 WrappedError.prototype = Object.create(Error.prototype, {});
753 753
754 754 var reject = function(message, log) {
755 755 /**
756 756 * Creates a wrappable Promise rejection function.
757 757 *
758 758 * Creates a function that returns a Promise.reject with a new WrappedError
759 759 * that has the provided message and wraps the original error that
760 760 * caused the promise to reject.
761 761 */
762 762 return function(error) {
763 763 var wrapped_error = new WrappedError(message, error);
764 764 if (log) console.error(wrapped_error);
765 765 return Promise.reject(wrapped_error);
766 766 };
767 767 };
768 768
769 var typeset = function(element, text) {
770 /**
771 * Apply MathJax rendering to an element, and optionally set its text
772 *
773 * If MathJax is not available, make no changes.
774 *
775 * Returns the output any number of typeset elements, or undefined if
776 * MathJax was not available.
777 *
778 * Parameters
779 * ----------
780 * element: Node, NodeList, or jQuery selection
781 * text: option string
782 */
783 if(!window.MathJax){
784 return;
785 }
786 var $el = element.jquery ? element : $(element);
787 if(arguments.length > 1){
788 $el.text(text);
789 }
790 return $el.map(function(){
791 // MathJax takes a DOM node: $.map makes `this` the context
792 return MathJax.Hub.Queue(["Typeset", MathJax.Hub, this]);
793 });
794 };
795
769 796 var utils = {
770 797 regex_split : regex_split,
771 798 uuid : uuid,
772 799 fixConsole : fixConsole,
773 800 fixCarriageReturn : fixCarriageReturn,
774 801 autoLinkUrls : autoLinkUrls,
775 802 points_to_pixels : points_to_pixels,
776 803 get_body_data : get_body_data,
777 804 parse_url : parse_url,
778 805 url_path_split : url_path_split,
779 806 url_path_join : url_path_join,
780 807 url_join_encode : url_join_encode,
781 808 encode_uri_components : encode_uri_components,
782 809 splitext : splitext,
783 810 escape_html : escape_html,
784 811 always_new : always_new,
785 812 to_absolute_cursor_pos : to_absolute_cursor_pos,
786 813 from_absolute_cursor_pos : from_absolute_cursor_pos,
787 814 browser : browser,
788 815 platform: platform,
789 816 is_or_has : is_or_has,
790 817 is_focused : is_focused,
791 818 mergeopt: mergeopt,
792 819 ajax_error_msg : ajax_error_msg,
793 820 log_ajax_error : log_ajax_error,
794 821 requireCodeMirrorMode : requireCodeMirrorMode,
795 822 XHR_ERROR : XHR_ERROR,
796 823 wrap_ajax_error : wrap_ajax_error,
797 824 promising_ajax : promising_ajax,
798 825 WrappedError: WrappedError,
799 826 load_class: load_class,
800 827 resolve_promises_dict: resolve_promises_dict,
801 828 reject: reject,
829 typeset: typeset,
802 830 };
803 831
804 832 // Backwards compatability.
805 833 IPython.utils = utils;
806 834
807 835 return utils;
808 836 });
@@ -1,681 +1,678 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 /**
5 5 *
6 6 *
7 7 * @module cell
8 8 * @namespace cell
9 9 * @class Cell
10 10 */
11 11
12 12
13 13 define([
14 14 'base/js/namespace',
15 15 'jquery',
16 16 'base/js/utils',
17 17 'codemirror/lib/codemirror',
18 18 'codemirror/addon/edit/matchbrackets',
19 19 'codemirror/addon/edit/closebrackets',
20 20 'codemirror/addon/comment/comment'
21 21 ], function(IPython, $, utils, CodeMirror, cm_match, cm_closeb, cm_comment) {
22 22 // TODO: remove IPython dependency here
23 23 "use strict";
24 24
25 25 var Cell = function (options) {
26 26 /* Constructor
27 27 *
28 28 * The Base `Cell` class from which to inherit.
29 29 * @constructor
30 30 * @param:
31 31 * options: dictionary
32 32 * Dictionary of keyword arguments.
33 33 * events: $(Events) instance
34 34 * config: dictionary
35 35 * keyboard_manager: KeyboardManager instance
36 36 */
37 37 options = options || {};
38 38 this.keyboard_manager = options.keyboard_manager;
39 39 this.events = options.events;
40 40 var config = utils.mergeopt(Cell, options.config);
41 41 // superclass default overwrite our default
42 42
43 43 this.placeholder = config.placeholder || '';
44 44 this.read_only = config.cm_config.readOnly;
45 45 this.selected = false;
46 46 this.rendered = false;
47 47 this.mode = 'command';
48 48
49 49 // Metadata property
50 50 var that = this;
51 51 this._metadata = {};
52 52 Object.defineProperty(this, 'metadata', {
53 53 get: function() { return that._metadata; },
54 54 set: function(value) {
55 55 that._metadata = value;
56 56 if (that.celltoolbar) {
57 57 that.celltoolbar.rebuild();
58 58 }
59 59 }
60 60 });
61 61
62 62 // load this from metadata later ?
63 63 this.user_highlight = 'auto';
64 64 this.cm_config = config.cm_config;
65 65 this.cell_id = utils.uuid();
66 66 this._options = config;
67 67
68 68 // For JS VM engines optimization, attributes should be all set (even
69 69 // to null) in the constructor, and if possible, if different subclass
70 70 // have new attributes with same name, they should be created in the
71 71 // same order. Easiest is to create and set to null in parent class.
72 72
73 73 this.element = null;
74 74 this.cell_type = this.cell_type || null;
75 75 this.code_mirror = null;
76 76
77 77 this.create_element();
78 78 if (this.element !== null) {
79 79 this.element.data("cell", this);
80 80 this.bind_events();
81 81 this.init_classes();
82 82 }
83 83 };
84 84
85 85 Cell.options_default = {
86 86 cm_config : {
87 87 indentUnit : 4,
88 88 readOnly: false,
89 89 theme: "default",
90 90 extraKeys: {
91 91 "Cmd-Right":"goLineRight",
92 92 "End":"goLineRight",
93 93 "Cmd-Left":"goLineLeft"
94 94 }
95 95 }
96 96 };
97 97
98 98 // FIXME: Workaround CM Bug #332 (Safari segfault on drag)
99 99 // by disabling drag/drop altogether on Safari
100 100 // https://github.com/codemirror/CodeMirror/issues/332
101 101 if (utils.browser[0] == "Safari") {
102 102 Cell.options_default.cm_config.dragDrop = false;
103 103 }
104 104
105 105 /**
106 106 * Empty. Subclasses must implement create_element.
107 107 * This should contain all the code to create the DOM element in notebook
108 108 * and will be called by Base Class constructor.
109 109 * @method create_element
110 110 */
111 111 Cell.prototype.create_element = function () {
112 112 };
113 113
114 114 Cell.prototype.init_classes = function () {
115 115 /**
116 116 * Call after this.element exists to initialize the css classes
117 117 * related to selected, rendered and mode.
118 118 */
119 119 if (this.selected) {
120 120 this.element.addClass('selected');
121 121 } else {
122 122 this.element.addClass('unselected');
123 123 }
124 124 if (this.rendered) {
125 125 this.element.addClass('rendered');
126 126 } else {
127 127 this.element.addClass('unrendered');
128 128 }
129 129 if (this.mode === 'edit') {
130 130 this.element.addClass('edit_mode');
131 131 } else {
132 132 this.element.addClass('command_mode');
133 133 }
134 134 };
135 135
136 136 /**
137 137 * Subclasses can implement override bind_events.
138 138 * Be carefull to call the parent method when overwriting as it fires event.
139 139 * this will be triggerd after create_element in constructor.
140 140 * @method bind_events
141 141 */
142 142 Cell.prototype.bind_events = function () {
143 143 var that = this;
144 144 // We trigger events so that Cell doesn't have to depend on Notebook.
145 145 that.element.click(function (event) {
146 146 if (!that.selected) {
147 147 that.events.trigger('select.Cell', {'cell':that});
148 148 }
149 149 });
150 150 that.element.focusin(function (event) {
151 151 if (!that.selected) {
152 152 that.events.trigger('select.Cell', {'cell':that});
153 153 }
154 154 });
155 155 if (this.code_mirror) {
156 156 this.code_mirror.on("change", function(cm, change) {
157 157 that.events.trigger("set_dirty.Notebook", {value: true});
158 158 });
159 159 }
160 160 if (this.code_mirror) {
161 161 this.code_mirror.on('focus', function(cm, change) {
162 162 that.events.trigger('edit_mode.Cell', {cell: that});
163 163 });
164 164 }
165 165 if (this.code_mirror) {
166 166 this.code_mirror.on('blur', function(cm, change) {
167 167 that.events.trigger('command_mode.Cell', {cell: that});
168 168 });
169 169 }
170 170
171 171 this.element.dblclick(function () {
172 172 if (that.selected === false) {
173 173 this.events.trigger('select.Cell', {'cell':that});
174 174 }
175 175 var cont = that.unrender();
176 176 if (cont) {
177 177 that.focus_editor();
178 178 }
179 179 });
180 180 };
181 181
182 182 /**
183 183 * This method gets called in CodeMirror's onKeyDown/onKeyPress
184 184 * handlers and is used to provide custom key handling.
185 185 *
186 186 * To have custom handling, subclasses should override this method, but still call it
187 187 * in order to process the Edit mode keyboard shortcuts.
188 188 *
189 189 * @method handle_codemirror_keyevent
190 190 * @param {CodeMirror} editor - The codemirror instance bound to the cell
191 191 * @param {event} event - key press event which either should or should not be handled by CodeMirror
192 192 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
193 193 */
194 194 Cell.prototype.handle_codemirror_keyevent = function (editor, event) {
195 195 var shortcuts = this.keyboard_manager.edit_shortcuts;
196 196
197 197 var cur = editor.getCursor();
198 198 if((cur.line !== 0 || cur.ch !==0) && event.keyCode === 38){
199 199 event._ipkmIgnore = true;
200 200 }
201 201 var nLastLine = editor.lastLine();
202 202 if ((event.keyCode === 40) &&
203 203 ((cur.line !== nLastLine) ||
204 204 (cur.ch !== editor.getLineHandle(nLastLine).text.length))
205 205 ) {
206 206 event._ipkmIgnore = true;
207 207 }
208 208 // if this is an edit_shortcuts shortcut, the global keyboard/shortcut
209 209 // manager will handle it
210 210 if (shortcuts.handles(event)) {
211 211 return true;
212 212 }
213 213
214 214 return false;
215 215 };
216 216
217 217
218 218 /**
219 219 * Triger typsetting of math by mathjax on current cell element
220 220 * @method typeset
221 221 */
222 222 Cell.prototype.typeset = function () {
223 if (window.MathJax) {
224 var cell_math = this.element.get(0);
225 MathJax.Hub.Queue(["Typeset", MathJax.Hub, cell_math]);
226 }
223 utils.typeset(this.element);
227 224 };
228 225
229 226 /**
230 227 * handle cell level logic when a cell is selected
231 228 * @method select
232 229 * @return is the action being taken
233 230 */
234 231 Cell.prototype.select = function () {
235 232 if (!this.selected) {
236 233 this.element.addClass('selected');
237 234 this.element.removeClass('unselected');
238 235 this.selected = true;
239 236 return true;
240 237 } else {
241 238 return false;
242 239 }
243 240 };
244 241
245 242 /**
246 243 * handle cell level logic when a cell is unselected
247 244 * @method unselect
248 245 * @return is the action being taken
249 246 */
250 247 Cell.prototype.unselect = function () {
251 248 if (this.selected) {
252 249 this.element.addClass('unselected');
253 250 this.element.removeClass('selected');
254 251 this.selected = false;
255 252 return true;
256 253 } else {
257 254 return false;
258 255 }
259 256 };
260 257
261 258 /**
262 259 * should be overritten by subclass
263 260 * @method execute
264 261 */
265 262 Cell.prototype.execute = function () {
266 263 return;
267 264 };
268 265
269 266 /**
270 267 * handle cell level logic when a cell is rendered
271 268 * @method render
272 269 * @return is the action being taken
273 270 */
274 271 Cell.prototype.render = function () {
275 272 if (!this.rendered) {
276 273 this.element.addClass('rendered');
277 274 this.element.removeClass('unrendered');
278 275 this.rendered = true;
279 276 return true;
280 277 } else {
281 278 return false;
282 279 }
283 280 };
284 281
285 282 /**
286 283 * handle cell level logic when a cell is unrendered
287 284 * @method unrender
288 285 * @return is the action being taken
289 286 */
290 287 Cell.prototype.unrender = function () {
291 288 if (this.rendered) {
292 289 this.element.addClass('unrendered');
293 290 this.element.removeClass('rendered');
294 291 this.rendered = false;
295 292 return true;
296 293 } else {
297 294 return false;
298 295 }
299 296 };
300 297
301 298 /**
302 299 * Delegates keyboard shortcut handling to either IPython keyboard
303 300 * manager when in command mode, or CodeMirror when in edit mode
304 301 *
305 302 * @method handle_keyevent
306 303 * @param {CodeMirror} editor - The codemirror instance bound to the cell
307 304 * @param {event} - key event to be handled
308 305 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
309 306 */
310 307 Cell.prototype.handle_keyevent = function (editor, event) {
311 308 if (this.mode === 'command') {
312 309 return true;
313 310 } else if (this.mode === 'edit') {
314 311 return this.handle_codemirror_keyevent(editor, event);
315 312 }
316 313 };
317 314
318 315 /**
319 316 * @method at_top
320 317 * @return {Boolean}
321 318 */
322 319 Cell.prototype.at_top = function () {
323 320 var cm = this.code_mirror;
324 321 var cursor = cm.getCursor();
325 322 if (cursor.line === 0 && cursor.ch === 0) {
326 323 return true;
327 324 }
328 325 return false;
329 326 };
330 327
331 328 /**
332 329 * @method at_bottom
333 330 * @return {Boolean}
334 331 * */
335 332 Cell.prototype.at_bottom = function () {
336 333 var cm = this.code_mirror;
337 334 var cursor = cm.getCursor();
338 335 if (cursor.line === (cm.lineCount()-1) && cursor.ch === cm.getLine(cursor.line).length) {
339 336 return true;
340 337 }
341 338 return false;
342 339 };
343 340
344 341 /**
345 342 * enter the command mode for the cell
346 343 * @method command_mode
347 344 * @return is the action being taken
348 345 */
349 346 Cell.prototype.command_mode = function () {
350 347 if (this.mode !== 'command') {
351 348 this.element.addClass('command_mode');
352 349 this.element.removeClass('edit_mode');
353 350 this.mode = 'command';
354 351 return true;
355 352 } else {
356 353 return false;
357 354 }
358 355 };
359 356
360 357 /**
361 358 * enter the edit mode for the cell
362 359 * @method command_mode
363 360 * @return is the action being taken
364 361 */
365 362 Cell.prototype.edit_mode = function () {
366 363 if (this.mode !== 'edit') {
367 364 this.element.addClass('edit_mode');
368 365 this.element.removeClass('command_mode');
369 366 this.mode = 'edit';
370 367 return true;
371 368 } else {
372 369 return false;
373 370 }
374 371 };
375 372
376 373 /**
377 374 * Focus the cell in the DOM sense
378 375 * @method focus_cell
379 376 */
380 377 Cell.prototype.focus_cell = function () {
381 378 this.element.focus();
382 379 };
383 380
384 381 /**
385 382 * Focus the editor area so a user can type
386 383 *
387 384 * NOTE: If codemirror is focused via a mouse click event, you don't want to
388 385 * call this because it will cause a page jump.
389 386 * @method focus_editor
390 387 */
391 388 Cell.prototype.focus_editor = function () {
392 389 this.refresh();
393 390 this.code_mirror.focus();
394 391 };
395 392
396 393 /**
397 394 * Refresh codemirror instance
398 395 * @method refresh
399 396 */
400 397 Cell.prototype.refresh = function () {
401 398 if (this.code_mirror) {
402 399 this.code_mirror.refresh();
403 400 }
404 401 };
405 402
406 403 /**
407 404 * should be overritten by subclass
408 405 * @method get_text
409 406 */
410 407 Cell.prototype.get_text = function () {
411 408 };
412 409
413 410 /**
414 411 * should be overritten by subclass
415 412 * @method set_text
416 413 * @param {string} text
417 414 */
418 415 Cell.prototype.set_text = function (text) {
419 416 };
420 417
421 418 /**
422 419 * should be overritten by subclass
423 420 * serialise cell to json.
424 421 * @method toJSON
425 422 **/
426 423 Cell.prototype.toJSON = function () {
427 424 var data = {};
428 425 // deepcopy the metadata so copied cells don't share the same object
429 426 data.metadata = JSON.parse(JSON.stringify(this.metadata));
430 427 data.cell_type = this.cell_type;
431 428 return data;
432 429 };
433 430
434 431 /**
435 432 * should be overritten by subclass
436 433 * @method fromJSON
437 434 **/
438 435 Cell.prototype.fromJSON = function (data) {
439 436 if (data.metadata !== undefined) {
440 437 this.metadata = data.metadata;
441 438 }
442 439 };
443 440
444 441
445 442 /**
446 443 * can the cell be split into two cells (false if not deletable)
447 444 * @method is_splittable
448 445 **/
449 446 Cell.prototype.is_splittable = function () {
450 447 return this.is_deletable();
451 448 };
452 449
453 450
454 451 /**
455 452 * can the cell be merged with other cells (false if not deletable)
456 453 * @method is_mergeable
457 454 **/
458 455 Cell.prototype.is_mergeable = function () {
459 456 return this.is_deletable();
460 457 };
461 458
462 459 /**
463 460 * is the cell deletable? only false (undeletable) if
464 461 * metadata.deletable is explicitly false -- everything else
465 462 * counts as true
466 463 *
467 464 * @method is_deletable
468 465 **/
469 466 Cell.prototype.is_deletable = function () {
470 467 if (this.metadata.deletable === false) {
471 468 return false;
472 469 }
473 470 return true;
474 471 };
475 472
476 473 /**
477 474 * @return {String} - the text before the cursor
478 475 * @method get_pre_cursor
479 476 **/
480 477 Cell.prototype.get_pre_cursor = function () {
481 478 var cursor = this.code_mirror.getCursor();
482 479 var text = this.code_mirror.getRange({line:0, ch:0}, cursor);
483 480 text = text.replace(/^\n+/, '').replace(/\n+$/, '');
484 481 return text;
485 482 };
486 483
487 484
488 485 /**
489 486 * @return {String} - the text after the cursor
490 487 * @method get_post_cursor
491 488 **/
492 489 Cell.prototype.get_post_cursor = function () {
493 490 var cursor = this.code_mirror.getCursor();
494 491 var last_line_num = this.code_mirror.lineCount()-1;
495 492 var last_line_len = this.code_mirror.getLine(last_line_num).length;
496 493 var end = {line:last_line_num, ch:last_line_len};
497 494 var text = this.code_mirror.getRange(cursor, end);
498 495 text = text.replace(/^\n+/, '').replace(/\n+$/, '');
499 496 return text;
500 497 };
501 498
502 499 /**
503 500 * Show/Hide CodeMirror LineNumber
504 501 * @method show_line_numbers
505 502 *
506 503 * @param value {Bool} show (true), or hide (false) the line number in CodeMirror
507 504 **/
508 505 Cell.prototype.show_line_numbers = function (value) {
509 506 this.code_mirror.setOption('lineNumbers', value);
510 507 this.code_mirror.refresh();
511 508 };
512 509
513 510 /**
514 511 * Toggle CodeMirror LineNumber
515 512 * @method toggle_line_numbers
516 513 **/
517 514 Cell.prototype.toggle_line_numbers = function () {
518 515 var val = this.code_mirror.getOption('lineNumbers');
519 516 this.show_line_numbers(!val);
520 517 };
521 518
522 519 /**
523 520 * Force codemirror highlight mode
524 521 * @method force_highlight
525 522 * @param {object} - CodeMirror mode
526 523 **/
527 524 Cell.prototype.force_highlight = function(mode) {
528 525 this.user_highlight = mode;
529 526 this.auto_highlight();
530 527 };
531 528
532 529 /**
533 530 * Try to autodetect cell highlight mode, or use selected mode
534 531 * @methods _auto_highlight
535 532 * @private
536 533 * @param {String|object|undefined} - CodeMirror mode | 'auto'
537 534 **/
538 535 Cell.prototype._auto_highlight = function (modes) {
539 536 /**
540 537 *Here we handle manually selected modes
541 538 */
542 539 var that = this;
543 540 var mode;
544 541 if( this.user_highlight !== undefined && this.user_highlight != 'auto' )
545 542 {
546 543 mode = this.user_highlight;
547 544 CodeMirror.autoLoadMode(this.code_mirror, mode);
548 545 this.code_mirror.setOption('mode', mode);
549 546 return;
550 547 }
551 548 var current_mode = this.code_mirror.getOption('mode', mode);
552 549 var first_line = this.code_mirror.getLine(0);
553 550 // loop on every pairs
554 551 for(mode in modes) {
555 552 var regs = modes[mode].reg;
556 553 // only one key every time but regexp can't be keys...
557 554 for(var i=0; i<regs.length; i++) {
558 555 // here we handle non magic_modes
559 556 if(first_line.match(regs[i]) !== null) {
560 557 if(current_mode == mode){
561 558 return;
562 559 }
563 560 if (mode.search('magic_') !== 0) {
564 561 utils.requireCodeMirrorMode(mode, function () {
565 562 that.code_mirror.setOption('mode', mode);
566 563 });
567 564 return;
568 565 }
569 566 var open = modes[mode].open || "%%";
570 567 var close = modes[mode].close || "%%end";
571 568 var magic_mode = mode;
572 569 mode = magic_mode.substr(6);
573 570 if(current_mode == magic_mode){
574 571 return;
575 572 }
576 573 utils.requireCodeMirrorMode(mode, function () {
577 574 // create on the fly a mode that switch between
578 575 // plain/text and something else, otherwise `%%` is
579 576 // source of some highlight issues.
580 577 CodeMirror.defineMode(magic_mode, function(config) {
581 578 return CodeMirror.multiplexingMode(
582 579 CodeMirror.getMode(config, 'text/plain'),
583 580 // always set something on close
584 581 {open: open, close: close,
585 582 mode: CodeMirror.getMode(config, mode),
586 583 delimStyle: "delimit"
587 584 }
588 585 );
589 586 });
590 587 that.code_mirror.setOption('mode', magic_mode);
591 588 });
592 589 return;
593 590 }
594 591 }
595 592 }
596 593 // fallback on default
597 594 var default_mode;
598 595 try {
599 596 default_mode = this._options.cm_config.mode;
600 597 } catch(e) {
601 598 default_mode = 'text/plain';
602 599 }
603 600 if( current_mode === default_mode){
604 601 return;
605 602 }
606 603 this.code_mirror.setOption('mode', default_mode);
607 604 };
608 605
609 606 var UnrecognizedCell = function (options) {
610 607 /** Constructor for unrecognized cells */
611 608 Cell.apply(this, arguments);
612 609 this.cell_type = 'unrecognized';
613 610 this.celltoolbar = null;
614 611 this.data = {};
615 612
616 613 Object.seal(this);
617 614 };
618 615
619 616 UnrecognizedCell.prototype = Object.create(Cell.prototype);
620 617
621 618
622 619 // cannot merge or split unrecognized cells
623 620 UnrecognizedCell.prototype.is_mergeable = function () {
624 621 return false;
625 622 };
626 623
627 624 UnrecognizedCell.prototype.is_splittable = function () {
628 625 return false;
629 626 };
630 627
631 628 UnrecognizedCell.prototype.toJSON = function () {
632 629 /**
633 630 * deepcopy the metadata so copied cells don't share the same object
634 631 */
635 632 return JSON.parse(JSON.stringify(this.data));
636 633 };
637 634
638 635 UnrecognizedCell.prototype.fromJSON = function (data) {
639 636 this.data = data;
640 637 if (data.metadata !== undefined) {
641 638 this.metadata = data.metadata;
642 639 } else {
643 640 data.metadata = this.metadata;
644 641 }
645 642 this.element.find('.inner_cell').find("a").text("Unrecognized cell type: " + data.cell_type);
646 643 };
647 644
648 645 UnrecognizedCell.prototype.create_element = function () {
649 646 Cell.prototype.create_element.apply(this, arguments);
650 647 var cell = this.element = $("<div>").addClass('cell unrecognized_cell');
651 648 cell.attr('tabindex','2');
652 649
653 650 var prompt = $('<div/>').addClass('prompt input_prompt');
654 651 cell.append(prompt);
655 652 var inner_cell = $('<div/>').addClass('inner_cell');
656 653 inner_cell.append(
657 654 $("<a>")
658 655 .attr("href", "#")
659 656 .text("Unrecognized cell type")
660 657 );
661 658 cell.append(inner_cell);
662 659 this.element = cell;
663 660 };
664 661
665 662 UnrecognizedCell.prototype.bind_events = function () {
666 663 Cell.prototype.bind_events.apply(this, arguments);
667 664 var cell = this;
668 665
669 666 this.element.find('.inner_cell').find("a").click(function () {
670 667 cell.events.trigger('unrecognized_cell.Cell', {cell: cell})
671 668 });
672 669 };
673 670
674 671 // Backwards compatibility.
675 672 IPython.Cell = Cell;
676 673
677 674 return {
678 675 Cell: Cell,
679 676 UnrecognizedCell: UnrecognizedCell
680 677 };
681 678 });
@@ -1,980 +1,978 b''
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 'jqueryui',
7 7 'base/js/utils',
8 8 'base/js/security',
9 9 'base/js/keyboard',
10 10 'notebook/js/mathjaxutils',
11 11 'components/marked/lib/marked',
12 12 ], function(IPython, $, utils, security, keyboard, mathjaxutils, marked) {
13 13 "use strict";
14 14
15 15 /**
16 16 * @class OutputArea
17 17 *
18 18 * @constructor
19 19 */
20 20
21 21 var OutputArea = function (options) {
22 22 this.selector = options.selector;
23 23 this.events = options.events;
24 24 this.keyboard_manager = options.keyboard_manager;
25 25 this.wrapper = $(options.selector);
26 26 this.outputs = [];
27 27 this.collapsed = false;
28 28 this.scrolled = false;
29 29 this.trusted = true;
30 30 this.clear_queued = null;
31 31 if (options.prompt_area === undefined) {
32 32 this.prompt_area = true;
33 33 } else {
34 34 this.prompt_area = options.prompt_area;
35 35 }
36 36 this.create_elements();
37 37 this.style();
38 38 this.bind_events();
39 39 };
40 40
41 41
42 42 /**
43 43 * Class prototypes
44 44 **/
45 45
46 46 OutputArea.prototype.create_elements = function () {
47 47 this.element = $("<div/>");
48 48 this.collapse_button = $("<div/>");
49 49 this.prompt_overlay = $("<div/>");
50 50 this.wrapper.append(this.prompt_overlay);
51 51 this.wrapper.append(this.element);
52 52 this.wrapper.append(this.collapse_button);
53 53 };
54 54
55 55
56 56 OutputArea.prototype.style = function () {
57 57 this.collapse_button.hide();
58 58 this.prompt_overlay.hide();
59 59
60 60 this.wrapper.addClass('output_wrapper');
61 61 this.element.addClass('output');
62 62
63 63 this.collapse_button.addClass("btn btn-default output_collapsed");
64 64 this.collapse_button.attr('title', 'click to expand output');
65 65 this.collapse_button.text('. . .');
66 66
67 67 this.prompt_overlay.addClass('out_prompt_overlay prompt');
68 68 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
69 69
70 70 this.collapse();
71 71 };
72 72
73 73 /**
74 74 * Should the OutputArea scroll?
75 75 * Returns whether the height (in lines) exceeds a threshold.
76 76 *
77 77 * @private
78 78 * @method _should_scroll
79 79 * @param [lines=100]{Integer}
80 80 * @return {Bool}
81 81 *
82 82 */
83 83 OutputArea.prototype._should_scroll = function (lines) {
84 84 if (lines <=0 ){ return; }
85 85 if (!lines) {
86 86 lines = 100;
87 87 }
88 88 // line-height from http://stackoverflow.com/questions/1185151
89 89 var fontSize = this.element.css('font-size');
90 90 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
91 91
92 92 return (this.element.height() > lines * lineHeight);
93 93 };
94 94
95 95
96 96 OutputArea.prototype.bind_events = function () {
97 97 var that = this;
98 98 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
99 99 this.prompt_overlay.click(function () { that.toggle_scroll(); });
100 100
101 101 this.element.resize(function () {
102 102 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
103 103 if ( utils.browser[0] === "Firefox" ) {
104 104 return;
105 105 }
106 106 // maybe scroll output,
107 107 // if it's grown large enough and hasn't already been scrolled.
108 108 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
109 109 that.scroll_area();
110 110 }
111 111 });
112 112 this.collapse_button.click(function () {
113 113 that.expand();
114 114 });
115 115 };
116 116
117 117
118 118 OutputArea.prototype.collapse = function () {
119 119 if (!this.collapsed) {
120 120 this.element.hide();
121 121 this.prompt_overlay.hide();
122 122 if (this.element.html()){
123 123 this.collapse_button.show();
124 124 }
125 125 this.collapsed = true;
126 126 }
127 127 };
128 128
129 129
130 130 OutputArea.prototype.expand = function () {
131 131 if (this.collapsed) {
132 132 this.collapse_button.hide();
133 133 this.element.show();
134 134 this.prompt_overlay.show();
135 135 this.collapsed = false;
136 136 }
137 137 };
138 138
139 139
140 140 OutputArea.prototype.toggle_output = function () {
141 141 if (this.collapsed) {
142 142 this.expand();
143 143 } else {
144 144 this.collapse();
145 145 }
146 146 };
147 147
148 148
149 149 OutputArea.prototype.scroll_area = function () {
150 150 this.element.addClass('output_scroll');
151 151 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
152 152 this.scrolled = true;
153 153 };
154 154
155 155
156 156 OutputArea.prototype.unscroll_area = function () {
157 157 this.element.removeClass('output_scroll');
158 158 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
159 159 this.scrolled = false;
160 160 };
161 161
162 162 /**
163 163 *
164 164 * Scroll OutputArea if height supperior than a threshold (in lines).
165 165 *
166 166 * Threshold is a maximum number of lines. If unspecified, defaults to
167 167 * OutputArea.minimum_scroll_threshold.
168 168 *
169 169 * Negative threshold will prevent the OutputArea from ever scrolling.
170 170 *
171 171 * @method scroll_if_long
172 172 *
173 173 * @param [lines=20]{Number} Default to 20 if not set,
174 174 * behavior undefined for value of `0`.
175 175 *
176 176 **/
177 177 OutputArea.prototype.scroll_if_long = function (lines) {
178 178 var n = lines | OutputArea.minimum_scroll_threshold;
179 179 if(n <= 0){
180 180 return;
181 181 }
182 182
183 183 if (this._should_scroll(n)) {
184 184 // only allow scrolling long-enough output
185 185 this.scroll_area();
186 186 }
187 187 };
188 188
189 189
190 190 OutputArea.prototype.toggle_scroll = function () {
191 191 if (this.scrolled) {
192 192 this.unscroll_area();
193 193 } else {
194 194 // only allow scrolling long-enough output
195 195 this.scroll_if_long();
196 196 }
197 197 };
198 198
199 199
200 200 // typeset with MathJax if MathJax is available
201 201 OutputArea.prototype.typeset = function () {
202 if (window.MathJax){
203 MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
204 }
202 utils.typeset(this.element);
205 203 };
206 204
207 205
208 206 OutputArea.prototype.handle_output = function (msg) {
209 207 var json = {};
210 208 var msg_type = json.output_type = msg.header.msg_type;
211 209 var content = msg.content;
212 210 if (msg_type === "stream") {
213 211 json.text = content.text;
214 212 json.name = content.name;
215 213 } else if (msg_type === "display_data") {
216 214 json.data = content.data;
217 215 json.output_type = msg_type;
218 216 json.metadata = content.metadata;
219 217 } else if (msg_type === "execute_result") {
220 218 json.data = content.data;
221 219 json.output_type = msg_type;
222 220 json.metadata = content.metadata;
223 221 json.execution_count = content.execution_count;
224 222 } else if (msg_type === "error") {
225 223 json.ename = content.ename;
226 224 json.evalue = content.evalue;
227 225 json.traceback = content.traceback;
228 226 } else {
229 227 console.log("unhandled output message", msg);
230 228 return;
231 229 }
232 230 this.append_output(json);
233 231 };
234 232
235 233
236 234 OutputArea.output_types = [
237 235 'application/javascript',
238 236 'text/html',
239 237 'text/markdown',
240 238 'text/latex',
241 239 'image/svg+xml',
242 240 'image/png',
243 241 'image/jpeg',
244 242 'application/pdf',
245 243 'text/plain'
246 244 ];
247 245
248 246 OutputArea.prototype.validate_mimebundle = function (json) {
249 247 /**
250 248 * scrub invalid outputs
251 249 */
252 250 var data = json.data;
253 251 $.map(OutputArea.output_types, function(key){
254 252 if (key !== 'application/json' &&
255 253 data[key] !== undefined &&
256 254 typeof data[key] !== 'string'
257 255 ) {
258 256 console.log("Invalid type for " + key, data[key]);
259 257 delete data[key];
260 258 }
261 259 });
262 260 return json;
263 261 };
264 262
265 263 OutputArea.prototype.append_output = function (json) {
266 264 this.expand();
267 265
268 266 // Clear the output if clear is queued.
269 267 var needs_height_reset = false;
270 268 if (this.clear_queued) {
271 269 this.clear_output(false);
272 270 needs_height_reset = true;
273 271 }
274 272
275 273 var record_output = true;
276 274 switch(json.output_type) {
277 275 case 'execute_result':
278 276 json = this.validate_mimebundle(json);
279 277 this.append_execute_result(json);
280 278 break;
281 279 case 'stream':
282 280 // append_stream might have merged the output with earlier stream output
283 281 record_output = this.append_stream(json);
284 282 break;
285 283 case 'error':
286 284 this.append_error(json);
287 285 break;
288 286 case 'display_data':
289 287 // append handled below
290 288 json = this.validate_mimebundle(json);
291 289 break;
292 290 default:
293 291 console.log("unrecognized output type: " + json.output_type);
294 292 this.append_unrecognized(json);
295 293 }
296 294
297 295 // We must release the animation fixed height in a callback since Gecko
298 296 // (FireFox) doesn't render the image immediately as the data is
299 297 // available.
300 298 var that = this;
301 299 var handle_appended = function ($el) {
302 300 /**
303 301 * Only reset the height to automatic if the height is currently
304 302 * fixed (done by wait=True flag on clear_output).
305 303 */
306 304 if (needs_height_reset) {
307 305 that.element.height('');
308 306 }
309 307 that.element.trigger('resize');
310 308 };
311 309 if (json.output_type === 'display_data') {
312 310 this.append_display_data(json, handle_appended);
313 311 } else {
314 312 handle_appended();
315 313 }
316 314
317 315 if (record_output) {
318 316 this.outputs.push(json);
319 317 }
320 318 };
321 319
322 320
323 321 OutputArea.prototype.create_output_area = function () {
324 322 var oa = $("<div/>").addClass("output_area");
325 323 if (this.prompt_area) {
326 324 oa.append($('<div/>').addClass('prompt'));
327 325 }
328 326 return oa;
329 327 };
330 328
331 329
332 330 function _get_metadata_key(metadata, key, mime) {
333 331 var mime_md = metadata[mime];
334 332 // mime-specific higher priority
335 333 if (mime_md && mime_md[key] !== undefined) {
336 334 return mime_md[key];
337 335 }
338 336 // fallback on global
339 337 return metadata[key];
340 338 }
341 339
342 340 OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
343 341 var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
344 342 if (_get_metadata_key(md, 'isolated', mime)) {
345 343 // Create an iframe to isolate the subarea from the rest of the
346 344 // document
347 345 var iframe = $('<iframe/>').addClass('box-flex1');
348 346 iframe.css({'height':1, 'width':'100%', 'display':'block'});
349 347 iframe.attr('frameborder', 0);
350 348 iframe.attr('scrolling', 'auto');
351 349
352 350 // Once the iframe is loaded, the subarea is dynamically inserted
353 351 iframe.on('load', function() {
354 352 // Workaround needed by Firefox, to properly render svg inside
355 353 // iframes, see http://stackoverflow.com/questions/10177190/
356 354 // svg-dynamically-added-to-iframe-does-not-render-correctly
357 355 this.contentDocument.open();
358 356
359 357 // Insert the subarea into the iframe
360 358 // We must directly write the html. When using Jquery's append
361 359 // method, javascript is evaluated in the parent document and
362 360 // not in the iframe document. At this point, subarea doesn't
363 361 // contain any user content.
364 362 this.contentDocument.write(subarea.html());
365 363
366 364 this.contentDocument.close();
367 365
368 366 var body = this.contentDocument.body;
369 367 // Adjust the iframe height automatically
370 368 iframe.height(body.scrollHeight + 'px');
371 369 });
372 370
373 371 // Elements should be appended to the inner subarea and not to the
374 372 // iframe
375 373 iframe.append = function(that) {
376 374 subarea.append(that);
377 375 };
378 376
379 377 return iframe;
380 378 } else {
381 379 return subarea;
382 380 }
383 381 };
384 382
385 383
386 384 OutputArea.prototype._append_javascript_error = function (err, element) {
387 385 /**
388 386 * display a message when a javascript error occurs in display output
389 387 */
390 388 var msg = "Javascript error adding output!";
391 389 if ( element === undefined ) return;
392 390 element
393 391 .append($('<div/>').text(msg).addClass('js-error'))
394 392 .append($('<div/>').text(err.toString()).addClass('js-error'))
395 393 .append($('<div/>').text('See your browser Javascript console for more details.').addClass('js-error'));
396 394 };
397 395
398 396 OutputArea.prototype._safe_append = function (toinsert) {
399 397 /**
400 398 * safely append an item to the document
401 399 * this is an object created by user code,
402 400 * and may have errors, which should not be raised
403 401 * under any circumstances.
404 402 */
405 403 try {
406 404 this.element.append(toinsert);
407 405 } catch(err) {
408 406 console.log(err);
409 407 // Create an actual output_area and output_subarea, which creates
410 408 // the prompt area and the proper indentation.
411 409 var toinsert = this.create_output_area();
412 410 var subarea = $('<div/>').addClass('output_subarea');
413 411 toinsert.append(subarea);
414 412 this._append_javascript_error(err, subarea);
415 413 this.element.append(toinsert);
416 414 }
417 415
418 416 // Notify others of changes.
419 417 this.element.trigger('changed');
420 418 };
421 419
422 420
423 421 OutputArea.prototype.append_execute_result = function (json) {
424 422 var n = json.execution_count || ' ';
425 423 var toinsert = this.create_output_area();
426 424 if (this.prompt_area) {
427 425 toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
428 426 }
429 427 var inserted = this.append_mime_type(json, toinsert);
430 428 if (inserted) {
431 429 inserted.addClass('output_result');
432 430 }
433 431 this._safe_append(toinsert);
434 432 // If we just output latex, typeset it.
435 433 if ((json.data['text/latex'] !== undefined) ||
436 434 (json.data['text/html'] !== undefined) ||
437 435 (json.data['text/markdown'] !== undefined)) {
438 436 this.typeset();
439 437 }
440 438 };
441 439
442 440
443 441 OutputArea.prototype.append_error = function (json) {
444 442 var tb = json.traceback;
445 443 if (tb !== undefined && tb.length > 0) {
446 444 var s = '';
447 445 var len = tb.length;
448 446 for (var i=0; i<len; i++) {
449 447 s = s + tb[i] + '\n';
450 448 }
451 449 s = s + '\n';
452 450 var toinsert = this.create_output_area();
453 451 var append_text = OutputArea.append_map['text/plain'];
454 452 if (append_text) {
455 453 append_text.apply(this, [s, {}, toinsert]).addClass('output_error');
456 454 }
457 455 this._safe_append(toinsert);
458 456 }
459 457 };
460 458
461 459
462 460 OutputArea.prototype.append_stream = function (json) {
463 461 var text = json.text;
464 462 var subclass = "output_"+json.name;
465 463 if (this.outputs.length > 0){
466 464 // have at least one output to consider
467 465 var last = this.outputs[this.outputs.length-1];
468 466 if (last.output_type == 'stream' && json.name == last.name){
469 467 // latest output was in the same stream,
470 468 // so append directly into its pre tag
471 469 // escape ANSI & HTML specials:
472 470 last.text = utils.fixCarriageReturn(last.text + json.text);
473 471 var pre = this.element.find('div.'+subclass).last().find('pre');
474 472 var html = utils.fixConsole(last.text);
475 473 // The only user content injected with this HTML call is
476 474 // escaped by the fixConsole() method.
477 475 pre.html(html);
478 476 // return false signals that we merged this output with the previous one,
479 477 // and the new output shouldn't be recorded.
480 478 return false;
481 479 }
482 480 }
483 481
484 482 if (!text.replace("\r", "")) {
485 483 // text is nothing (empty string, \r, etc.)
486 484 // so don't append any elements, which might add undesirable space
487 485 // return true to indicate the output should be recorded.
488 486 return true;
489 487 }
490 488
491 489 // If we got here, attach a new div
492 490 var toinsert = this.create_output_area();
493 491 var append_text = OutputArea.append_map['text/plain'];
494 492 if (append_text) {
495 493 append_text.apply(this, [text, {}, toinsert]).addClass("output_stream " + subclass);
496 494 }
497 495 this._safe_append(toinsert);
498 496 return true;
499 497 };
500 498
501 499
502 500 OutputArea.prototype.append_unrecognized = function (json) {
503 501 var that = this;
504 502 var toinsert = this.create_output_area();
505 503 var subarea = $('<div/>').addClass('output_subarea output_unrecognized');
506 504 toinsert.append(subarea);
507 505 subarea.append(
508 506 $("<a>")
509 507 .attr("href", "#")
510 508 .text("Unrecognized output: " + json.output_type)
511 509 .click(function () {
512 510 that.events.trigger('unrecognized_output.OutputArea', {output: json})
513 511 })
514 512 );
515 513 this._safe_append(toinsert);
516 514 };
517 515
518 516
519 517 OutputArea.prototype.append_display_data = function (json, handle_inserted) {
520 518 var toinsert = this.create_output_area();
521 519 if (this.append_mime_type(json, toinsert, handle_inserted)) {
522 520 this._safe_append(toinsert);
523 521 // If we just output latex, typeset it.
524 522 if ((json.data['text/latex'] !== undefined) ||
525 523 (json.data['text/html'] !== undefined) ||
526 524 (json.data['text/markdown'] !== undefined)) {
527 525 this.typeset();
528 526 }
529 527 }
530 528 };
531 529
532 530
533 531 OutputArea.safe_outputs = {
534 532 'text/plain' : true,
535 533 'text/latex' : true,
536 534 'image/png' : true,
537 535 'image/jpeg' : true
538 536 };
539 537
540 538 OutputArea.prototype.append_mime_type = function (json, element, handle_inserted) {
541 539 for (var i=0; i < OutputArea.display_order.length; i++) {
542 540 var type = OutputArea.display_order[i];
543 541 var append = OutputArea.append_map[type];
544 542 if ((json.data[type] !== undefined) && append) {
545 543 var value = json.data[type];
546 544 if (!this.trusted && !OutputArea.safe_outputs[type]) {
547 545 // not trusted, sanitize HTML
548 546 if (type==='text/html' || type==='text/svg') {
549 547 value = security.sanitize_html(value);
550 548 } else {
551 549 // don't display if we don't know how to sanitize it
552 550 console.log("Ignoring untrusted " + type + " output.");
553 551 continue;
554 552 }
555 553 }
556 554 var md = json.metadata || {};
557 555 var toinsert = append.apply(this, [value, md, element, handle_inserted]);
558 556 // Since only the png and jpeg mime types call the inserted
559 557 // callback, if the mime type is something other we must call the
560 558 // inserted callback only when the element is actually inserted
561 559 // into the DOM. Use a timeout of 0 to do this.
562 560 if (['image/png', 'image/jpeg'].indexOf(type) < 0 && handle_inserted !== undefined) {
563 561 setTimeout(handle_inserted, 0);
564 562 }
565 563 this.events.trigger('output_appended.OutputArea', [type, value, md, toinsert]);
566 564 return toinsert;
567 565 }
568 566 }
569 567 return null;
570 568 };
571 569
572 570
573 571 var append_html = function (html, md, element) {
574 572 var type = 'text/html';
575 573 var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
576 574 this.keyboard_manager.register_events(toinsert);
577 575 toinsert.append(html);
578 576 element.append(toinsert);
579 577 return toinsert;
580 578 };
581 579
582 580
583 581 var append_markdown = function(markdown, md, element) {
584 582 var type = 'text/markdown';
585 583 var toinsert = this.create_output_subarea(md, "output_markdown", type);
586 584 var text_and_math = mathjaxutils.remove_math(markdown);
587 585 var text = text_and_math[0];
588 586 var math = text_and_math[1];
589 587 marked(text, function (err, html) {
590 588 html = mathjaxutils.replace_math(html, math);
591 589 toinsert.append(html);
592 590 });
593 591 element.append(toinsert);
594 592 return toinsert;
595 593 };
596 594
597 595
598 596 var append_javascript = function (js, md, element) {
599 597 /**
600 598 * We just eval the JS code, element appears in the local scope.
601 599 */
602 600 var type = 'application/javascript';
603 601 var toinsert = this.create_output_subarea(md, "output_javascript", type);
604 602 this.keyboard_manager.register_events(toinsert);
605 603 element.append(toinsert);
606 604
607 605 // Fix for ipython/issues/5293, make sure `element` is the area which
608 606 // output can be inserted into at the time of JS execution.
609 607 element = toinsert;
610 608 try {
611 609 eval(js);
612 610 } catch(err) {
613 611 console.log(err);
614 612 this._append_javascript_error(err, toinsert);
615 613 }
616 614 return toinsert;
617 615 };
618 616
619 617
620 618 var append_text = function (data, md, element) {
621 619 var type = 'text/plain';
622 620 var toinsert = this.create_output_subarea(md, "output_text", type);
623 621 // escape ANSI & HTML specials in plaintext:
624 622 data = utils.fixConsole(data);
625 623 data = utils.fixCarriageReturn(data);
626 624 data = utils.autoLinkUrls(data);
627 625 // The only user content injected with this HTML call is
628 626 // escaped by the fixConsole() method.
629 627 toinsert.append($("<pre/>").html(data));
630 628 element.append(toinsert);
631 629 return toinsert;
632 630 };
633 631
634 632
635 633 var append_svg = function (svg_html, md, element) {
636 634 var type = 'image/svg+xml';
637 635 var toinsert = this.create_output_subarea(md, "output_svg", type);
638 636
639 637 // Get the svg element from within the HTML.
640 638 var svg = $('<div />').html(svg_html).find('svg');
641 639 var svg_area = $('<div />');
642 640 var width = svg.attr('width');
643 641 var height = svg.attr('height');
644 642 svg
645 643 .width('100%')
646 644 .height('100%');
647 645 svg_area
648 646 .width(width)
649 647 .height(height);
650 648
651 649 // The jQuery resize handlers don't seem to work on the svg element.
652 650 // When the svg renders completely, measure it's size and set the parent
653 651 // div to that size. Then set the svg to 100% the size of the parent
654 652 // div and make the parent div resizable.
655 653 this._dblclick_to_reset_size(svg_area, true, false);
656 654
657 655 svg_area.append(svg);
658 656 toinsert.append(svg_area);
659 657 element.append(toinsert);
660 658
661 659 return toinsert;
662 660 };
663 661
664 662 OutputArea.prototype._dblclick_to_reset_size = function (img, immediately, resize_parent) {
665 663 /**
666 664 * Add a resize handler to an element
667 665 *
668 666 * img: jQuery element
669 667 * immediately: bool=False
670 668 * Wait for the element to load before creating the handle.
671 669 * resize_parent: bool=True
672 670 * Should the parent of the element be resized when the element is
673 671 * reset (by double click).
674 672 */
675 673 var callback = function (){
676 674 var h0 = img.height();
677 675 var w0 = img.width();
678 676 if (!(h0 && w0)) {
679 677 // zero size, don't make it resizable
680 678 return;
681 679 }
682 680 img.resizable({
683 681 aspectRatio: true,
684 682 autoHide: true
685 683 });
686 684 img.dblclick(function () {
687 685 // resize wrapper & image together for some reason:
688 686 img.height(h0);
689 687 img.width(w0);
690 688 if (resize_parent === undefined || resize_parent) {
691 689 img.parent().height(h0);
692 690 img.parent().width(w0);
693 691 }
694 692 });
695 693 };
696 694
697 695 if (immediately) {
698 696 callback();
699 697 } else {
700 698 img.on("load", callback);
701 699 }
702 700 };
703 701
704 702 var set_width_height = function (img, md, mime) {
705 703 /**
706 704 * set width and height of an img element from metadata
707 705 */
708 706 var height = _get_metadata_key(md, 'height', mime);
709 707 if (height !== undefined) img.attr('height', height);
710 708 var width = _get_metadata_key(md, 'width', mime);
711 709 if (width !== undefined) img.attr('width', width);
712 710 };
713 711
714 712 var append_png = function (png, md, element, handle_inserted) {
715 713 var type = 'image/png';
716 714 var toinsert = this.create_output_subarea(md, "output_png", type);
717 715 var img = $("<img/>");
718 716 if (handle_inserted !== undefined) {
719 717 img.on('load', function(){
720 718 handle_inserted(img);
721 719 });
722 720 }
723 721 img[0].src = 'data:image/png;base64,'+ png;
724 722 set_width_height(img, md, 'image/png');
725 723 this._dblclick_to_reset_size(img);
726 724 toinsert.append(img);
727 725 element.append(toinsert);
728 726 return toinsert;
729 727 };
730 728
731 729
732 730 var append_jpeg = function (jpeg, md, element, handle_inserted) {
733 731 var type = 'image/jpeg';
734 732 var toinsert = this.create_output_subarea(md, "output_jpeg", type);
735 733 var img = $("<img/>");
736 734 if (handle_inserted !== undefined) {
737 735 img.on('load', function(){
738 736 handle_inserted(img);
739 737 });
740 738 }
741 739 img[0].src = 'data:image/jpeg;base64,'+ jpeg;
742 740 set_width_height(img, md, 'image/jpeg');
743 741 this._dblclick_to_reset_size(img);
744 742 toinsert.append(img);
745 743 element.append(toinsert);
746 744 return toinsert;
747 745 };
748 746
749 747
750 748 var append_pdf = function (pdf, md, element) {
751 749 var type = 'application/pdf';
752 750 var toinsert = this.create_output_subarea(md, "output_pdf", type);
753 751 var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
754 752 a.attr('target', '_blank');
755 753 a.text('View PDF');
756 754 toinsert.append(a);
757 755 element.append(toinsert);
758 756 return toinsert;
759 757 };
760 758
761 759 var append_latex = function (latex, md, element) {
762 760 /**
763 761 * This method cannot do the typesetting because the latex first has to
764 762 * be on the page.
765 763 */
766 764 var type = 'text/latex';
767 765 var toinsert = this.create_output_subarea(md, "output_latex", type);
768 766 toinsert.append(latex);
769 767 element.append(toinsert);
770 768 return toinsert;
771 769 };
772 770
773 771
774 772 OutputArea.prototype.append_raw_input = function (msg) {
775 773 var that = this;
776 774 this.expand();
777 775 var content = msg.content;
778 776 var area = this.create_output_area();
779 777
780 778 // disable any other raw_inputs, if they are left around
781 779 $("div.output_subarea.raw_input_container").remove();
782 780
783 781 var input_type = content.password ? 'password' : 'text';
784 782
785 783 area.append(
786 784 $("<div/>")
787 785 .addClass("box-flex1 output_subarea raw_input_container")
788 786 .append(
789 787 $("<span/>")
790 788 .addClass("raw_input_prompt")
791 789 .text(content.prompt)
792 790 )
793 791 .append(
794 792 $("<input/>")
795 793 .addClass("raw_input")
796 794 .attr('type', input_type)
797 795 .attr("size", 47)
798 796 .keydown(function (event, ui) {
799 797 // make sure we submit on enter,
800 798 // and don't re-execute the *cell* on shift-enter
801 799 if (event.which === keyboard.keycodes.enter) {
802 800 that._submit_raw_input();
803 801 return false;
804 802 }
805 803 })
806 804 )
807 805 );
808 806
809 807 this.element.append(area);
810 808 var raw_input = area.find('input.raw_input');
811 809 // Register events that enable/disable the keyboard manager while raw
812 810 // input is focused.
813 811 this.keyboard_manager.register_events(raw_input);
814 812 // Note, the following line used to read raw_input.focus().focus().
815 813 // This seemed to be needed otherwise only the cell would be focused.
816 814 // But with the modal UI, this seems to work fine with one call to focus().
817 815 raw_input.focus();
818 816 };
819 817
820 818 OutputArea.prototype._submit_raw_input = function (evt) {
821 819 var container = this.element.find("div.raw_input_container");
822 820 var theprompt = container.find("span.raw_input_prompt");
823 821 var theinput = container.find("input.raw_input");
824 822 var value = theinput.val();
825 823 var echo = value;
826 824 // don't echo if it's a password
827 825 if (theinput.attr('type') == 'password') {
828 826 echo = 'Β·Β·Β·Β·Β·Β·Β·Β·';
829 827 }
830 828 var content = {
831 829 output_type : 'stream',
832 830 name : 'stdout',
833 831 text : theprompt.text() + echo + '\n'
834 832 };
835 833 // remove form container
836 834 container.parent().remove();
837 835 // replace with plaintext version in stdout
838 836 this.append_output(content, false);
839 837 this.events.trigger('send_input_reply.Kernel', value);
840 838 };
841 839
842 840
843 841 OutputArea.prototype.handle_clear_output = function (msg) {
844 842 /**
845 843 * msg spec v4 had stdout, stderr, display keys
846 844 * v4.1 replaced these with just wait
847 845 * The default behavior is the same (stdout=stderr=display=True, wait=False),
848 846 * so v4 messages will still be properly handled,
849 847 * except for the rarely used clearing less than all output.
850 848 */
851 849 this.clear_output(msg.content.wait || false);
852 850 };
853 851
854 852
855 853 OutputArea.prototype.clear_output = function(wait) {
856 854 if (wait) {
857 855
858 856 // If a clear is queued, clear before adding another to the queue.
859 857 if (this.clear_queued) {
860 858 this.clear_output(false);
861 859 }
862 860
863 861 this.clear_queued = true;
864 862 } else {
865 863
866 864 // Fix the output div's height if the clear_output is waiting for
867 865 // new output (it is being used in an animation).
868 866 if (this.clear_queued) {
869 867 var height = this.element.height();
870 868 this.element.height(height);
871 869 this.clear_queued = false;
872 870 }
873 871
874 872 // Clear all
875 873 // Remove load event handlers from img tags because we don't want
876 874 // them to fire if the image is never added to the page.
877 875 this.element.find('img').off('load');
878 876 this.element.html("");
879 877
880 878 // Notify others of changes.
881 879 this.element.trigger('changed');
882 880
883 881 this.outputs = [];
884 882 this.trusted = true;
885 883 this.unscroll_area();
886 884 return;
887 885 }
888 886 };
889 887
890 888
891 889 // JSON serialization
892 890
893 891 OutputArea.prototype.fromJSON = function (outputs, metadata) {
894 892 var len = outputs.length;
895 893 metadata = metadata || {};
896 894
897 895 for (var i=0; i<len; i++) {
898 896 this.append_output(outputs[i]);
899 897 }
900 898
901 899 if (metadata.collapsed !== undefined) {
902 900 this.collapsed = metadata.collapsed;
903 901 if (metadata.collapsed) {
904 902 this.collapse_output();
905 903 }
906 904 }
907 905 if (metadata.autoscroll !== undefined) {
908 906 this.collapsed = metadata.collapsed;
909 907 if (metadata.collapsed) {
910 908 this.collapse_output();
911 909 } else {
912 910 this.expand_output();
913 911 }
914 912 }
915 913 };
916 914
917 915
918 916 OutputArea.prototype.toJSON = function () {
919 917 return this.outputs;
920 918 };
921 919
922 920 /**
923 921 * Class properties
924 922 **/
925 923
926 924 /**
927 925 * Threshold to trigger autoscroll when the OutputArea is resized,
928 926 * typically when new outputs are added.
929 927 *
930 928 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
931 929 * unless it is < 0, in which case autoscroll will never be triggered
932 930 *
933 931 * @property auto_scroll_threshold
934 932 * @type Number
935 933 * @default 100
936 934 *
937 935 **/
938 936 OutputArea.auto_scroll_threshold = 100;
939 937
940 938 /**
941 939 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
942 940 * shorter than this are never scrolled.
943 941 *
944 942 * @property minimum_scroll_threshold
945 943 * @type Number
946 944 * @default 20
947 945 *
948 946 **/
949 947 OutputArea.minimum_scroll_threshold = 20;
950 948
951 949
952 950 OutputArea.display_order = [
953 951 'application/javascript',
954 952 'text/html',
955 953 'text/markdown',
956 954 'text/latex',
957 955 'image/svg+xml',
958 956 'image/png',
959 957 'image/jpeg',
960 958 'application/pdf',
961 959 'text/plain'
962 960 ];
963 961
964 962 OutputArea.append_map = {
965 963 "text/plain" : append_text,
966 964 "text/html" : append_html,
967 965 "text/markdown": append_markdown,
968 966 "image/svg+xml" : append_svg,
969 967 "image/png" : append_png,
970 968 "image/jpeg" : append_jpeg,
971 969 "text/latex" : append_latex,
972 970 "application/javascript" : append_javascript,
973 971 "application/pdf" : append_pdf
974 972 };
975 973
976 974 // For backwards compatability.
977 975 IPython.OutputArea = OutputArea;
978 976
979 977 return {'OutputArea': OutputArea};
980 978 });
@@ -1,695 +1,683 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define(["widgets/js/manager",
5 5 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/utils",
9 9 "base/js/namespace",
10 10 ], function(widgetmanager, _, Backbone, $, utils, IPython){
11 11
12 12 var WidgetModel = Backbone.Model.extend({
13 13 constructor: function (widget_manager, model_id, comm) {
14 14 /**
15 15 * Constructor
16 16 *
17 17 * Creates a WidgetModel instance.
18 18 *
19 19 * Parameters
20 20 * ----------
21 21 * widget_manager : WidgetManager instance
22 22 * model_id : string
23 23 * An ID unique to this model.
24 24 * comm : Comm instance (optional)
25 25 */
26 26 this.widget_manager = widget_manager;
27 27 this.state_change = Promise.resolve();
28 28 this._buffered_state_diff = {};
29 29 this.pending_msgs = 0;
30 30 this.msg_buffer = null;
31 31 this.state_lock = null;
32 32 this.id = model_id;
33 33 this.views = {};
34 34
35 35 if (comm !== undefined) {
36 36 // Remember comm associated with the model.
37 37 this.comm = comm;
38 38 comm.model = this;
39 39
40 40 // Hook comm messages up to model.
41 41 comm.on_close($.proxy(this._handle_comm_closed, this));
42 42 comm.on_msg($.proxy(this._handle_comm_msg, this));
43 43 }
44 44 return Backbone.Model.apply(this);
45 45 },
46 46
47 47 send: function (content, callbacks) {
48 48 /**
49 49 * Send a custom msg over the comm.
50 50 */
51 51 if (this.comm !== undefined) {
52 52 var data = {method: 'custom', content: content};
53 53 this.comm.send(data, callbacks);
54 54 this.pending_msgs++;
55 55 }
56 56 },
57 57
58 58 _handle_comm_closed: function (msg) {
59 59 /**
60 60 * Handle when a widget is closed.
61 61 */
62 62 this.trigger('comm:close');
63 63 this.stopListening();
64 64 this.trigger('destroy', this);
65 65 delete this.comm.model; // Delete ref so GC will collect widget model.
66 66 delete this.comm;
67 67 delete this.model_id; // Delete id from model so widget manager cleans up.
68 68 for (var id in this.views) {
69 69 if (this.views.hasOwnProperty(id)) {
70 70 this.views[id].remove();
71 71 }
72 72 }
73 73 },
74 74
75 75 _handle_comm_msg: function (msg) {
76 76 /**
77 77 * Handle incoming comm msg.
78 78 */
79 79 var method = msg.content.data.method;
80 80 var that = this;
81 81 switch (method) {
82 82 case 'update':
83 83 this.state_change = this.state_change.then(function() {
84 84 return that.set_state(msg.content.data.state);
85 85 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true));
86 86 break;
87 87 case 'custom':
88 88 this.trigger('msg:custom', msg.content.data.content);
89 89 break;
90 90 case 'display':
91 91 this.widget_manager.display_view(msg, this);
92 92 break;
93 93 }
94 94 },
95 95
96 96 set_state: function (state) {
97 97 var that = this;
98 98 // Handle when a widget is updated via the python side.
99 99 return this._unpack_models(state).then(function(state) {
100 100 that.state_lock = state;
101 101 try {
102 102 WidgetModel.__super__.set.call(that, state);
103 103 } finally {
104 104 that.state_lock = null;
105 105 }
106 106 }).catch(utils.reject("Couldn't set model state", true));
107 107 },
108 108
109 109 _handle_status: function (msg, callbacks) {
110 110 /**
111 111 * Handle status msgs.
112 112 *
113 113 * execution_state : ('busy', 'idle', 'starting')
114 114 */
115 115 if (this.comm !== undefined) {
116 116 if (msg.content.execution_state ==='idle') {
117 117 // Send buffer if this message caused another message to be
118 118 // throttled.
119 119 if (this.msg_buffer !== null &&
120 120 (this.get('msg_throttle') || 3) === this.pending_msgs) {
121 121 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
122 122 this.comm.send(data, callbacks);
123 123 this.msg_buffer = null;
124 124 } else {
125 125 --this.pending_msgs;
126 126 }
127 127 }
128 128 }
129 129 },
130 130
131 131 callbacks: function(view) {
132 132 /**
133 133 * Create msg callbacks for a comm msg.
134 134 */
135 135 var callbacks = this.widget_manager.callbacks(view);
136 136
137 137 if (callbacks.iopub === undefined) {
138 138 callbacks.iopub = {};
139 139 }
140 140
141 141 var that = this;
142 142 callbacks.iopub.status = function (msg) {
143 143 that._handle_status(msg, callbacks);
144 144 };
145 145 return callbacks;
146 146 },
147 147
148 148 set: function(key, val, options) {
149 149 /**
150 150 * Set a value.
151 151 */
152 152 var return_value = WidgetModel.__super__.set.apply(this, arguments);
153 153
154 154 // Backbone only remembers the diff of the most recent set()
155 155 // operation. Calling set multiple times in a row results in a
156 156 // loss of diff information. Here we keep our own running diff.
157 157 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
158 158 return return_value;
159 159 },
160 160
161 161 sync: function (method, model, options) {
162 162 /**
163 163 * Handle sync to the back-end. Called when a model.save() is called.
164 164 *
165 165 * Make sure a comm exists.
166 166 */
167 167 var error = options.error || function() {
168 168 console.error('Backbone sync error:', arguments);
169 169 };
170 170 if (this.comm === undefined) {
171 171 error();
172 172 return false;
173 173 }
174 174
175 175 // Delete any key value pairs that the back-end already knows about.
176 176 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
177 177 if (this.state_lock !== null) {
178 178 var keys = Object.keys(this.state_lock);
179 179 for (var i=0; i<keys.length; i++) {
180 180 var key = keys[i];
181 181 if (attrs[key] === this.state_lock[key]) {
182 182 delete attrs[key];
183 183 }
184 184 }
185 185 }
186 186
187 187 // Only sync if there are attributes to send to the back-end.
188 188 attrs = this._pack_models(attrs);
189 189 if (_.size(attrs) > 0) {
190 190
191 191 // If this message was sent via backbone itself, it will not
192 192 // have any callbacks. It's important that we create callbacks
193 193 // so we can listen for status messages, etc...
194 194 var callbacks = options.callbacks || this.callbacks();
195 195
196 196 // Check throttle.
197 197 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
198 198 // The throttle has been exceeded, buffer the current msg so
199 199 // it can be sent once the kernel has finished processing
200 200 // some of the existing messages.
201 201
202 202 // Combine updates if it is a 'patch' sync, otherwise replace updates
203 203 switch (method) {
204 204 case 'patch':
205 205 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
206 206 break;
207 207 case 'update':
208 208 case 'create':
209 209 this.msg_buffer = attrs;
210 210 break;
211 211 default:
212 212 error();
213 213 return false;
214 214 }
215 215 this.msg_buffer_callbacks = callbacks;
216 216
217 217 } else {
218 218 // We haven't exceeded the throttle, send the message like
219 219 // normal.
220 220 var data = {method: 'backbone', sync_data: attrs};
221 221 this.comm.send(data, callbacks);
222 222 this.pending_msgs++;
223 223 }
224 224 }
225 225 // Since the comm is a one-way communication, assume the message
226 226 // arrived. Don't call success since we don't have a model back from the server
227 227 // this means we miss out on the 'sync' event.
228 228 this._buffered_state_diff = {};
229 229 },
230 230
231 231 save_changes: function(callbacks) {
232 232 /**
233 233 * Push this model's state to the back-end
234 234 *
235 235 * This invokes a Backbone.Sync.
236 236 */
237 237 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
238 238 },
239 239
240 240 _pack_models: function(value) {
241 241 /**
242 242 * Replace models with model ids recursively.
243 243 */
244 244 var that = this;
245 245 var packed;
246 246 if (value instanceof Backbone.Model) {
247 247 return "IPY_MODEL_" + value.id;
248 248
249 249 } else if ($.isArray(value)) {
250 250 packed = [];
251 251 _.each(value, function(sub_value, key) {
252 252 packed.push(that._pack_models(sub_value));
253 253 });
254 254 return packed;
255 255 } else if (value instanceof Date || value instanceof String) {
256 256 return value;
257 257 } else if (value instanceof Object) {
258 258 packed = {};
259 259 _.each(value, function(sub_value, key) {
260 260 packed[key] = that._pack_models(sub_value);
261 261 });
262 262 return packed;
263 263
264 264 } else {
265 265 return value;
266 266 }
267 267 },
268 268
269 269 _unpack_models: function(value) {
270 270 /**
271 271 * Replace model ids with models recursively.
272 272 */
273 273 var that = this;
274 274 var unpacked;
275 275 if ($.isArray(value)) {
276 276 unpacked = [];
277 277 _.each(value, function(sub_value, key) {
278 278 unpacked.push(that._unpack_models(sub_value));
279 279 });
280 280 return Promise.all(unpacked);
281 281 } else if (value instanceof Object) {
282 282 unpacked = {};
283 283 _.each(value, function(sub_value, key) {
284 284 unpacked[key] = that._unpack_models(sub_value);
285 285 });
286 286 return utils.resolve_promises_dict(unpacked);
287 287 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
288 288 // get_model returns a promise already
289 289 return this.widget_manager.get_model(value.slice(10, value.length));
290 290 } else {
291 291 return Promise.resolve(value);
292 292 }
293 293 },
294 294
295 295 on_some_change: function(keys, callback, context) {
296 296 /**
297 297 * on_some_change(["key1", "key2"], foo, context) differs from
298 298 * on("change:key1 change:key2", foo, context).
299 299 * If the widget attributes key1 and key2 are both modified,
300 300 * the second form will result in foo being called twice
301 301 * while the first will call foo only once.
302 302 */
303 303 this.on('change', function() {
304 304 if (keys.some(this.hasChanged, this)) {
305 305 callback.apply(context);
306 306 }
307 307 }, this);
308 308
309 309 },
310 310 });
311 311 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
312 312
313 313
314 314 var WidgetView = Backbone.View.extend({
315 315 initialize: function(parameters) {
316 316 /**
317 317 * Public constructor.
318 318 */
319 319 this.model.on('change',this.update,this);
320 320 this.options = parameters.options;
321 321 this.id = this.id || utils.uuid();
322 322 this.model.views[this.id] = this;
323 323 this.on('displayed', function() {
324 324 this.is_displayed = true;
325 325 }, this);
326 326 },
327 327
328 328 update: function(){
329 329 /**
330 330 * Triggered on model change.
331 331 *
332 332 * Update view to be consistent with this.model
333 333 */
334 334 },
335 335
336 336 create_child_view: function(child_model, options) {
337 337 /**
338 338 * Create and promise that resolves to a child view of a given model
339 339 */
340 340 var that = this;
341 341 options = $.extend({ parent: this }, options || {});
342 342 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view"), true);
343 343 },
344 344
345 345 callbacks: function(){
346 346 /**
347 347 * Create msg callbacks for a comm msg.
348 348 */
349 349 return this.model.callbacks(this);
350 350 },
351 351
352 352 render: function(){
353 353 /**
354 354 * Render the view.
355 355 *
356 356 * By default, this is only called the first time the view is created
357 357 */
358 358 },
359 359
360 360 show: function(){
361 361 /**
362 362 * Show the widget-area
363 363 */
364 364 if (this.options && this.options.cell &&
365 365 this.options.cell.widget_area !== undefined) {
366 366 this.options.cell.widget_area.show();
367 367 }
368 368 },
369 369
370 370 send: function (content) {
371 371 /**
372 372 * Send a custom msg associated with this view.
373 373 */
374 374 this.model.send(content, this.callbacks());
375 375 },
376 376
377 377 touch: function () {
378 378 this.model.save_changes(this.callbacks());
379 379 },
380 380
381 381 after_displayed: function (callback, context) {
382 382 /**
383 383 * Calls the callback right away is the view is already displayed
384 384 * otherwise, register the callback to the 'displayed' event.
385 385 */
386 386 if (this.is_displayed) {
387 387 callback.apply(context);
388 388 } else {
389 389 this.on('displayed', callback, context);
390 390 }
391 391 }
392 392 });
393 393
394 394
395 395 var DOMWidgetView = WidgetView.extend({
396 396 initialize: function (parameters) {
397 397 /**
398 398 * Public constructor
399 399 */
400 400 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
401 401 this.on('displayed', this.show, this);
402 402 this.model.on('change:visible', this.update_visible, this);
403 403 this.model.on('change:_css', this.update_css, this);
404 404
405 405 this.model.on('change:_dom_classes', function(model, new_classes) {
406 406 var old_classes = model.previous('_dom_classes');
407 407 this.update_classes(old_classes, new_classes);
408 408 }, this);
409 409
410 410 this.model.on('change:color', function (model, value) {
411 411 this.update_attr('color', value); }, this);
412 412
413 413 this.model.on('change:background_color', function (model, value) {
414 414 this.update_attr('background', value); }, this);
415 415
416 416 this.model.on('change:width', function (model, value) {
417 417 this.update_attr('width', value); }, this);
418 418
419 419 this.model.on('change:height', function (model, value) {
420 420 this.update_attr('height', value); }, this);
421 421
422 422 this.model.on('change:border_color', function (model, value) {
423 423 this.update_attr('border-color', value); }, this);
424 424
425 425 this.model.on('change:border_width', function (model, value) {
426 426 this.update_attr('border-width', value); }, this);
427 427
428 428 this.model.on('change:border_style', function (model, value) {
429 429 this.update_attr('border-style', value); }, this);
430 430
431 431 this.model.on('change:font_style', function (model, value) {
432 432 this.update_attr('font-style', value); }, this);
433 433
434 434 this.model.on('change:font_weight', function (model, value) {
435 435 this.update_attr('font-weight', value); }, this);
436 436
437 437 this.model.on('change:font_size', function (model, value) {
438 438 this.update_attr('font-size', this._default_px(value)); }, this);
439 439
440 440 this.model.on('change:font_family', function (model, value) {
441 441 this.update_attr('font-family', value); }, this);
442 442
443 443 this.model.on('change:padding', function (model, value) {
444 444 this.update_attr('padding', value); }, this);
445 445
446 446 this.model.on('change:margin', function (model, value) {
447 447 this.update_attr('margin', this._default_px(value)); }, this);
448 448
449 449 this.model.on('change:border_radius', function (model, value) {
450 450 this.update_attr('border-radius', this._default_px(value)); }, this);
451 451
452 452 this.after_displayed(function() {
453 453 this.update_visible(this.model, this.model.get("visible"));
454 454 this.update_classes([], this.model.get('_dom_classes'));
455 455
456 456 this.update_attr('color', this.model.get('color'));
457 457 this.update_attr('background', this.model.get('background_color'));
458 458 this.update_attr('width', this.model.get('width'));
459 459 this.update_attr('height', this.model.get('height'));
460 460 this.update_attr('border-color', this.model.get('border_color'));
461 461 this.update_attr('border-width', this.model.get('border_width'));
462 462 this.update_attr('border-style', this.model.get('border_style'));
463 463 this.update_attr('font-style', this.model.get('font_style'));
464 464 this.update_attr('font-weight', this.model.get('font_weight'));
465 465 this.update_attr('font-size', this.model.get('font_size'));
466 466 this.update_attr('font-family', this.model.get('font_family'));
467 467 this.update_attr('padding', this.model.get('padding'));
468 468 this.update_attr('margin', this.model.get('margin'));
469 469 this.update_attr('border-radius', this.model.get('border_radius'));
470 470
471 471 this.update_css(this.model, this.model.get("_css"));
472 472 }, this);
473 473 },
474 474
475 475 _default_px: function(value) {
476 476 /**
477 477 * Makes browser interpret a numerical string as a pixel value.
478 478 */
479 479 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
480 480 return value.trim() + 'px';
481 481 }
482 482 return value;
483 483 },
484 484
485 485 update_attr: function(name, value) {
486 486 /**
487 487 * Set a css attr of the widget view.
488 488 */
489 489 this.$el.css(name, value);
490 490 },
491 491
492 492 update_visible: function(model, value) {
493 493 /**
494 494 * Update visibility
495 495 */
496 496 this.$el.toggle(value);
497 497 },
498 498
499 499 update_css: function (model, css) {
500 500 /**
501 501 * Update the css styling of this view.
502 502 */
503 503 var e = this.$el;
504 504 if (css === undefined) {return;}
505 505 for (var i = 0; i < css.length; i++) {
506 506 // Apply the css traits to all elements that match the selector.
507 507 var selector = css[i][0];
508 508 var elements = this._get_selector_element(selector);
509 509 if (elements.length > 0) {
510 510 var trait_key = css[i][1];
511 511 var trait_value = css[i][2];
512 512 elements.css(trait_key ,trait_value);
513 513 }
514 514 }
515 515 },
516 516
517 517 update_classes: function (old_classes, new_classes, $el) {
518 518 /**
519 519 * Update the DOM classes applied to an element, default to this.$el.
520 520 */
521 521 if ($el===undefined) {
522 522 $el = this.$el;
523 523 }
524 524 _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);})
525 525 _.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);})
526 526 },
527 527
528 528 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
529 529 /**
530 530 * Update the DOM classes applied to the widget based on a single
531 531 * trait's value.
532 532 *
533 533 * Given a trait value classes map, this function automatically
534 534 * handles applying the appropriate classes to the widget element
535 535 * and removing classes that are no longer valid.
536 536 *
537 537 * Parameters
538 538 * ----------
539 539 * class_map: dictionary
540 540 * Dictionary of trait values to class lists.
541 541 * Example:
542 542 * {
543 543 * success: ['alert', 'alert-success'],
544 544 * info: ['alert', 'alert-info'],
545 545 * warning: ['alert', 'alert-warning'],
546 546 * danger: ['alert', 'alert-danger']
547 547 * };
548 548 * trait_name: string
549 549 * Name of the trait to check the value of.
550 550 * previous_trait_value: optional string, default ''
551 551 * Last trait value
552 552 * $el: optional jQuery element handle, defaults to this.$el
553 553 * Element that the classes are applied to.
554 554 */
555 555 var key = previous_trait_value;
556 556 if (key === undefined) {
557 557 key = this.model.previous(trait_name);
558 558 }
559 559 var old_classes = class_map[key] ? class_map[key] : [];
560 560 key = this.model.get(trait_name);
561 561 var new_classes = class_map[key] ? class_map[key] : [];
562 562
563 563 this.update_classes(old_classes, new_classes, $el || this.$el);
564 564 },
565 565
566 566 _get_selector_element: function (selector) {
567 567 /**
568 568 * Get the elements via the css selector.
569 569 */
570 570 var elements;
571 571 if (!selector) {
572 572 elements = this.$el;
573 573 } else {
574 574 elements = this.$el.find(selector).addBack(selector);
575 575 }
576 576 return elements;
577 577 },
578 578
579 579 typeset: function(element, text){
580 // after (optionally) updating a node(list) or jQuery selection's
581 // text, check if MathJax is available and typeset it
582 var $el = element.jquery ? element : $(element);
583
584 if(arguments.length > 1){
585 $el.text(text);
586 }
587 if(!window.MathJax){
588 return;
589 }
590 return $el.map(function(){
591 return MathJax.Hub.Queue(["Typeset", MathJax.Hub, this]);
592 });
580 utils.typeset.apply(null, arguments);
593 581 },
594 582 });
595 583
596 584
597 585 var ViewList = function(create_view, remove_view, context) {
598 586 /**
599 587 * - create_view and remove_view are default functions called when adding or removing views
600 588 * - create_view takes a model and returns a view or a promise for a view for that model
601 589 * - remove_view takes a view and destroys it (including calling `view.remove()`)
602 590 * - each time the update() function is called with a new list, the create and remove
603 591 * callbacks will be called in an order so that if you append the views created in the
604 592 * create callback and remove the views in the remove callback, you will duplicate
605 593 * the order of the list.
606 594 * - the remove callback defaults to just removing the view (e.g., pass in null for the second parameter)
607 595 * - the context defaults to the created ViewList. If you pass another context, the create and remove
608 596 * will be called in that context.
609 597 */
610 598
611 599 this.initialize.apply(this, arguments);
612 600 };
613 601
614 602 _.extend(ViewList.prototype, {
615 603 initialize: function(create_view, remove_view, context) {
616 604 this.state_change = Promise.resolve();
617 605 this._handler_context = context || this;
618 606 this._models = [];
619 607 this.views = [];
620 608 this._create_view = create_view;
621 609 this._remove_view = remove_view || function(view) {view.remove();};
622 610 },
623 611
624 612 update: function(new_models, create_view, remove_view, context) {
625 613 /**
626 614 * the create_view, remove_view, and context arguments override the defaults
627 615 * specified when the list is created.
628 616 * returns a promise that resolves after this update is done
629 617 */
630 618 var remove = remove_view || this._remove_view;
631 619 var create = create_view || this._create_view;
632 620 if (create === undefined || remove === undefined){
633 621 console.error("Must define a create a remove function");
634 622 }
635 623 var context = context || this._handler_context;
636 624 var added_views = [];
637 625 var that = this;
638 626 this.state_change = this.state_change.then(function() {
639 627 var i;
640 628 // first, skip past the beginning of the lists if they are identical
641 629 for (i = 0; i < new_models.length; i++) {
642 630 if (i >= that._models.length || new_models[i] !== that._models[i]) {
643 631 break;
644 632 }
645 633 }
646 634 var first_removed = i;
647 635 // Remove the non-matching items from the old list.
648 636 for (var j = first_removed; j < that._models.length; j++) {
649 637 remove.call(context, that.views[j]);
650 638 }
651 639
652 640 // Add the rest of the new list items.
653 641 for (; i < new_models.length; i++) {
654 642 added_views.push(create.call(context, new_models[i]));
655 643 }
656 644 // make a copy of the input array
657 645 that._models = new_models.slice();
658 646 return Promise.all(added_views).then(function(added) {
659 647 Array.prototype.splice.apply(that.views, [first_removed, that.views.length].concat(added));
660 648 return that.views;
661 649 });
662 650 });
663 651 return this.state_change;
664 652 },
665 653
666 654 remove: function() {
667 655 /**
668 656 * removes every view in the list; convenience function for `.update([])`
669 657 * that should be faster
670 658 * returns a promise that resolves after this removal is done
671 659 */
672 660 var that = this;
673 661 this.state_change = this.state_change.then(function() {
674 662 for (var i = 0; i < that.views.length; i++) {
675 663 that._remove_view.call(that._handler_context, that.views[i]);
676 664 }
677 665 that._models = [];
678 666 that.views = [];
679 667 });
680 668 return this.state_change;
681 669 },
682 670 });
683 671
684 672 var widget = {
685 673 'WidgetModel': WidgetModel,
686 674 'WidgetView': WidgetView,
687 675 'DOMWidgetView': DOMWidgetView,
688 676 'ViewList': ViewList,
689 677 };
690 678
691 679 // For backwards compatability.
692 680 $.extend(IPython, widget);
693 681
694 682 return widget;
695 683 });
General Comments 0
You need to be logged in to leave comments. Login now