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