##// END OF EJS Templates
Use rsvp.js for Promises
Jonathan Frederic -
Show More
@@ -1,735 +1,764 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 ], function(IPython, $, CodeMirror){
8 'components/rsvp/rsvp.min',
9 ], function(IPython, $, CodeMirror, rsvp){
9 10 "use strict";
10 11
11 12 IPython.load_extensions = function () {
12 13 // load one or more IPython notebook extensions with requirejs
13 14
14 15 var extensions = [];
15 16 var extension_names = arguments;
16 17 for (var i = 0; i < extension_names.length; i++) {
17 18 extensions.push("nbextensions/" + arguments[i]);
18 19 }
19 20
20 21 require(extensions,
21 22 function () {
22 23 for (var i = 0; i < arguments.length; i++) {
23 24 var ext = arguments[i];
24 25 var ext_name = extension_names[i];
25 26 // success callback
26 27 console.log("Loaded extension: " + ext_name);
27 28 if (ext && ext.load_ipython_extension !== undefined) {
28 29 ext.load_ipython_extension();
29 30 }
30 31 }
31 32 },
32 33 function (err) {
33 34 // failure callback
34 35 console.log("Failed to load extension(s):", err.requireModules, err);
35 36 }
36 37 );
37 38 };
38 39
39 40 //============================================================================
40 41 // Cross-browser RegEx Split
41 42 //============================================================================
42 43
43 44 // This code has been MODIFIED from the code licensed below to not replace the
44 45 // default browser split. The license is reproduced here.
45 46
46 47 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
47 48 /*!
48 49 * Cross-Browser Split 1.1.1
49 50 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
50 51 * Available under the MIT License
51 52 * ECMAScript compliant, uniform cross-browser split method
52 53 */
53 54
54 55 /**
55 56 * Splits a string into an array of strings using a regex or string
56 57 * separator. Matches of the separator are not included in the result array.
57 58 * However, if `separator` is a regex that contains capturing groups,
58 59 * backreferences are spliced into the result each time `separator` is
59 60 * matched. Fixes browser bugs compared to the native
60 61 * `String.prototype.split` and can be used reliably cross-browser.
61 62 * @param {String} str String to split.
62 63 * @param {RegExp|String} separator Regex or string to use for separating
63 64 * the string.
64 65 * @param {Number} [limit] Maximum number of items to include in the result
65 66 * array.
66 67 * @returns {Array} Array of substrings.
67 68 * @example
68 69 *
69 70 * // Basic use
70 71 * regex_split('a b c d', ' ');
71 72 * // -> ['a', 'b', 'c', 'd']
72 73 *
73 74 * // With limit
74 75 * regex_split('a b c d', ' ', 2);
75 76 * // -> ['a', 'b']
76 77 *
77 78 * // Backreferences in result array
78 79 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
79 80 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
80 81 */
81 82 var regex_split = function (str, separator, limit) {
82 83 // If `separator` is not a regex, use `split`
83 84 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
84 85 return split.call(str, separator, limit);
85 86 }
86 87 var output = [],
87 88 flags = (separator.ignoreCase ? "i" : "") +
88 89 (separator.multiline ? "m" : "") +
89 90 (separator.extended ? "x" : "") + // Proposed for ES6
90 91 (separator.sticky ? "y" : ""), // Firefox 3+
91 92 lastLastIndex = 0,
92 93 // Make `global` and avoid `lastIndex` issues by working with a copy
93 94 separator = new RegExp(separator.source, flags + "g"),
94 95 separator2, match, lastIndex, lastLength;
95 96 str += ""; // Type-convert
96 97
97 98 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
98 99 if (!compliantExecNpcg) {
99 100 // Doesn't need flags gy, but they don't hurt
100 101 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
101 102 }
102 103 /* Values for `limit`, per the spec:
103 104 * If undefined: 4294967295 // Math.pow(2, 32) - 1
104 105 * If 0, Infinity, or NaN: 0
105 106 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
106 107 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
107 108 * If other: Type-convert, then use the above rules
108 109 */
109 110 limit = typeof(limit) === "undefined" ?
110 111 -1 >>> 0 : // Math.pow(2, 32) - 1
111 112 limit >>> 0; // ToUint32(limit)
112 113 while (match = separator.exec(str)) {
113 114 // `separator.lastIndex` is not reliable cross-browser
114 115 lastIndex = match.index + match[0].length;
115 116 if (lastIndex > lastLastIndex) {
116 117 output.push(str.slice(lastLastIndex, match.index));
117 118 // Fix browsers whose `exec` methods don't consistently return `undefined` for
118 119 // nonparticipating capturing groups
119 120 if (!compliantExecNpcg && match.length > 1) {
120 121 match[0].replace(separator2, function () {
121 122 for (var i = 1; i < arguments.length - 2; i++) {
122 123 if (typeof(arguments[i]) === "undefined") {
123 124 match[i] = undefined;
124 125 }
125 126 }
126 127 });
127 128 }
128 129 if (match.length > 1 && match.index < str.length) {
129 130 Array.prototype.push.apply(output, match.slice(1));
130 131 }
131 132 lastLength = match[0].length;
132 133 lastLastIndex = lastIndex;
133 134 if (output.length >= limit) {
134 135 break;
135 136 }
136 137 }
137 138 if (separator.lastIndex === match.index) {
138 139 separator.lastIndex++; // Avoid an infinite loop
139 140 }
140 141 }
141 142 if (lastLastIndex === str.length) {
142 143 if (lastLength || !separator.test("")) {
143 144 output.push("");
144 145 }
145 146 } else {
146 147 output.push(str.slice(lastLastIndex));
147 148 }
148 149 return output.length > limit ? output.slice(0, limit) : output;
149 150 };
150 151
151 152 //============================================================================
152 153 // End contributed Cross-browser RegEx Split
153 154 //============================================================================
154 155
155 156
156 157 var uuid = function () {
157 158 // http://www.ietf.org/rfc/rfc4122.txt
158 159 var s = [];
159 160 var hexDigits = "0123456789ABCDEF";
160 161 for (var i = 0; i < 32; i++) {
161 162 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
162 163 }
163 164 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
164 165 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
165 166
166 167 var uuid = s.join("");
167 168 return uuid;
168 169 };
169 170
170 171
171 172 //Fix raw text to parse correctly in crazy XML
172 173 function xmlencode(string) {
173 174 return string.replace(/\&/g,'&'+'amp;')
174 175 .replace(/</g,'&'+'lt;')
175 176 .replace(/>/g,'&'+'gt;')
176 177 .replace(/\'/g,'&'+'apos;')
177 178 .replace(/\"/g,'&'+'quot;')
178 179 .replace(/`/g,'&'+'#96;');
179 180 }
180 181
181 182
182 183 //Map from terminal commands to CSS classes
183 184 var ansi_colormap = {
184 185 "01":"ansibold",
185 186
186 187 "30":"ansiblack",
187 188 "31":"ansired",
188 189 "32":"ansigreen",
189 190 "33":"ansiyellow",
190 191 "34":"ansiblue",
191 192 "35":"ansipurple",
192 193 "36":"ansicyan",
193 194 "37":"ansigray",
194 195
195 196 "40":"ansibgblack",
196 197 "41":"ansibgred",
197 198 "42":"ansibggreen",
198 199 "43":"ansibgyellow",
199 200 "44":"ansibgblue",
200 201 "45":"ansibgpurple",
201 202 "46":"ansibgcyan",
202 203 "47":"ansibggray"
203 204 };
204 205
205 206 function _process_numbers(attrs, numbers) {
206 207 // process ansi escapes
207 208 var n = numbers.shift();
208 209 if (ansi_colormap[n]) {
209 210 if ( ! attrs["class"] ) {
210 211 attrs["class"] = ansi_colormap[n];
211 212 } else {
212 213 attrs["class"] += " " + ansi_colormap[n];
213 214 }
214 215 } else if (n == "38" || n == "48") {
215 216 // VT100 256 color or 24 bit RGB
216 217 if (numbers.length < 2) {
217 218 console.log("Not enough fields for VT100 color", numbers);
218 219 return;
219 220 }
220 221
221 222 var index_or_rgb = numbers.shift();
222 223 var r,g,b;
223 224 if (index_or_rgb == "5") {
224 225 // 256 color
225 226 var idx = parseInt(numbers.shift());
226 227 if (idx < 16) {
227 228 // indexed ANSI
228 229 // ignore bright / non-bright distinction
229 230 idx = idx % 8;
230 231 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
231 232 if ( ! attrs["class"] ) {
232 233 attrs["class"] = ansiclass;
233 234 } else {
234 235 attrs["class"] += " " + ansiclass;
235 236 }
236 237 return;
237 238 } else if (idx < 232) {
238 239 // 216 color 6x6x6 RGB
239 240 idx = idx - 16;
240 241 b = idx % 6;
241 242 g = Math.floor(idx / 6) % 6;
242 243 r = Math.floor(idx / 36) % 6;
243 244 // convert to rgb
244 245 r = (r * 51);
245 246 g = (g * 51);
246 247 b = (b * 51);
247 248 } else {
248 249 // grayscale
249 250 idx = idx - 231;
250 251 // it's 1-24 and should *not* include black or white,
251 252 // so a 26 point scale
252 253 r = g = b = Math.floor(idx * 256 / 26);
253 254 }
254 255 } else if (index_or_rgb == "2") {
255 256 // Simple 24 bit RGB
256 257 if (numbers.length > 3) {
257 258 console.log("Not enough fields for RGB", numbers);
258 259 return;
259 260 }
260 261 r = numbers.shift();
261 262 g = numbers.shift();
262 263 b = numbers.shift();
263 264 } else {
264 265 console.log("unrecognized control", numbers);
265 266 return;
266 267 }
267 268 if (r !== undefined) {
268 269 // apply the rgb color
269 270 var line;
270 271 if (n == "38") {
271 272 line = "color: ";
272 273 } else {
273 274 line = "background-color: ";
274 275 }
275 276 line = line + "rgb(" + r + "," + g + "," + b + ");";
276 277 if ( !attrs.style ) {
277 278 attrs.style = line;
278 279 } else {
279 280 attrs.style += " " + line;
280 281 }
281 282 }
282 283 }
283 284 }
284 285
285 286 function ansispan(str) {
286 287 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
287 288 // regular ansi escapes (using the table above)
288 289 var is_open = false;
289 290 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
290 291 if (!pattern) {
291 292 // [(01|22|39|)m close spans
292 293 if (is_open) {
293 294 is_open = false;
294 295 return "</span>";
295 296 } else {
296 297 return "";
297 298 }
298 299 } else {
299 300 is_open = true;
300 301
301 302 // consume sequence of color escapes
302 303 var numbers = pattern.match(/\d+/g);
303 304 var attrs = {};
304 305 while (numbers.length > 0) {
305 306 _process_numbers(attrs, numbers);
306 307 }
307 308
308 309 var span = "<span ";
309 310 for (var attr in attrs) {
310 311 var value = attrs[attr];
311 312 span = span + " " + attr + '="' + attrs[attr] + '"';
312 313 }
313 314 return span + ">";
314 315 }
315 316 });
316 317 }
317 318
318 319 // Transform ANSI color escape codes into HTML <span> tags with css
319 320 // classes listed in the above ansi_colormap object. The actual color used
320 321 // are set in the css file.
321 322 function fixConsole(txt) {
322 323 txt = xmlencode(txt);
323 324 var re = /\033\[([\dA-Fa-f;]*?)m/;
324 325 var opened = false;
325 326 var cmds = [];
326 327 var opener = "";
327 328 var closer = "";
328 329
329 330 // Strip all ANSI codes that are not color related. Matches
330 331 // all ANSI codes that do not end with "m".
331 332 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
332 333 txt = txt.replace(ignored_re, "");
333 334
334 335 // color ansi codes
335 336 txt = ansispan(txt);
336 337 return txt;
337 338 }
338 339
339 340 // Remove chunks that should be overridden by the effect of
340 341 // carriage return characters
341 342 function fixCarriageReturn(txt) {
342 343 var tmp = txt;
343 344 do {
344 345 txt = tmp;
345 346 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
346 347 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
347 348 } while (tmp.length < txt.length);
348 349 return txt;
349 350 }
350 351
351 352 // Locate any URLs and convert them to a anchor tag
352 353 function autoLinkUrls(txt) {
353 354 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
354 355 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
355 356 }
356 357
357 358 var points_to_pixels = function (points) {
358 359 // A reasonably good way of converting between points and pixels.
359 360 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
360 361 $(body).append(test);
361 362 var pixel_per_point = test.width()/10000;
362 363 test.remove();
363 364 return Math.floor(points*pixel_per_point);
364 365 };
365 366
366 367 var always_new = function (constructor) {
367 368 // wrapper around contructor to avoid requiring `var a = new constructor()`
368 369 // useful for passing constructors as callbacks,
369 370 // not for programmer laziness.
370 371 // from http://programmers.stackexchange.com/questions/118798
371 372 return function () {
372 373 var obj = Object.create(constructor.prototype);
373 374 constructor.apply(obj, arguments);
374 375 return obj;
375 376 };
376 377 };
377 378
378 379 var url_path_join = function () {
379 380 // join a sequence of url components with '/'
380 381 var url = '';
381 382 for (var i = 0; i < arguments.length; i++) {
382 383 if (arguments[i] === '') {
383 384 continue;
384 385 }
385 386 if (url.length > 0 && url[url.length-1] != '/') {
386 387 url = url + '/' + arguments[i];
387 388 } else {
388 389 url = url + arguments[i];
389 390 }
390 391 }
391 392 url = url.replace(/\/\/+/, '/');
392 393 return url;
393 394 };
394 395
395 396 var url_path_split = function (path) {
396 397 // Like os.path.split for URLs.
397 398 // Always returns two strings, the directory path and the base filename
398 399
399 400 var idx = path.lastIndexOf('/');
400 401 if (idx === -1) {
401 402 return ['', path];
402 403 } else {
403 404 return [ path.slice(0, idx), path.slice(idx + 1) ];
404 405 }
405 406 };
406 407
407 408 var parse_url = function (url) {
408 409 // an `a` element with an href allows attr-access to the parsed segments of a URL
409 410 // a = parse_url("http://localhost:8888/path/name#hash")
410 411 // a.protocol = "http:"
411 412 // a.host = "localhost:8888"
412 413 // a.hostname = "localhost"
413 414 // a.port = 8888
414 415 // a.pathname = "/path/name"
415 416 // a.hash = "#hash"
416 417 var a = document.createElement("a");
417 418 a.href = url;
418 419 return a;
419 420 };
420 421
421 422 var encode_uri_components = function (uri) {
422 423 // encode just the components of a multi-segment uri,
423 424 // leaving '/' separators
424 425 return uri.split('/').map(encodeURIComponent).join('/');
425 426 };
426 427
427 428 var url_join_encode = function () {
428 429 // join a sequence of url components with '/',
429 430 // encoding each component with encodeURIComponent
430 431 return encode_uri_components(url_path_join.apply(null, arguments));
431 432 };
432 433
433 434
434 435 var splitext = function (filename) {
435 436 // mimic Python os.path.splitext
436 437 // Returns ['base', '.ext']
437 438 var idx = filename.lastIndexOf('.');
438 439 if (idx > 0) {
439 440 return [filename.slice(0, idx), filename.slice(idx)];
440 441 } else {
441 442 return [filename, ''];
442 443 }
443 444 };
444 445
445 446
446 447 var escape_html = function (text) {
447 448 // escape text to HTML
448 449 return $("<div/>").text(text).html();
449 450 };
450 451
451 452
452 453 var get_body_data = function(key) {
453 454 // get a url-encoded item from body.data and decode it
454 455 // we should never have any encoded URLs anywhere else in code
455 456 // until we are building an actual request
456 457 return decodeURIComponent($('body').data(key));
457 458 };
458 459
459 460 var to_absolute_cursor_pos = function (cm, cursor) {
460 461 // get the absolute cursor position from CodeMirror's col, ch
461 462 if (!cursor) {
462 463 cursor = cm.getCursor();
463 464 }
464 465 var cursor_pos = cursor.ch;
465 466 for (var i = 0; i < cursor.line; i++) {
466 467 cursor_pos += cm.getLine(i).length + 1;
467 468 }
468 469 return cursor_pos;
469 470 };
470 471
471 472 var from_absolute_cursor_pos = function (cm, cursor_pos) {
472 473 // turn absolute cursor postion into CodeMirror col, ch cursor
473 474 var i, line;
474 475 var offset = 0;
475 476 for (i = 0, line=cm.getLine(i); line !== undefined; i++, line=cm.getLine(i)) {
476 477 if (offset + line.length < cursor_pos) {
477 478 offset += line.length + 1;
478 479 } else {
479 480 return {
480 481 line : i,
481 482 ch : cursor_pos - offset,
482 483 };
483 484 }
484 485 }
485 486 // reached end, return endpoint
486 487 return {
487 488 ch : line.length - 1,
488 489 line : i - 1,
489 490 };
490 491 };
491 492
492 493 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
493 494 var browser = (function() {
494 495 if (typeof navigator === 'undefined') {
495 496 // navigator undefined in node
496 497 return 'None';
497 498 }
498 499 var N= navigator.appName, ua= navigator.userAgent, tem;
499 500 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
500 501 if (M && (tem= ua.match(/version\/([\.\d]+)/i)) !== null) M[2]= tem[1];
501 502 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
502 503 return M;
503 504 })();
504 505
505 506 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
506 507 var platform = (function () {
507 508 if (typeof navigator === 'undefined') {
508 509 // navigator undefined in node
509 510 return 'None';
510 511 }
511 512 var OSName="None";
512 513 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
513 514 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
514 515 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
515 516 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
516 517 return OSName;
517 518 })();
518 519
519 520 var is_or_has = function (a, b) {
520 521 // Is b a child of a or a itself?
521 522 return a.has(b).length !==0 || a.is(b);
522 523 };
523 524
524 525 var is_focused = function (e) {
525 526 // Is element e, or one of its children focused?
526 527 e = $(e);
527 528 var target = $(document.activeElement);
528 529 if (target.length > 0) {
529 530 if (is_or_has(e, target)) {
530 531 return true;
531 532 } else {
532 533 return false;
533 534 }
534 535 } else {
535 536 return false;
536 537 }
537 538 };
538 539
539 540 var mergeopt = function(_class, options, overwrite){
540 541 options = options || {};
541 542 overwrite = overwrite || {};
542 543 return $.extend(true, {}, _class.options_default, options, overwrite);
543 544 };
544 545
545 546 var ajax_error_msg = function (jqXHR) {
546 547 // Return a JSON error message if there is one,
547 548 // otherwise the basic HTTP status text.
548 549 if (jqXHR.responseJSON && jqXHR.responseJSON.traceback) {
549 550 return jqXHR.responseJSON.traceback;
550 551 } else if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
551 552 return jqXHR.responseJSON.message;
552 553 } else {
553 554 return jqXHR.statusText;
554 555 }
555 556 };
556 557 var log_ajax_error = function (jqXHR, status, error) {
557 558 // log ajax failures with informative messages
558 559 var msg = "API request failed (" + jqXHR.status + "): ";
559 560 console.log(jqXHR);
560 561 msg += ajax_error_msg(jqXHR);
561 562 console.log(msg);
562 563 };
563 564
564 565 var requireCodeMirrorMode = function (mode, callback, errback) {
565 566 // load a mode with requirejs
566 567 if (typeof mode != "string") mode = mode.name;
567 568 if (CodeMirror.modes.hasOwnProperty(mode)) {
568 569 callback(CodeMirror.modes.mode);
569 570 return;
570 571 }
571 572 require([
572 573 // might want to use CodeMirror.modeURL here
573 574 ['codemirror/mode', mode, mode].join('/'),
574 575 ], callback, errback
575 576 );
576 577 };
577 578
578 579 /** Error type for wrapped XHR errors. */
579 580 var XHR_ERROR = 'XhrError';
580 581
581 582 /**
582 583 * Wraps an AJAX error as an Error object.
583 584 */
584 585 var wrap_ajax_error = function (jqXHR, status, error) {
585 586 var wrapped_error = new Error(ajax_error_msg(jqXHR));
586 587 wrapped_error.name = XHR_ERROR;
587 588 // provide xhr response
588 589 wrapped_error.xhr = jqXHR;
589 590 wrapped_error.xhr_status = status;
590 591 wrapped_error.xhr_error = error;
591 592 return wrapped_error;
592 593 };
593 594
594 595 var promising_ajax = function(url, settings) {
595 596 // Like $.ajax, but returning an ES6 promise. success and error settings
596 597 // will be ignored.
597 598 return new Promise(function(resolve, reject) {
598 599 settings.success = function(data, status, jqXHR) {
599 600 resolve(data);
600 601 };
601 602 settings.error = function(jqXHR, status, error) {
602 603 log_ajax_error(jqXHR, status, error);
603 604 reject(wrap_ajax_error(jqXHR, status, error));
604 605 };
605 606 $.ajax(url, settings);
606 607 });
607 608 };
608 609
610 var WrappedError = function(message, error){
611 // Wrappable Error class
612
613 // The Error class doesn't actually act on `this`. Instead it always
614 // returns a new instance of Error. Here we capture that instance so we
615 // can apply it's properties to `this`.
616 var tmp = Error.apply(this, [message]);
617
618 // Copy the properties of the error over to this.
619 var properties = Object.getOwnPropertyNames(tmp);
620 for (var i = 0; i < properties.length; i++) {
621 this[properties[i]] = tmp[properties[i]];
622 }
623
624 // Keep a stack of the original error messages.
625 if (error instanceof WrappedError) {
626 this.error_stack = error.error_stack;
627 } else {
628 this.error_stack = [error];
629 }
630 this.error_stack.push(tmp);
631
632 return this;
633 };
634
635 WrappedError.prototype = Object.create(Error.prototype, {});
636
637
609 638 var load_class = function(class_name, module_name, registry) {
610 639 // Tries to load a class
611 640 //
612 641 // Tries to load a class from a module using require.js, if a module
613 642 // is specified, otherwise tries to load a class from the global
614 643 // registry, if the global registry is provided.
615 return new Promise(function(resolve, reject) {
644 return new rsvp.Promise(function(resolve, reject) {
616 645
617 646 // Try loading the view module using require.js
618 647 if (module_name) {
619 648 require([module_name], function(module) {
620 649 if (module[class_name] === undefined) {
621 650 reject(new Error('Class '+class_name+' not found in module '+module_name));
622 651 } else {
623 652 resolve(module[class_name]);
624 653 }
625 654 }, reject);
626 655 } else {
627 656 if (registry && registry[class_name]) {
628 657 resolve(registry[class_name]);
629 658 } else {
630 659 reject(new Error({
631 660 message: 'Class '+class_name+' not found in registry ',
632 661 registry: registry
633 662 }));
634 663 }
635 664 }
636 665 });
637 666 };
638 667
639 668 var resolve_dict = function(d) {
640 669 // Resolve a promiseful dictionary.
641 // Returns a single Promise.
670 // Returns a single rsvp.Promise.
642 671 var keys = Object.keys(d);
643 672 var values = [];
644 673 keys.forEach(function(key) {
645 674 values.push(d[key]);
646 675 });
647 return Promise.all(values).then(function(v) {
676 return rsvp.Promise.all(values).then(function(v) {
648 677 d = {};
649 678 for(var i=0; i<keys.length; i++) {
650 679 d[keys[i]] = v[i];
651 680 }
652 681 return d;
653 682 });
654 683 };
655 684
656 685 var WrappedError = function(message, error){
657 686 // Wrappable Error class
658 687
659 688 // The Error class doesn't actually act on `this`. Instead it always
660 689 // returns a new instance of Error. Here we capture that instance so we
661 690 // can apply it's properties to `this`.
662 691 var tmp = Error.apply(this, [message]);
663 692
664 693 // Copy the properties of the error over to this.
665 694 var properties = Object.getOwnPropertyNames(tmp);
666 695 for (var i = 0; i < properties.length; i++) {
667 696 this[properties[i]] = tmp[properties[i]];
668 697 }
669 698
670 699 // Keep a stack of the original error messages.
671 700 if (error instanceof WrappedError) {
672 701 this.error_stack = error.error_stack;
673 702 } else {
674 703 this.error_stack = [error];
675 704 }
676 705 this.error_stack.push(tmp);
677 706
678 707 return this;
679 708 };
680 709
681 710 WrappedError.prototype = Object.create(Error.prototype, {});
682 711
683 712 var reject = function(message, log) {
684 // Creates a wrappable Promise rejection function.
713 // Creates a wrappable rsvp.Promise rejection function.
685 714 //
686 // Creates a function that returns a Promise.reject with a new WrappedError
715 // Creates a function that returns a rsvp.Promise.reject with a new WrappedError
687 716 // that has the provided message and wraps the original error that
688 717 // caused the promise to reject.
689 718 return function(error) {
690 719 var wrapped_error = new WrappedError(message, error);
691 720 if (log) console.error(wrapped_error);
692 return Promise.reject(wrapped_error);
721 return rsvp.Promise.reject(wrapped_error);
693 722 };
694 723 };
695 724
696 725 var utils = {
697 726 regex_split : regex_split,
698 727 uuid : uuid,
699 728 fixConsole : fixConsole,
700 729 fixCarriageReturn : fixCarriageReturn,
701 730 autoLinkUrls : autoLinkUrls,
702 731 points_to_pixels : points_to_pixels,
703 732 get_body_data : get_body_data,
704 733 parse_url : parse_url,
705 734 url_path_split : url_path_split,
706 735 url_path_join : url_path_join,
707 736 url_join_encode : url_join_encode,
708 737 encode_uri_components : encode_uri_components,
709 738 splitext : splitext,
710 739 escape_html : escape_html,
711 740 always_new : always_new,
712 741 to_absolute_cursor_pos : to_absolute_cursor_pos,
713 742 from_absolute_cursor_pos : from_absolute_cursor_pos,
714 743 browser : browser,
715 744 platform: platform,
716 745 is_or_has : is_or_has,
717 746 is_focused : is_focused,
718 747 mergeopt: mergeopt,
719 748 ajax_error_msg : ajax_error_msg,
720 749 log_ajax_error : log_ajax_error,
721 750 requireCodeMirrorMode : requireCodeMirrorMode,
722 751 XHR_ERROR : XHR_ERROR,
723 752 wrap_ajax_error : wrap_ajax_error,
724 753 promising_ajax : promising_ajax,
754 WrappedError: WrappedError,
725 755 load_class: load_class,
726 756 resolve_dict: resolve_dict,
727 WrappedError: WrappedError,
728 757 reject: reject,
729 758 };
730 759
731 760 // Backwards compatability.
732 761 IPython.utils = utils;
733 762
734 763 return utils;
735 764 });
@@ -1,192 +1,193 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 'base/js/utils',
8 ], function(IPython, $, utils) {
8 'components/rsvp/rsvp.min',
9 ], function(IPython, $, utils, rsvp) {
9 10 "use strict";
10 11
11 12 //-----------------------------------------------------------------------
12 13 // CommManager class
13 14 //-----------------------------------------------------------------------
14 15
15 16 var CommManager = function (kernel) {
16 17 this.comms = {};
17 18 this.targets = {};
18 19 if (kernel !== undefined) {
19 20 this.init_kernel(kernel);
20 21 }
21 22 };
22 23
23 24 CommManager.prototype.init_kernel = function (kernel) {
24 25 // connect the kernel, and register message handlers
25 26 this.kernel = kernel;
26 27 var msg_types = ['comm_open', 'comm_msg', 'comm_close'];
27 28 for (var i = 0; i < msg_types.length; i++) {
28 29 var msg_type = msg_types[i];
29 30 kernel.register_iopub_handler(msg_type, $.proxy(this[msg_type], this));
30 31 }
31 32 };
32 33
33 34 CommManager.prototype.new_comm = function (target_name, data, callbacks, metadata) {
34 35 // Create a new Comm, register it, and open its Kernel-side counterpart
35 36 // Mimics the auto-registration in `Comm.__init__` in the IPython Comm
36 37 var comm = new Comm(target_name);
37 38 this.register_comm(comm);
38 39 comm.open(data, callbacks, metadata);
39 40 return comm;
40 41 };
41 42
42 43 CommManager.prototype.register_target = function (target_name, f) {
43 44 // Register a target function for a given target name
44 45 this.targets[target_name] = f;
45 46 };
46 47
47 48 CommManager.prototype.unregister_target = function (target_name, f) {
48 49 // Unregister a target function for a given target name
49 50 delete this.targets[target_name];
50 51 };
51 52
52 53 CommManager.prototype.register_comm = function (comm) {
53 54 // Register a comm in the mapping
54 55 this.comms[comm.comm_id] = comm;
55 56 comm.kernel = this.kernel;
56 57 return comm.comm_id;
57 58 };
58 59
59 60 CommManager.prototype.unregister_comm = function (comm) {
60 61 // Remove a comm from the mapping
61 62 delete this.comms[comm.comm_id];
62 63 };
63 64
64 65 // comm message handlers
65 66
66 67 CommManager.prototype.comm_open = function (msg) {
67 68 var content = msg.content;
68 69 var that = this;
69 70
70 71 return utils.load_class(content.target_name, content.target_module,
71 72 this.targets).then(function(target) {
72 73
73 74 var comm = new Comm(content.target_name, content.comm_id);
74 75 that.register_comm(comm);
75 76 try {
76 77 target(comm, msg);
77 78 } catch (e) {
78 79 comm.close();
79 80 that.unregister_comm(comm);
80 81 var error = new utils.WrappedError("Exception opening new comm", e);
81 return Promise.reject(error);
82 return rsvp.Promise.reject(error);
82 83 }
83 84 return comm;
84 85 }, utils.reject('Could not open comm', true));
85 86 };
86 87
87 88 CommManager.prototype.comm_close = function (msg) {
88 89 var content = msg.content;
89 90 var comm = this.comms[content.comm_id];
90 91 if (comm === undefined) {
91 92 return;
92 93 }
93 94 this.unregister_comm(comm);
94 95 try {
95 96 comm.handle_close(msg);
96 97 } catch (e) {
97 98 console.log("Exception closing comm: ", e, e.stack, msg);
98 99 }
99 100 };
100 101
101 102 CommManager.prototype.comm_msg = function (msg) {
102 103 var content = msg.content;
103 104 var comm = this.comms[content.comm_id];
104 105 if (comm === undefined) {
105 106 return;
106 107 }
107 108 try {
108 109 comm.handle_msg(msg);
109 110 } catch (e) {
110 111 console.log("Exception handling comm msg: ", e, e.stack, msg);
111 112 }
112 113 };
113 114
114 115 //-----------------------------------------------------------------------
115 116 // Comm base class
116 117 //-----------------------------------------------------------------------
117 118
118 119 var Comm = function (target_name, comm_id) {
119 120 this.target_name = target_name;
120 121 this.comm_id = comm_id || utils.uuid();
121 122 this._msg_callback = this._close_callback = null;
122 123 };
123 124
124 125 // methods for sending messages
125 126 Comm.prototype.open = function (data, callbacks, metadata) {
126 127 var content = {
127 128 comm_id : this.comm_id,
128 129 target_name : this.target_name,
129 130 data : data || {},
130 131 };
131 132 return this.kernel.send_shell_message("comm_open", content, callbacks, metadata);
132 133 };
133 134
134 135 Comm.prototype.send = function (data, callbacks, metadata, buffers) {
135 136 var content = {
136 137 comm_id : this.comm_id,
137 138 data : data || {},
138 139 };
139 140 return this.kernel.send_shell_message("comm_msg", content, callbacks, metadata, buffers);
140 141 };
141 142
142 143 Comm.prototype.close = function (data, callbacks, metadata) {
143 144 var content = {
144 145 comm_id : this.comm_id,
145 146 data : data || {},
146 147 };
147 148 return this.kernel.send_shell_message("comm_close", content, callbacks, metadata);
148 149 };
149 150
150 151 // methods for registering callbacks for incoming messages
151 152 Comm.prototype._register_callback = function (key, callback) {
152 153 this['_' + key + '_callback'] = callback;
153 154 };
154 155
155 156 Comm.prototype.on_msg = function (callback) {
156 157 this._register_callback('msg', callback);
157 158 };
158 159
159 160 Comm.prototype.on_close = function (callback) {
160 161 this._register_callback('close', callback);
161 162 };
162 163
163 164 // methods for handling incoming messages
164 165
165 166 Comm.prototype._maybe_callback = function (key, msg) {
166 167 var callback = this['_' + key + '_callback'];
167 168 if (callback) {
168 169 try {
169 170 callback(msg);
170 171 } catch (e) {
171 172 console.log("Exception in Comm callback", e, e.stack, msg);
172 173 }
173 174 }
174 175 };
175 176
176 177 Comm.prototype.handle_msg = function (msg) {
177 178 this._maybe_callback('msg', msg);
178 179 };
179 180
180 181 Comm.prototype.handle_close = function (msg) {
181 182 this._maybe_callback('close', msg);
182 183 };
183 184
184 185 // For backwards compatability.
185 186 IPython.CommManager = CommManager;
186 187 IPython.Comm = Comm;
187 188
188 189 return {
189 190 'CommManager': CommManager,
190 191 'Comm': Comm
191 192 };
192 193 });
@@ -1,243 +1,244 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 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/utils",
9 "base/js/namespace"
10 ], function (_, Backbone, $, utils, IPython) {
9 "base/js/namespace",
10 'components/rsvp/rsvp.min',
11 ], function (_, Backbone, $, utils, IPython, rsvp) {
11 12 "use strict";
12 13 //--------------------------------------------------------------------
13 14 // WidgetManager class
14 15 //--------------------------------------------------------------------
15 16 var WidgetManager = function (comm_manager, notebook) {
16 17 // Public constructor
17 18 WidgetManager._managers.push(this);
18 19
19 20 // Attach a comm manager to the
20 21 this.keyboard_manager = notebook.keyboard_manager;
21 22 this.notebook = notebook;
22 23 this.comm_manager = comm_manager;
23 24 this._models = {}; /* Dictionary of model ids and model instances */
24 25
25 26 // Register with the comm manager.
26 27 this.comm_manager.register_target('ipython.widget', $.proxy(this._handle_comm_open, this));
27 28 };
28 29
29 30 //--------------------------------------------------------------------
30 31 // Class level
31 32 //--------------------------------------------------------------------
32 33 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
33 34 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
34 35 WidgetManager._managers = []; /* List of widget managers */
35 36
36 37 WidgetManager.register_widget_model = function (model_name, model_type) {
37 38 // Registers a widget model by name.
38 39 WidgetManager._model_types[model_name] = model_type;
39 40 };
40 41
41 42 WidgetManager.register_widget_view = function (view_name, view_type) {
42 43 // Registers a widget view by name.
43 44 WidgetManager._view_types[view_name] = view_type;
44 45 };
45 46
46 47 //--------------------------------------------------------------------
47 48 // Instance level
48 49 //--------------------------------------------------------------------
49 50 WidgetManager.prototype.display_view = function(msg, model) {
50 51 // Displays a view for a particular model.
51 return new Promise(function(resolve, reject) {
52 return new rsvp.Promise(function(resolve, reject) {
52 53 var cell = this.get_msg_cell(msg.parent_header.msg_id);
53 54 if (cell === null) {
54 55 reject(new Error("Could not determine where the display" +
55 56 " message was from. Widget will not be displayed"));
56 57 } else {
57 58 var dummy = null;
58 59 if (cell.widget_subarea) {
59 60 dummy = $('<div />');
60 61 cell.widget_subarea.append(dummy);
61 62 }
62 63
63 64 var that = this;
64 65 this.create_view(model, {cell: cell}).then(function(view) {
65 66 that._handle_display_view(view);
66 67 if (dummy) {
67 68 dummy.replaceWith(view.$el);
68 69 }
69 70 view.trigger('displayed');
70 71 resolve(view);
71 72 }, function(error) {
72 73 reject(new utils.WrappedError('Could not display view', error));
73 74 });
74 75 }
75 76 });
76 77
77 78
78 79 };
79 80
80 81 WidgetManager.prototype._handle_display_view = function (view) {
81 82 // Have the IPython keyboard manager disable its event
82 83 // handling so the widget can capture keyboard input.
83 84 // Note, this is only done on the outer most widgets.
84 85 if (this.keyboard_manager) {
85 86 this.keyboard_manager.register_events(view.$el);
86 87
87 88 if (view.additional_elements) {
88 89 for (var i = 0; i < view.additional_elements.length; i++) {
89 90 this.keyboard_manager.register_events(view.additional_elements[i]);
90 91 }
91 92 }
92 93 }
93 94 };
94 95
95 96 WidgetManager.prototype.create_view = function(model, options) {
96 97 // Creates a promise for a view of a given model
97 98 return utils.load_class(model.get('_view_name'), model.get('_view_module'),
98 99 WidgetManager._view_types).then(function(ViewType) {
99 100
100 101 // If a view is passed into the method, use that view's cell as
101 102 // the cell for the view that is created.
102 103 options = options || {};
103 104 if (options.parent !== undefined) {
104 105 options.cell = options.parent.options.cell;
105 106 }
106 107 // Create and render the view...
107 108 var parameters = {model: model, options: options};
108 109 var view = new ViewType(parameters);
109 110 view.listenTo(model, 'destroy', view.remove);
110 111 view.render();
111 112 return view;
112 113 }, utils.reject("Couldn't create a view for model id '" + String(model.id) + "'"));
113 114 };
114 115
115 116 WidgetManager.prototype.get_msg_cell = function (msg_id) {
116 117 var cell = null;
117 118 // First, check to see if the msg was triggered by cell execution.
118 119 if (this.notebook) {
119 120 cell = this.notebook.get_msg_cell(msg_id);
120 121 }
121 122 if (cell !== null) {
122 123 return cell;
123 124 }
124 125 // Second, check to see if a get_cell callback was defined
125 126 // for the message. get_cell callbacks are registered for
126 127 // widget messages, so this block is actually checking to see if the
127 128 // message was triggered by a widget.
128 129 var kernel = this.comm_manager.kernel;
129 130 if (kernel) {
130 131 var callbacks = kernel.get_callbacks_for_msg(msg_id);
131 132 if (callbacks && callbacks.iopub &&
132 133 callbacks.iopub.get_cell !== undefined) {
133 134 return callbacks.iopub.get_cell();
134 135 }
135 136 }
136 137
137 138 // Not triggered by a cell or widget (no get_cell callback
138 139 // exists).
139 140 return null;
140 141 };
141 142
142 143 WidgetManager.prototype.callbacks = function (view) {
143 144 // callback handlers specific a view
144 145 var callbacks = {};
145 146 if (view && view.options.cell) {
146 147
147 148 // Try to get output handlers
148 149 var cell = view.options.cell;
149 150 var handle_output = null;
150 151 var handle_clear_output = null;
151 152 if (cell.output_area) {
152 153 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
153 154 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
154 155 }
155 156
156 157 // Create callback dictionary using what is known
157 158 var that = this;
158 159 callbacks = {
159 160 iopub : {
160 161 output : handle_output,
161 162 clear_output : handle_clear_output,
162 163
163 164 // Special function only registered by widget messages.
164 165 // Allows us to get the cell for a message so we know
165 166 // where to add widgets if the code requires it.
166 167 get_cell : function () {
167 168 return cell;
168 169 },
169 170 },
170 171 };
171 172 }
172 173 return callbacks;
173 174 };
174 175
175 176 WidgetManager.prototype.get_model = function (model_id) {
176 177 return that._models[model_id];
177 178 };
178 179
179 180 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
180 181 // Handle when a comm is opened.
181 182 this.create_model({
182 183 model_name: msg.content.data.model_name,
183 184 model_module: msg.content.data.model_module,
184 185 comm: comm}).handle($.proxy(console.error, error));
185 186 };
186 187
187 188 WidgetManager.prototype.create_model = function (options) {
188 189 // Create and return a promise for a new widget model
189 190 //
190 191 // Minimally, one must provide the model_name and widget_class
191 192 // parameters to create a model from Javascript.
192 193 //
193 194 // Example
194 195 // --------
195 196 // JS:
196 197 // IPython.notebook.kernel.widget_manager.create_model({
197 198 // model_name: 'WidgetModel',
198 199 // widget_class: 'IPython.html.widgets.widget_int.IntSlider'})
199 200 // .then(function(model) { console.log('Create success!', model); },
200 201 // $.proxy(console.error, console));
201 202 //
202 203 // Parameters
203 204 // ----------
204 205 // options: dictionary
205 206 // Dictionary of options with the following contents:
206 207 // model_name: string
207 208 // Target name of the widget model to create.
208 209 // model_module: (optional) string
209 210 // Module name of the widget model to create.
210 211 // widget_class: (optional) string
211 212 // Target name of the widget in the back-end.
212 213 // comm: (optional) Comm
213 214
214 215 // Create a comm if it wasn't provided.
215 216 var comm = options.comm;
216 217 if (!comm) {
217 218 comm = this.comm_manager.new_comm('ipython.widget', {'widget_class': options.widget_class});
218 219 }
219 220
220 221 var that = this;
221 222 var model_id = comm.comm_id;
222 223 var model_promise = utils.load_class(options.model_name, options.model_module, WidgetManager._model_types)
223 224 .then(function(ModelType) {
224 225 var widget_model = new ModelType(that, model_id, comm);
225 226 widget_model.once('comm:close', function () {
226 227 delete that._models[model_id];
227 228 });
228 229 return widget_model;
229 230
230 231 }, function(error) {
231 232 delete that._models[model_id];
232 233 var wrapped_error = new utils.WrappedError("Couldn't create model", error);
233 return Promise.reject(wrapped_error);
234 return rsvp.Promise.reject(wrapped_error);
234 235 });
235 236 this._models[model_id] = model_promise;
236 237 return model_promise;
237 238 };
238 239
239 240 // Backwards compatibility.
240 241 IPython.WidgetManager = WidgetManager;
241 242
242 243 return {'WidgetManager': WidgetManager};
243 244 });
@@ -1,604 +1,605 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 ], function(widgetmanager, _, Backbone, $, utils, IPython){
10 "components/rsvp/rsvp.min",
11 ], function(widgetmanager, _, Backbone, $, utils, IPython, rsvp){
11 12
12 13 var WidgetModel = Backbone.Model.extend({
13 14 constructor: function (widget_manager, model_id, comm) {
14 15 // Constructor
15 16 //
16 17 // Creates a WidgetModel instance.
17 18 //
18 19 // Parameters
19 20 // ----------
20 21 // widget_manager : WidgetManager instance
21 22 // model_id : string
22 23 // An ID unique to this model.
23 24 // comm : Comm instance (optional)
24 25 this.widget_manager = widget_manager;
25 this.state_change = Promise.resolve();
26 this.state_change = rsvp.Promise.resolve();
26 27 this._buffered_state_diff = {};
27 28 this.pending_msgs = 0;
28 29 this.msg_buffer = null;
29 30 this.state_lock = null;
30 31 this.id = model_id;
31 32 this.views = {};
32 33
33 34 if (comm !== undefined) {
34 35 // Remember comm associated with the model.
35 36 this.comm = comm;
36 37 comm.model = this;
37 38
38 39 // Hook comm messages up to model.
39 40 comm.on_close($.proxy(this._handle_comm_closed, this));
40 41 comm.on_msg($.proxy(this._handle_comm_msg, this));
41 42 }
42 43 return Backbone.Model.apply(this);
43 44 },
44 45
45 46 send: function (content, callbacks) {
46 47 // Send a custom msg over the comm.
47 48 if (this.comm !== undefined) {
48 49 var data = {method: 'custom', content: content};
49 50 this.comm.send(data, callbacks);
50 51 this.pending_msgs++;
51 52 }
52 53 },
53 54
54 55 _handle_comm_closed: function (msg) {
55 56 // Handle when a widget is closed.
56 57 this.trigger('comm:close');
57 58 this.stopListening();
58 59 this.trigger('destroy', this);
59 60 delete this.comm.model; // Delete ref so GC will collect widget model.
60 61 delete this.comm;
61 62 delete this.model_id; // Delete id from model so widget manager cleans up.
62 63 for (var id in this.views) {
63 64 if (this.views.hasOwnProperty(id)) {
64 65 this.views[id].remove();
65 66 }
66 67 }
67 68 },
68 69
69 70 _handle_comm_msg: function (msg) {
70 71 // Handle incoming comm msg.
71 72 var method = msg.content.data.method;
72 73 var that = this;
73 74 switch (method) {
74 75 case 'update':
75 76 this.state_change = this.state_change.then(function() {
76 77 return that.set_state(msg.content.data.state);
77 78 });
78 79 break;
79 80 case 'custom':
80 81 this.trigger('msg:custom', msg.content.data.content);
81 82 break;
82 83 case 'display':
83 84 this.state_change = this.state_change.then(function () {
84 85 that.widget_manager.display_view(msg, that);
85 86 });
86 87 break;
87 88 }
88 89 },
89 90
90 91 set_state: function (state) {
91 92 var that = this;
92 93 // Handle when a widget is updated via the python side.
93 94 return this._unpack_models(state).then(function(state) {
94 95 that.state_lock = state;
95 96 try {
96 97 WidgetModel.__super__.set.call(that, state);
97 98 } finally {
98 99 that.state_lock = null;
99 100 }
100 101 }, utils.reject("Couldn't set model state", true));
101 102 },
102 103
103 104 _handle_status: function (msg, callbacks) {
104 105 // Handle status msgs.
105 106
106 107 // execution_state : ('busy', 'idle', 'starting')
107 108 if (this.comm !== undefined) {
108 109 if (msg.content.execution_state ==='idle') {
109 110 // Send buffer if this message caused another message to be
110 111 // throttled.
111 112 if (this.msg_buffer !== null &&
112 113 (this.get('msg_throttle') || 3) === this.pending_msgs) {
113 114 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
114 115 this.comm.send(data, callbacks);
115 116 this.msg_buffer = null;
116 117 } else {
117 118 --this.pending_msgs;
118 119 }
119 120 }
120 121 }
121 122 },
122 123
123 124 callbacks: function(view) {
124 125 // Create msg callbacks for a comm msg.
125 126 var callbacks = this.widget_manager.callbacks(view);
126 127
127 128 if (callbacks.iopub === undefined) {
128 129 callbacks.iopub = {};
129 130 }
130 131
131 132 var that = this;
132 133 callbacks.iopub.status = function (msg) {
133 134 that._handle_status(msg, callbacks);
134 135 };
135 136 return callbacks;
136 137 },
137 138
138 139 set: function(key, val, options) {
139 140 // Set a value.
140 141 var return_value = WidgetModel.__super__.set.apply(this, arguments);
141 142
142 143 // Backbone only remembers the diff of the most recent set()
143 144 // operation. Calling set multiple times in a row results in a
144 145 // loss of diff information. Here we keep our own running diff.
145 146 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
146 147 return return_value;
147 148 },
148 149
149 150 sync: function (method, model, options) {
150 151 // Handle sync to the back-end. Called when a model.save() is called.
151 152
152 153 // Make sure a comm exists.
153 154 var error = options.error || function() {
154 155 console.error('Backbone sync error:', arguments);
155 156 };
156 157 if (this.comm === undefined) {
157 158 error();
158 159 return false;
159 160 }
160 161
161 162 // Delete any key value pairs that the back-end already knows about.
162 163 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
163 164 if (this.state_lock !== null) {
164 165 var keys = Object.keys(this.state_lock);
165 166 for (var i=0; i<keys.length; i++) {
166 167 var key = keys[i];
167 168 if (attrs[key] === this.state_lock[key]) {
168 169 delete attrs[key];
169 170 }
170 171 }
171 172 }
172 173
173 174 // Only sync if there are attributes to send to the back-end.
174 175 attrs = this._pack_models(attrs);
175 176 if (_.size(attrs) > 0) {
176 177
177 178 // If this message was sent via backbone itself, it will not
178 179 // have any callbacks. It's important that we create callbacks
179 180 // so we can listen for status messages, etc...
180 181 var callbacks = options.callbacks || this.callbacks();
181 182
182 183 // Check throttle.
183 184 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
184 185 // The throttle has been exceeded, buffer the current msg so
185 186 // it can be sent once the kernel has finished processing
186 187 // some of the existing messages.
187 188
188 189 // Combine updates if it is a 'patch' sync, otherwise replace updates
189 190 switch (method) {
190 191 case 'patch':
191 192 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
192 193 break;
193 194 case 'update':
194 195 case 'create':
195 196 this.msg_buffer = attrs;
196 197 break;
197 198 default:
198 199 error();
199 200 return false;
200 201 }
201 202 this.msg_buffer_callbacks = callbacks;
202 203
203 204 } else {
204 205 // We haven't exceeded the throttle, send the message like
205 206 // normal.
206 207 var data = {method: 'backbone', sync_data: attrs};
207 208 this.comm.send(data, callbacks);
208 209 this.pending_msgs++;
209 210 }
210 211 }
211 212 // Since the comm is a one-way communication, assume the message
212 213 // arrived. Don't call success since we don't have a model back from the server
213 214 // this means we miss out on the 'sync' event.
214 215 this._buffered_state_diff = {};
215 216 },
216 217
217 218 save_changes: function(callbacks) {
218 219 // Push this model's state to the back-end
219 220 //
220 221 // This invokes a Backbone.Sync.
221 222 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
222 223 },
223 224
224 225 _pack_models: function(value) {
225 226 // Replace models with model ids recursively.
226 227 var that = this;
227 228 var packed;
228 229 if (value instanceof Backbone.Model) {
229 230 return "IPY_MODEL_" + value.id;
230 231
231 232 } else if ($.isArray(value)) {
232 233 packed = [];
233 234 _.each(value, function(sub_value, key) {
234 235 packed.push(that._pack_models(sub_value));
235 236 });
236 237 return packed;
237 238 } else if (value instanceof Date || value instanceof String) {
238 239 return value;
239 240 } else if (value instanceof Object) {
240 241 packed = {};
241 242 _.each(value, function(sub_value, key) {
242 243 packed[key] = that._pack_models(sub_value);
243 244 });
244 245 return packed;
245 246
246 247 } else {
247 248 return value;
248 249 }
249 250 },
250 251
251 252 _unpack_models: function(value) {
252 253 // Replace model ids with models recursively.
253 254 var that = this;
254 255 var unpacked;
255 256 if ($.isArray(value)) {
256 257 unpacked = [];
257 258 _.each(value, function(sub_value, key) {
258 259 unpacked.push(that._unpack_models(sub_value));
259 260 });
260 return Promise.all(unpacked);
261 return rsvp.Promise.all(unpacked);
261 262 } else if (value instanceof Object) {
262 263 unpacked = {};
263 264 _.each(value, function(sub_value, key) {
264 265 unpacked[key] = that._unpack_models(sub_value);
265 266 });
266 267 return utils.resolve_dict(unpacked);
267 268 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
268 269 // get_model returns a promise already
269 270 return this.widget_manager.get_model(value.slice(10, value.length));
270 271 } else {
271 return Promise.resolve(value);
272 return rsvp.Promise.resolve(value);
272 273 }
273 274 },
274 275
275 276 on_some_change: function(keys, callback, context) {
276 277 // on_some_change(["key1", "key2"], foo, context) differs from
277 278 // on("change:key1 change:key2", foo, context).
278 279 // If the widget attributes key1 and key2 are both modified,
279 280 // the second form will result in foo being called twice
280 281 // while the first will call foo only once.
281 282 this.on('change', function() {
282 283 if (keys.some(this.hasChanged, this)) {
283 284 callback.apply(context);
284 285 }
285 286 }, this);
286 287
287 288 },
288 289 });
289 290 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
290 291
291 292
292 293 var WidgetView = Backbone.View.extend({
293 294 initialize: function(parameters) {
294 295 // Public constructor.
295 296 this.model.on('change',this.update,this);
296 297 this.options = parameters.options;
297 298 this.child_model_views = {};
298 299 this.child_views = {};
299 300 this.id = this.id || utils.uuid();
300 301 this.model.views[this.id] = this;
301 302 this.on('displayed', function() {
302 303 this.is_displayed = true;
303 304 }, this);
304 305 },
305 306
306 307 update: function(){
307 308 // Triggered on model change.
308 309 //
309 310 // Update view to be consistent with this.model
310 311 },
311 312
312 313 create_child_view: function(child_model, options) {
313 314 // Create and promise that resolves to a child view of a given model
314 315 var that = this;
315 316 options = $.extend({ parent: this }, options || {});
316 317 return this.model.widget_manager.create_view(child_model, options).then(function(child_view) {
317 318 // Associate the view id with the model id.
318 319 if (that.child_model_views[child_model.id] === undefined) {
319 320 that.child_model_views[child_model.id] = [];
320 321 }
321 322 that.child_model_views[child_model.id].push(child_view.id);
322 323 // Remember the view by id.
323 324 that.child_views[child_view.id] = child_view;
324 325 return child_view;
325 326 }, utils.reject("Couldn't create child view"));
326 327 },
327 328
328 329 pop_child_view: function(child_model) {
329 330 // Delete a child view that was previously created using create_child_view.
330 331 var view_ids = this.child_model_views[child_model.id];
331 332 if (view_ids !== undefined) {
332 333
333 334 // Only delete the first view in the list.
334 335 var view_id = view_ids[0];
335 336 var view = this.child_views[view_id];
336 337 delete this.child_views[view_id];
337 338 view_ids.splice(0,1);
338 339 delete child_model.views[view_id];
339 340
340 341 // Remove the view list specific to this model if it is empty.
341 342 if (view_ids.length === 0) {
342 343 delete this.child_model_views[child_model.id];
343 344 }
344 345 return view;
345 346 }
346 347 return null;
347 348 },
348 349
349 350 do_diff: function(old_list, new_list, removed_callback, added_callback) {
350 351 // Difference a changed list and call remove and add callbacks for
351 352 // each removed and added item in the new list.
352 353 //
353 354 // Parameters
354 355 // ----------
355 356 // old_list : array
356 357 // new_list : array
357 358 // removed_callback : Callback(item)
358 359 // Callback that is called for each item removed.
359 360 // added_callback : Callback(item)
360 361 // Callback that is called for each item added.
361 362
362 363 // Walk the lists until an unequal entry is found.
363 364 var i;
364 365 for (i = 0; i < new_list.length; i++) {
365 366 if (i >= old_list.length || new_list[i] !== old_list[i]) {
366 367 break;
367 368 }
368 369 }
369 370
370 371 // Remove the non-matching items from the old list.
371 372 for (var j = i; j < old_list.length; j++) {
372 373 removed_callback(old_list[j]);
373 374 }
374 375
375 376 // Add the rest of the new list items.
376 377 for (; i < new_list.length; i++) {
377 378 added_callback(new_list[i]);
378 379 }
379 380 },
380 381
381 382 callbacks: function(){
382 383 // Create msg callbacks for a comm msg.
383 384 return this.model.callbacks(this);
384 385 },
385 386
386 387 render: function(){
387 388 // Render the view.
388 389 //
389 390 // By default, this is only called the first time the view is created
390 391 },
391 392
392 393 show: function(){
393 394 // Show the widget-area
394 395 if (this.options && this.options.cell &&
395 396 this.options.cell.widget_area !== undefined) {
396 397 this.options.cell.widget_area.show();
397 398 }
398 399 },
399 400
400 401 send: function (content) {
401 402 // Send a custom msg associated with this view.
402 403 this.model.send(content, this.callbacks());
403 404 },
404 405
405 406 touch: function () {
406 407 this.model.save_changes(this.callbacks());
407 408 },
408 409
409 410 after_displayed: function (callback, context) {
410 411 // Calls the callback right away is the view is already displayed
411 412 // otherwise, register the callback to the 'displayed' event.
412 413 if (this.is_displayed) {
413 414 callback.apply(context);
414 415 } else {
415 416 this.on('displayed', callback, context);
416 417 }
417 418 },
418 419 });
419 420
420 421
421 422 var DOMWidgetView = WidgetView.extend({
422 423 initialize: function (parameters) {
423 424 // Public constructor
424 425 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
425 426 this.on('displayed', this.show, this);
426 427 this.model.on('change:visible', this.update_visible, this);
427 428 this.model.on('change:_css', this.update_css, this);
428 429
429 430 this.model.on('change:_dom_classes', function(model, new_classes) {
430 431 var old_classes = model.previous('_dom_classes');
431 432 this.update_classes(old_classes, new_classes);
432 433 }, this);
433 434
434 435 this.model.on('change:color', function (model, value) {
435 436 this.update_attr('color', value); }, this);
436 437
437 438 this.model.on('change:background_color', function (model, value) {
438 439 this.update_attr('background', value); }, this);
439 440
440 441 this.model.on('change:width', function (model, value) {
441 442 this.update_attr('width', value); }, this);
442 443
443 444 this.model.on('change:height', function (model, value) {
444 445 this.update_attr('height', value); }, this);
445 446
446 447 this.model.on('change:border_color', function (model, value) {
447 448 this.update_attr('border-color', value); }, this);
448 449
449 450 this.model.on('change:border_width', function (model, value) {
450 451 this.update_attr('border-width', value); }, this);
451 452
452 453 this.model.on('change:border_style', function (model, value) {
453 454 this.update_attr('border-style', value); }, this);
454 455
455 456 this.model.on('change:font_style', function (model, value) {
456 457 this.update_attr('font-style', value); }, this);
457 458
458 459 this.model.on('change:font_weight', function (model, value) {
459 460 this.update_attr('font-weight', value); }, this);
460 461
461 462 this.model.on('change:font_size', function (model, value) {
462 463 this.update_attr('font-size', this._default_px(value)); }, this);
463 464
464 465 this.model.on('change:font_family', function (model, value) {
465 466 this.update_attr('font-family', value); }, this);
466 467
467 468 this.model.on('change:padding', function (model, value) {
468 469 this.update_attr('padding', value); }, this);
469 470
470 471 this.model.on('change:margin', function (model, value) {
471 472 this.update_attr('margin', this._default_px(value)); }, this);
472 473
473 474 this.model.on('change:border_radius', function (model, value) {
474 475 this.update_attr('border-radius', this._default_px(value)); }, this);
475 476
476 477 this.after_displayed(function() {
477 478 this.update_visible(this.model, this.model.get("visible"));
478 479 this.update_classes([], this.model.get('_dom_classes'));
479 480
480 481 this.update_attr('color', this.model.get('color'));
481 482 this.update_attr('background', this.model.get('background_color'));
482 483 this.update_attr('width', this.model.get('width'));
483 484 this.update_attr('height', this.model.get('height'));
484 485 this.update_attr('border-color', this.model.get('border_color'));
485 486 this.update_attr('border-width', this.model.get('border_width'));
486 487 this.update_attr('border-style', this.model.get('border_style'));
487 488 this.update_attr('font-style', this.model.get('font_style'));
488 489 this.update_attr('font-weight', this.model.get('font_weight'));
489 490 this.update_attr('font-size', this.model.get('font_size'));
490 491 this.update_attr('font-family', this.model.get('font_family'));
491 492 this.update_attr('padding', this.model.get('padding'));
492 493 this.update_attr('margin', this.model.get('margin'));
493 494 this.update_attr('border-radius', this.model.get('border_radius'));
494 495
495 496 this.update_css(this.model, this.model.get("_css"));
496 497 }, this);
497 498 },
498 499
499 500 _default_px: function(value) {
500 501 // Makes browser interpret a numerical string as a pixel value.
501 502 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
502 503 return value.trim() + 'px';
503 504 }
504 505 return value;
505 506 },
506 507
507 508 update_attr: function(name, value) {
508 509 // Set a css attr of the widget view.
509 510 this.$el.css(name, value);
510 511 },
511 512
512 513 update_visible: function(model, value) {
513 514 // Update visibility
514 515 this.$el.toggle(value);
515 516 },
516 517
517 518 update_css: function (model, css) {
518 519 // Update the css styling of this view.
519 520 var e = this.$el;
520 521 if (css === undefined) {return;}
521 522 for (var i = 0; i < css.length; i++) {
522 523 // Apply the css traits to all elements that match the selector.
523 524 var selector = css[i][0];
524 525 var elements = this._get_selector_element(selector);
525 526 if (elements.length > 0) {
526 527 var trait_key = css[i][1];
527 528 var trait_value = css[i][2];
528 529 elements.css(trait_key ,trait_value);
529 530 }
530 531 }
531 532 },
532 533
533 534 update_classes: function (old_classes, new_classes, $el) {
534 535 // Update the DOM classes applied to an element, default to this.$el.
535 536 if ($el===undefined) {
536 537 $el = this.$el;
537 538 }
538 539 this.do_diff(old_classes, new_classes, function(removed) {
539 540 $el.removeClass(removed);
540 541 }, function(added) {
541 542 $el.addClass(added);
542 543 });
543 544 },
544 545
545 546 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
546 547 // Update the DOM classes applied to the widget based on a single
547 548 // trait's value.
548 549 //
549 550 // Given a trait value classes map, this function automatically
550 551 // handles applying the appropriate classes to the widget element
551 552 // and removing classes that are no longer valid.
552 553 //
553 554 // Parameters
554 555 // ----------
555 556 // class_map: dictionary
556 557 // Dictionary of trait values to class lists.
557 558 // Example:
558 559 // {
559 560 // success: ['alert', 'alert-success'],
560 561 // info: ['alert', 'alert-info'],
561 562 // warning: ['alert', 'alert-warning'],
562 563 // danger: ['alert', 'alert-danger']
563 564 // };
564 565 // trait_name: string
565 566 // Name of the trait to check the value of.
566 567 // previous_trait_value: optional string, default ''
567 568 // Last trait value
568 569 // $el: optional jQuery element handle, defaults to this.$el
569 570 // Element that the classes are applied to.
570 571 var key = previous_trait_value;
571 572 if (key === undefined) {
572 573 key = this.model.previous(trait_name);
573 574 }
574 575 var old_classes = class_map[key] ? class_map[key] : [];
575 576 key = this.model.get(trait_name);
576 577 var new_classes = class_map[key] ? class_map[key] : [];
577 578
578 579 this.update_classes(old_classes, new_classes, $el || this.$el);
579 580 },
580 581
581 582 _get_selector_element: function (selector) {
582 583 // Get the elements via the css selector.
583 584 var elements;
584 585 if (!selector) {
585 586 elements = this.$el;
586 587 } else {
587 588 elements = this.$el.find(selector).addBack(selector);
588 589 }
589 590 return elements;
590 591 },
591 592 });
592 593
593 594
594 595 var widget = {
595 596 'WidgetModel': WidgetModel,
596 597 'WidgetView': WidgetView,
597 598 'DOMWidgetView': DOMWidgetView,
598 599 };
599 600
600 601 // For backwards compatability.
601 602 $.extend(IPython, widget);
602 603
603 604 return widget;
604 605 });
General Comments 0
You need to be logged in to leave comments. Login now