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