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