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