##// END OF EJS Templates
handling explicitly provided modes from kernelspec.language_info
Nicholas Bollweg (Nick) -
Show More
@@ -1,862 +1,860
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 // silently upgrades CodeMirror
8 // silently upgrades CodeMirror
9 'codemirror/mode/meta',
9 'codemirror/mode/meta',
10 ], function(IPython, $, CodeMirror){
10 ], function(IPython, $, CodeMirror){
11 "use strict";
11 "use strict";
12
12
13 IPython.load_extensions = function () {
13 IPython.load_extensions = function () {
14 // load one or more IPython notebook extensions with requirejs
14 // load one or more IPython notebook extensions with requirejs
15
15
16 var extensions = [];
16 var extensions = [];
17 var extension_names = arguments;
17 var extension_names = arguments;
18 for (var i = 0; i < extension_names.length; i++) {
18 for (var i = 0; i < extension_names.length; i++) {
19 extensions.push("nbextensions/" + arguments[i]);
19 extensions.push("nbextensions/" + arguments[i]);
20 }
20 }
21
21
22 require(extensions,
22 require(extensions,
23 function () {
23 function () {
24 for (var i = 0; i < arguments.length; i++) {
24 for (var i = 0; i < arguments.length; i++) {
25 var ext = arguments[i];
25 var ext = arguments[i];
26 var ext_name = extension_names[i];
26 var ext_name = extension_names[i];
27 // success callback
27 // success callback
28 console.log("Loaded extension: " + ext_name);
28 console.log("Loaded extension: " + ext_name);
29 if (ext && ext.load_ipython_extension !== undefined) {
29 if (ext && ext.load_ipython_extension !== undefined) {
30 ext.load_ipython_extension();
30 ext.load_ipython_extension();
31 }
31 }
32 }
32 }
33 },
33 },
34 function (err) {
34 function (err) {
35 // failure callback
35 // failure callback
36 console.log("Failed to load extension(s):", err.requireModules, err);
36 console.log("Failed to load extension(s):", err.requireModules, err);
37 }
37 }
38 );
38 );
39 };
39 };
40
40
41 //============================================================================
41 //============================================================================
42 // Cross-browser RegEx Split
42 // Cross-browser RegEx Split
43 //============================================================================
43 //============================================================================
44
44
45 // This code has been MODIFIED from the code licensed below to not replace the
45 // This code has been MODIFIED from the code licensed below to not replace the
46 // default browser split. The license is reproduced here.
46 // default browser split. The license is reproduced here.
47
47
48 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
48 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
49 /*!
49 /*!
50 * Cross-Browser Split 1.1.1
50 * Cross-Browser Split 1.1.1
51 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
51 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
52 * Available under the MIT License
52 * Available under the MIT License
53 * ECMAScript compliant, uniform cross-browser split method
53 * ECMAScript compliant, uniform cross-browser split method
54 */
54 */
55
55
56 /**
56 /**
57 * Splits a string into an array of strings using a regex or string
57 * Splits a string into an array of strings using a regex or string
58 * separator. Matches of the separator are not included in the result array.
58 * separator. Matches of the separator are not included in the result array.
59 * However, if `separator` is a regex that contains capturing groups,
59 * However, if `separator` is a regex that contains capturing groups,
60 * backreferences are spliced into the result each time `separator` is
60 * backreferences are spliced into the result each time `separator` is
61 * matched. Fixes browser bugs compared to the native
61 * matched. Fixes browser bugs compared to the native
62 * `String.prototype.split` and can be used reliably cross-browser.
62 * `String.prototype.split` and can be used reliably cross-browser.
63 * @param {String} str String to split.
63 * @param {String} str String to split.
64 * @param {RegExp|String} separator Regex or string to use for separating
64 * @param {RegExp|String} separator Regex or string to use for separating
65 * the string.
65 * the string.
66 * @param {Number} [limit] Maximum number of items to include in the result
66 * @param {Number} [limit] Maximum number of items to include in the result
67 * array.
67 * array.
68 * @returns {Array} Array of substrings.
68 * @returns {Array} Array of substrings.
69 * @example
69 * @example
70 *
70 *
71 * // Basic use
71 * // Basic use
72 * regex_split('a b c d', ' ');
72 * regex_split('a b c d', ' ');
73 * // -> ['a', 'b', 'c', 'd']
73 * // -> ['a', 'b', 'c', 'd']
74 *
74 *
75 * // With limit
75 * // With limit
76 * regex_split('a b c d', ' ', 2);
76 * regex_split('a b c d', ' ', 2);
77 * // -> ['a', 'b']
77 * // -> ['a', 'b']
78 *
78 *
79 * // Backreferences in result array
79 * // Backreferences in result array
80 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
80 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
81 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
81 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
82 */
82 */
83 var regex_split = function (str, separator, limit) {
83 var regex_split = function (str, separator, limit) {
84 // If `separator` is not a regex, use `split`
84 // If `separator` is not a regex, use `split`
85 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
85 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
86 return split.call(str, separator, limit);
86 return split.call(str, separator, limit);
87 }
87 }
88 var output = [],
88 var output = [],
89 flags = (separator.ignoreCase ? "i" : "") +
89 flags = (separator.ignoreCase ? "i" : "") +
90 (separator.multiline ? "m" : "") +
90 (separator.multiline ? "m" : "") +
91 (separator.extended ? "x" : "") + // Proposed for ES6
91 (separator.extended ? "x" : "") + // Proposed for ES6
92 (separator.sticky ? "y" : ""), // Firefox 3+
92 (separator.sticky ? "y" : ""), // Firefox 3+
93 lastLastIndex = 0,
93 lastLastIndex = 0,
94 // Make `global` and avoid `lastIndex` issues by working with a copy
94 // Make `global` and avoid `lastIndex` issues by working with a copy
95 separator = new RegExp(separator.source, flags + "g"),
95 separator = new RegExp(separator.source, flags + "g"),
96 separator2, match, lastIndex, lastLength;
96 separator2, match, lastIndex, lastLength;
97 str += ""; // Type-convert
97 str += ""; // Type-convert
98
98
99 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
99 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
100 if (!compliantExecNpcg) {
100 if (!compliantExecNpcg) {
101 // Doesn't need flags gy, but they don't hurt
101 // Doesn't need flags gy, but they don't hurt
102 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
102 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
103 }
103 }
104 /* Values for `limit`, per the spec:
104 /* Values for `limit`, per the spec:
105 * If undefined: 4294967295 // Math.pow(2, 32) - 1
105 * If undefined: 4294967295 // Math.pow(2, 32) - 1
106 * If 0, Infinity, or NaN: 0
106 * If 0, Infinity, or NaN: 0
107 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
107 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
108 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
108 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
109 * If other: Type-convert, then use the above rules
109 * If other: Type-convert, then use the above rules
110 */
110 */
111 limit = typeof(limit) === "undefined" ?
111 limit = typeof(limit) === "undefined" ?
112 -1 >>> 0 : // Math.pow(2, 32) - 1
112 -1 >>> 0 : // Math.pow(2, 32) - 1
113 limit >>> 0; // ToUint32(limit)
113 limit >>> 0; // ToUint32(limit)
114 while (match = separator.exec(str)) {
114 while (match = separator.exec(str)) {
115 // `separator.lastIndex` is not reliable cross-browser
115 // `separator.lastIndex` is not reliable cross-browser
116 lastIndex = match.index + match[0].length;
116 lastIndex = match.index + match[0].length;
117 if (lastIndex > lastLastIndex) {
117 if (lastIndex > lastLastIndex) {
118 output.push(str.slice(lastLastIndex, match.index));
118 output.push(str.slice(lastLastIndex, match.index));
119 // Fix browsers whose `exec` methods don't consistently return `undefined` for
119 // Fix browsers whose `exec` methods don't consistently return `undefined` for
120 // nonparticipating capturing groups
120 // nonparticipating capturing groups
121 if (!compliantExecNpcg && match.length > 1) {
121 if (!compliantExecNpcg && match.length > 1) {
122 match[0].replace(separator2, function () {
122 match[0].replace(separator2, function () {
123 for (var i = 1; i < arguments.length - 2; i++) {
123 for (var i = 1; i < arguments.length - 2; i++) {
124 if (typeof(arguments[i]) === "undefined") {
124 if (typeof(arguments[i]) === "undefined") {
125 match[i] = undefined;
125 match[i] = undefined;
126 }
126 }
127 }
127 }
128 });
128 });
129 }
129 }
130 if (match.length > 1 && match.index < str.length) {
130 if (match.length > 1 && match.index < str.length) {
131 Array.prototype.push.apply(output, match.slice(1));
131 Array.prototype.push.apply(output, match.slice(1));
132 }
132 }
133 lastLength = match[0].length;
133 lastLength = match[0].length;
134 lastLastIndex = lastIndex;
134 lastLastIndex = lastIndex;
135 if (output.length >= limit) {
135 if (output.length >= limit) {
136 break;
136 break;
137 }
137 }
138 }
138 }
139 if (separator.lastIndex === match.index) {
139 if (separator.lastIndex === match.index) {
140 separator.lastIndex++; // Avoid an infinite loop
140 separator.lastIndex++; // Avoid an infinite loop
141 }
141 }
142 }
142 }
143 if (lastLastIndex === str.length) {
143 if (lastLastIndex === str.length) {
144 if (lastLength || !separator.test("")) {
144 if (lastLength || !separator.test("")) {
145 output.push("");
145 output.push("");
146 }
146 }
147 } else {
147 } else {
148 output.push(str.slice(lastLastIndex));
148 output.push(str.slice(lastLastIndex));
149 }
149 }
150 return output.length > limit ? output.slice(0, limit) : output;
150 return output.length > limit ? output.slice(0, limit) : output;
151 };
151 };
152
152
153 //============================================================================
153 //============================================================================
154 // End contributed Cross-browser RegEx Split
154 // End contributed Cross-browser RegEx Split
155 //============================================================================
155 //============================================================================
156
156
157
157
158 var uuid = function () {
158 var uuid = function () {
159 /**
159 /**
160 * http://www.ietf.org/rfc/rfc4122.txt
160 * http://www.ietf.org/rfc/rfc4122.txt
161 */
161 */
162 var s = [];
162 var s = [];
163 var hexDigits = "0123456789ABCDEF";
163 var hexDigits = "0123456789ABCDEF";
164 for (var i = 0; i < 32; i++) {
164 for (var i = 0; i < 32; i++) {
165 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
165 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
166 }
166 }
167 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
167 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
168 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
168 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
169
169
170 var uuid = s.join("");
170 var uuid = s.join("");
171 return uuid;
171 return uuid;
172 };
172 };
173
173
174
174
175 //Fix raw text to parse correctly in crazy XML
175 //Fix raw text to parse correctly in crazy XML
176 function xmlencode(string) {
176 function xmlencode(string) {
177 return string.replace(/\&/g,'&'+'amp;')
177 return string.replace(/\&/g,'&'+'amp;')
178 .replace(/</g,'&'+'lt;')
178 .replace(/</g,'&'+'lt;')
179 .replace(/>/g,'&'+'gt;')
179 .replace(/>/g,'&'+'gt;')
180 .replace(/\'/g,'&'+'apos;')
180 .replace(/\'/g,'&'+'apos;')
181 .replace(/\"/g,'&'+'quot;')
181 .replace(/\"/g,'&'+'quot;')
182 .replace(/`/g,'&'+'#96;');
182 .replace(/`/g,'&'+'#96;');
183 }
183 }
184
184
185
185
186 //Map from terminal commands to CSS classes
186 //Map from terminal commands to CSS classes
187 var ansi_colormap = {
187 var ansi_colormap = {
188 "01":"ansibold",
188 "01":"ansibold",
189
189
190 "30":"ansiblack",
190 "30":"ansiblack",
191 "31":"ansired",
191 "31":"ansired",
192 "32":"ansigreen",
192 "32":"ansigreen",
193 "33":"ansiyellow",
193 "33":"ansiyellow",
194 "34":"ansiblue",
194 "34":"ansiblue",
195 "35":"ansipurple",
195 "35":"ansipurple",
196 "36":"ansicyan",
196 "36":"ansicyan",
197 "37":"ansigray",
197 "37":"ansigray",
198
198
199 "40":"ansibgblack",
199 "40":"ansibgblack",
200 "41":"ansibgred",
200 "41":"ansibgred",
201 "42":"ansibggreen",
201 "42":"ansibggreen",
202 "43":"ansibgyellow",
202 "43":"ansibgyellow",
203 "44":"ansibgblue",
203 "44":"ansibgblue",
204 "45":"ansibgpurple",
204 "45":"ansibgpurple",
205 "46":"ansibgcyan",
205 "46":"ansibgcyan",
206 "47":"ansibggray"
206 "47":"ansibggray"
207 };
207 };
208
208
209 function _process_numbers(attrs, numbers) {
209 function _process_numbers(attrs, numbers) {
210 // process ansi escapes
210 // process ansi escapes
211 var n = numbers.shift();
211 var n = numbers.shift();
212 if (ansi_colormap[n]) {
212 if (ansi_colormap[n]) {
213 if ( ! attrs["class"] ) {
213 if ( ! attrs["class"] ) {
214 attrs["class"] = ansi_colormap[n];
214 attrs["class"] = ansi_colormap[n];
215 } else {
215 } else {
216 attrs["class"] += " " + ansi_colormap[n];
216 attrs["class"] += " " + ansi_colormap[n];
217 }
217 }
218 } else if (n == "38" || n == "48") {
218 } else if (n == "38" || n == "48") {
219 // VT100 256 color or 24 bit RGB
219 // VT100 256 color or 24 bit RGB
220 if (numbers.length < 2) {
220 if (numbers.length < 2) {
221 console.log("Not enough fields for VT100 color", numbers);
221 console.log("Not enough fields for VT100 color", numbers);
222 return;
222 return;
223 }
223 }
224
224
225 var index_or_rgb = numbers.shift();
225 var index_or_rgb = numbers.shift();
226 var r,g,b;
226 var r,g,b;
227 if (index_or_rgb == "5") {
227 if (index_or_rgb == "5") {
228 // 256 color
228 // 256 color
229 var idx = parseInt(numbers.shift());
229 var idx = parseInt(numbers.shift());
230 if (idx < 16) {
230 if (idx < 16) {
231 // indexed ANSI
231 // indexed ANSI
232 // ignore bright / non-bright distinction
232 // ignore bright / non-bright distinction
233 idx = idx % 8;
233 idx = idx % 8;
234 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
234 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
235 if ( ! attrs["class"] ) {
235 if ( ! attrs["class"] ) {
236 attrs["class"] = ansiclass;
236 attrs["class"] = ansiclass;
237 } else {
237 } else {
238 attrs["class"] += " " + ansiclass;
238 attrs["class"] += " " + ansiclass;
239 }
239 }
240 return;
240 return;
241 } else if (idx < 232) {
241 } else if (idx < 232) {
242 // 216 color 6x6x6 RGB
242 // 216 color 6x6x6 RGB
243 idx = idx - 16;
243 idx = idx - 16;
244 b = idx % 6;
244 b = idx % 6;
245 g = Math.floor(idx / 6) % 6;
245 g = Math.floor(idx / 6) % 6;
246 r = Math.floor(idx / 36) % 6;
246 r = Math.floor(idx / 36) % 6;
247 // convert to rgb
247 // convert to rgb
248 r = (r * 51);
248 r = (r * 51);
249 g = (g * 51);
249 g = (g * 51);
250 b = (b * 51);
250 b = (b * 51);
251 } else {
251 } else {
252 // grayscale
252 // grayscale
253 idx = idx - 231;
253 idx = idx - 231;
254 // it's 1-24 and should *not* include black or white,
254 // it's 1-24 and should *not* include black or white,
255 // so a 26 point scale
255 // so a 26 point scale
256 r = g = b = Math.floor(idx * 256 / 26);
256 r = g = b = Math.floor(idx * 256 / 26);
257 }
257 }
258 } else if (index_or_rgb == "2") {
258 } else if (index_or_rgb == "2") {
259 // Simple 24 bit RGB
259 // Simple 24 bit RGB
260 if (numbers.length > 3) {
260 if (numbers.length > 3) {
261 console.log("Not enough fields for RGB", numbers);
261 console.log("Not enough fields for RGB", numbers);
262 return;
262 return;
263 }
263 }
264 r = numbers.shift();
264 r = numbers.shift();
265 g = numbers.shift();
265 g = numbers.shift();
266 b = numbers.shift();
266 b = numbers.shift();
267 } else {
267 } else {
268 console.log("unrecognized control", numbers);
268 console.log("unrecognized control", numbers);
269 return;
269 return;
270 }
270 }
271 if (r !== undefined) {
271 if (r !== undefined) {
272 // apply the rgb color
272 // apply the rgb color
273 var line;
273 var line;
274 if (n == "38") {
274 if (n == "38") {
275 line = "color: ";
275 line = "color: ";
276 } else {
276 } else {
277 line = "background-color: ";
277 line = "background-color: ";
278 }
278 }
279 line = line + "rgb(" + r + "," + g + "," + b + ");";
279 line = line + "rgb(" + r + "," + g + "," + b + ");";
280 if ( !attrs.style ) {
280 if ( !attrs.style ) {
281 attrs.style = line;
281 attrs.style = line;
282 } else {
282 } else {
283 attrs.style += " " + line;
283 attrs.style += " " + line;
284 }
284 }
285 }
285 }
286 }
286 }
287 }
287 }
288
288
289 function ansispan(str) {
289 function ansispan(str) {
290 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
290 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
291 // regular ansi escapes (using the table above)
291 // regular ansi escapes (using the table above)
292 var is_open = false;
292 var is_open = false;
293 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
293 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
294 if (!pattern) {
294 if (!pattern) {
295 // [(01|22|39|)m close spans
295 // [(01|22|39|)m close spans
296 if (is_open) {
296 if (is_open) {
297 is_open = false;
297 is_open = false;
298 return "</span>";
298 return "</span>";
299 } else {
299 } else {
300 return "";
300 return "";
301 }
301 }
302 } else {
302 } else {
303 is_open = true;
303 is_open = true;
304
304
305 // consume sequence of color escapes
305 // consume sequence of color escapes
306 var numbers = pattern.match(/\d+/g);
306 var numbers = pattern.match(/\d+/g);
307 var attrs = {};
307 var attrs = {};
308 while (numbers.length > 0) {
308 while (numbers.length > 0) {
309 _process_numbers(attrs, numbers);
309 _process_numbers(attrs, numbers);
310 }
310 }
311
311
312 var span = "<span ";
312 var span = "<span ";
313 for (var attr in attrs) {
313 for (var attr in attrs) {
314 var value = attrs[attr];
314 var value = attrs[attr];
315 span = span + " " + attr + '="' + attrs[attr] + '"';
315 span = span + " " + attr + '="' + attrs[attr] + '"';
316 }
316 }
317 return span + ">";
317 return span + ">";
318 }
318 }
319 });
319 });
320 }
320 }
321
321
322 // Transform ANSI color escape codes into HTML <span> tags with css
322 // Transform ANSI color escape codes into HTML <span> tags with css
323 // classes listed in the above ansi_colormap object. The actual color used
323 // classes listed in the above ansi_colormap object. The actual color used
324 // are set in the css file.
324 // are set in the css file.
325 function fixConsole(txt) {
325 function fixConsole(txt) {
326 txt = xmlencode(txt);
326 txt = xmlencode(txt);
327 var re = /\033\[([\dA-Fa-f;]*?)m/;
327 var re = /\033\[([\dA-Fa-f;]*?)m/;
328 var opened = false;
328 var opened = false;
329 var cmds = [];
329 var cmds = [];
330 var opener = "";
330 var opener = "";
331 var closer = "";
331 var closer = "";
332
332
333 // Strip all ANSI codes that are not color related. Matches
333 // Strip all ANSI codes that are not color related. Matches
334 // all ANSI codes that do not end with "m".
334 // all ANSI codes that do not end with "m".
335 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
335 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
336 txt = txt.replace(ignored_re, "");
336 txt = txt.replace(ignored_re, "");
337
337
338 // color ansi codes
338 // color ansi codes
339 txt = ansispan(txt);
339 txt = ansispan(txt);
340 return txt;
340 return txt;
341 }
341 }
342
342
343 // Remove chunks that should be overridden by the effect of
343 // Remove chunks that should be overridden by the effect of
344 // carriage return characters
344 // carriage return characters
345 function fixCarriageReturn(txt) {
345 function fixCarriageReturn(txt) {
346 var tmp = txt;
346 var tmp = txt;
347 do {
347 do {
348 txt = tmp;
348 txt = tmp;
349 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
349 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
350 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
350 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
351 } while (tmp.length < txt.length);
351 } while (tmp.length < txt.length);
352 return txt;
352 return txt;
353 }
353 }
354
354
355 // Locate any URLs and convert them to a anchor tag
355 // Locate any URLs and convert them to a anchor tag
356 function autoLinkUrls(txt) {
356 function autoLinkUrls(txt) {
357 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
357 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
358 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
358 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
359 }
359 }
360
360
361 var points_to_pixels = function (points) {
361 var points_to_pixels = function (points) {
362 /**
362 /**
363 * A reasonably good way of converting between points and pixels.
363 * A reasonably good way of converting between points and pixels.
364 */
364 */
365 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
365 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
366 $(body).append(test);
366 $(body).append(test);
367 var pixel_per_point = test.width()/10000;
367 var pixel_per_point = test.width()/10000;
368 test.remove();
368 test.remove();
369 return Math.floor(points*pixel_per_point);
369 return Math.floor(points*pixel_per_point);
370 };
370 };
371
371
372 var always_new = function (constructor) {
372 var always_new = function (constructor) {
373 /**
373 /**
374 * wrapper around contructor to avoid requiring `var a = new constructor()`
374 * wrapper around contructor to avoid requiring `var a = new constructor()`
375 * useful for passing constructors as callbacks,
375 * useful for passing constructors as callbacks,
376 * not for programmer laziness.
376 * not for programmer laziness.
377 * from http://programmers.stackexchange.com/questions/118798
377 * from http://programmers.stackexchange.com/questions/118798
378 */
378 */
379 return function () {
379 return function () {
380 var obj = Object.create(constructor.prototype);
380 var obj = Object.create(constructor.prototype);
381 constructor.apply(obj, arguments);
381 constructor.apply(obj, arguments);
382 return obj;
382 return obj;
383 };
383 };
384 };
384 };
385
385
386 var url_path_join = function () {
386 var url_path_join = function () {
387 /**
387 /**
388 * join a sequence of url components with '/'
388 * join a sequence of url components with '/'
389 */
389 */
390 var url = '';
390 var url = '';
391 for (var i = 0; i < arguments.length; i++) {
391 for (var i = 0; i < arguments.length; i++) {
392 if (arguments[i] === '') {
392 if (arguments[i] === '') {
393 continue;
393 continue;
394 }
394 }
395 if (url.length > 0 && url[url.length-1] != '/') {
395 if (url.length > 0 && url[url.length-1] != '/') {
396 url = url + '/' + arguments[i];
396 url = url + '/' + arguments[i];
397 } else {
397 } else {
398 url = url + arguments[i];
398 url = url + arguments[i];
399 }
399 }
400 }
400 }
401 url = url.replace(/\/\/+/, '/');
401 url = url.replace(/\/\/+/, '/');
402 return url;
402 return url;
403 };
403 };
404
404
405 var url_path_split = function (path) {
405 var url_path_split = function (path) {
406 /**
406 /**
407 * Like os.path.split for URLs.
407 * Like os.path.split for URLs.
408 * Always returns two strings, the directory path and the base filename
408 * Always returns two strings, the directory path and the base filename
409 */
409 */
410
410
411 var idx = path.lastIndexOf('/');
411 var idx = path.lastIndexOf('/');
412 if (idx === -1) {
412 if (idx === -1) {
413 return ['', path];
413 return ['', path];
414 } else {
414 } else {
415 return [ path.slice(0, idx), path.slice(idx + 1) ];
415 return [ path.slice(0, idx), path.slice(idx + 1) ];
416 }
416 }
417 };
417 };
418
418
419 var parse_url = function (url) {
419 var parse_url = function (url) {
420 /**
420 /**
421 * an `a` element with an href allows attr-access to the parsed segments of a URL
421 * an `a` element with an href allows attr-access to the parsed segments of a URL
422 * a = parse_url("http://localhost:8888/path/name#hash")
422 * a = parse_url("http://localhost:8888/path/name#hash")
423 * a.protocol = "http:"
423 * a.protocol = "http:"
424 * a.host = "localhost:8888"
424 * a.host = "localhost:8888"
425 * a.hostname = "localhost"
425 * a.hostname = "localhost"
426 * a.port = 8888
426 * a.port = 8888
427 * a.pathname = "/path/name"
427 * a.pathname = "/path/name"
428 * a.hash = "#hash"
428 * a.hash = "#hash"
429 */
429 */
430 var a = document.createElement("a");
430 var a = document.createElement("a");
431 a.href = url;
431 a.href = url;
432 return a;
432 return a;
433 };
433 };
434
434
435 var encode_uri_components = function (uri) {
435 var encode_uri_components = function (uri) {
436 /**
436 /**
437 * encode just the components of a multi-segment uri,
437 * encode just the components of a multi-segment uri,
438 * leaving '/' separators
438 * leaving '/' separators
439 */
439 */
440 return uri.split('/').map(encodeURIComponent).join('/');
440 return uri.split('/').map(encodeURIComponent).join('/');
441 };
441 };
442
442
443 var url_join_encode = function () {
443 var url_join_encode = function () {
444 /**
444 /**
445 * join a sequence of url components with '/',
445 * join a sequence of url components with '/',
446 * encoding each component with encodeURIComponent
446 * encoding each component with encodeURIComponent
447 */
447 */
448 return encode_uri_components(url_path_join.apply(null, arguments));
448 return encode_uri_components(url_path_join.apply(null, arguments));
449 };
449 };
450
450
451
451
452 var splitext = function (filename) {
452 var splitext = function (filename) {
453 /**
453 /**
454 * mimic Python os.path.splitext
454 * mimic Python os.path.splitext
455 * Returns ['base', '.ext']
455 * Returns ['base', '.ext']
456 */
456 */
457 var idx = filename.lastIndexOf('.');
457 var idx = filename.lastIndexOf('.');
458 if (idx > 0) {
458 if (idx > 0) {
459 return [filename.slice(0, idx), filename.slice(idx)];
459 return [filename.slice(0, idx), filename.slice(idx)];
460 } else {
460 } else {
461 return [filename, ''];
461 return [filename, ''];
462 }
462 }
463 };
463 };
464
464
465
465
466 var escape_html = function (text) {
466 var escape_html = function (text) {
467 /**
467 /**
468 * escape text to HTML
468 * escape text to HTML
469 */
469 */
470 return $("<div/>").text(text).html();
470 return $("<div/>").text(text).html();
471 };
471 };
472
472
473
473
474 var get_body_data = function(key) {
474 var get_body_data = function(key) {
475 /**
475 /**
476 * get a url-encoded item from body.data and decode it
476 * get a url-encoded item from body.data and decode it
477 * we should never have any encoded URLs anywhere else in code
477 * we should never have any encoded URLs anywhere else in code
478 * until we are building an actual request
478 * until we are building an actual request
479 */
479 */
480 return decodeURIComponent($('body').data(key));
480 return decodeURIComponent($('body').data(key));
481 };
481 };
482
482
483 var to_absolute_cursor_pos = function (cm, cursor) {
483 var to_absolute_cursor_pos = function (cm, cursor) {
484 /**
484 /**
485 * get the absolute cursor position from CodeMirror's col, ch
485 * get the absolute cursor position from CodeMirror's col, ch
486 */
486 */
487 if (!cursor) {
487 if (!cursor) {
488 cursor = cm.getCursor();
488 cursor = cm.getCursor();
489 }
489 }
490 var cursor_pos = cursor.ch;
490 var cursor_pos = cursor.ch;
491 for (var i = 0; i < cursor.line; i++) {
491 for (var i = 0; i < cursor.line; i++) {
492 cursor_pos += cm.getLine(i).length + 1;
492 cursor_pos += cm.getLine(i).length + 1;
493 }
493 }
494 return cursor_pos;
494 return cursor_pos;
495 };
495 };
496
496
497 var from_absolute_cursor_pos = function (cm, cursor_pos) {
497 var from_absolute_cursor_pos = function (cm, cursor_pos) {
498 /**
498 /**
499 * turn absolute cursor postion into CodeMirror col, ch cursor
499 * turn absolute cursor postion into CodeMirror col, ch cursor
500 */
500 */
501 var i, line;
501 var i, line;
502 var offset = 0;
502 var offset = 0;
503 for (i = 0, line=cm.getLine(i); line !== undefined; i++, line=cm.getLine(i)) {
503 for (i = 0, line=cm.getLine(i); line !== undefined; i++, line=cm.getLine(i)) {
504 if (offset + line.length < cursor_pos) {
504 if (offset + line.length < cursor_pos) {
505 offset += line.length + 1;
505 offset += line.length + 1;
506 } else {
506 } else {
507 return {
507 return {
508 line : i,
508 line : i,
509 ch : cursor_pos - offset,
509 ch : cursor_pos - offset,
510 };
510 };
511 }
511 }
512 }
512 }
513 // reached end, return endpoint
513 // reached end, return endpoint
514 return {
514 return {
515 ch : line.length - 1,
515 ch : line.length - 1,
516 line : i - 1,
516 line : i - 1,
517 };
517 };
518 };
518 };
519
519
520 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
520 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
521 var browser = (function() {
521 var browser = (function() {
522 if (typeof navigator === 'undefined') {
522 if (typeof navigator === 'undefined') {
523 // navigator undefined in node
523 // navigator undefined in node
524 return 'None';
524 return 'None';
525 }
525 }
526 var N= navigator.appName, ua= navigator.userAgent, tem;
526 var N= navigator.appName, ua= navigator.userAgent, tem;
527 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
527 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
528 if (M && (tem= ua.match(/version\/([\.\d]+)/i)) !== null) M[2]= tem[1];
528 if (M && (tem= ua.match(/version\/([\.\d]+)/i)) !== null) M[2]= tem[1];
529 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
529 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
530 return M;
530 return M;
531 })();
531 })();
532
532
533 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
533 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
534 var platform = (function () {
534 var platform = (function () {
535 if (typeof navigator === 'undefined') {
535 if (typeof navigator === 'undefined') {
536 // navigator undefined in node
536 // navigator undefined in node
537 return 'None';
537 return 'None';
538 }
538 }
539 var OSName="None";
539 var OSName="None";
540 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
540 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
541 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
541 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
542 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
542 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
543 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
543 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
544 return OSName;
544 return OSName;
545 })();
545 })();
546
546
547 var get_url_param = function (name) {
547 var get_url_param = function (name) {
548 // get a URL parameter. I cannot believe we actually need this.
548 // get a URL parameter. I cannot believe we actually need this.
549 // Based on http://stackoverflow.com/a/25359264/938949
549 // Based on http://stackoverflow.com/a/25359264/938949
550 var match = new RegExp('[\?&]' + name + '=([^&]*)').exec(window.location.search);
550 var match = new RegExp('[\?&]' + name + '=([^&]*)').exec(window.location.search);
551 if (match){
551 if (match){
552 return decodeURIComponent(match[1] || '');
552 return decodeURIComponent(match[1] || '');
553 }
553 }
554 };
554 };
555
555
556 var is_or_has = function (a, b) {
556 var is_or_has = function (a, b) {
557 /**
557 /**
558 * Is b a child of a or a itself?
558 * Is b a child of a or a itself?
559 */
559 */
560 return a.has(b).length !==0 || a.is(b);
560 return a.has(b).length !==0 || a.is(b);
561 };
561 };
562
562
563 var is_focused = function (e) {
563 var is_focused = function (e) {
564 /**
564 /**
565 * Is element e, or one of its children focused?
565 * Is element e, or one of its children focused?
566 */
566 */
567 e = $(e);
567 e = $(e);
568 var target = $(document.activeElement);
568 var target = $(document.activeElement);
569 if (target.length > 0) {
569 if (target.length > 0) {
570 if (is_or_has(e, target)) {
570 if (is_or_has(e, target)) {
571 return true;
571 return true;
572 } else {
572 } else {
573 return false;
573 return false;
574 }
574 }
575 } else {
575 } else {
576 return false;
576 return false;
577 }
577 }
578 };
578 };
579
579
580 var mergeopt = function(_class, options, overwrite){
580 var mergeopt = function(_class, options, overwrite){
581 options = options || {};
581 options = options || {};
582 overwrite = overwrite || {};
582 overwrite = overwrite || {};
583 return $.extend(true, {}, _class.options_default, options, overwrite);
583 return $.extend(true, {}, _class.options_default, options, overwrite);
584 };
584 };
585
585
586 var ajax_error_msg = function (jqXHR) {
586 var ajax_error_msg = function (jqXHR) {
587 /**
587 /**
588 * Return a JSON error message if there is one,
588 * Return a JSON error message if there is one,
589 * otherwise the basic HTTP status text.
589 * otherwise the basic HTTP status text.
590 */
590 */
591 if (jqXHR.responseJSON && jqXHR.responseJSON.traceback) {
591 if (jqXHR.responseJSON && jqXHR.responseJSON.traceback) {
592 return jqXHR.responseJSON.traceback;
592 return jqXHR.responseJSON.traceback;
593 } else if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
593 } else if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
594 return jqXHR.responseJSON.message;
594 return jqXHR.responseJSON.message;
595 } else {
595 } else {
596 return jqXHR.statusText;
596 return jqXHR.statusText;
597 }
597 }
598 };
598 };
599 var log_ajax_error = function (jqXHR, status, error) {
599 var log_ajax_error = function (jqXHR, status, error) {
600 /**
600 /**
601 * log ajax failures with informative messages
601 * log ajax failures with informative messages
602 */
602 */
603 var msg = "API request failed (" + jqXHR.status + "): ";
603 var msg = "API request failed (" + jqXHR.status + "): ";
604 console.log(jqXHR);
604 console.log(jqXHR);
605 msg += ajax_error_msg(jqXHR);
605 msg += ajax_error_msg(jqXHR);
606 console.log(msg);
606 console.log(msg);
607 };
607 };
608
608
609 var requireCodeMirrorMode = function (mode, callback, errback) {
609 var requireCodeMirrorMode = function (mode, callback, errback) {
610 /**
610 /**
611 * find a predefined mode or detect from CM metadata then
611 * find a predefined mode or detect from CM metadata then
612 * require and callback with the resolveable mode string: mime or
612 * require and callback with the resolveable mode string: mime or
613 * custom name
613 * custom name
614 */
614 */
615 if (typeof mode != "string") mode = mode.name;
616
615
617 if (CodeMirror.modes.hasOwnProperty(mode)) {
616 var modename = (typeof mode == "string") ? mode :
617 mode.mode || mode.name;
618
619 if (CodeMirror.modes.hasOwnProperty(modename)) {
618 callback(mode);
620 callback(mode);
619 return;
621 return;
620 }
622 }
621
623
622 var info = CodeMirror.findModeByName(mode) ||
624 var info = (mode && mode.mode && mode.mime && mode) ||
623 CodeMirror.findModeByExtension(mode.split(".").slice(-1)) ||
625 CodeMirror.findModeByName(modename) ||
624 CodeMirror.findModeByMIME(mode);
626 CodeMirror.findModeByExtension(modename.split(".").slice(-1)) ||
625
627 CodeMirror.findModeByMIME(modename);
626 if(!info){
627 errback && errback();
628 return;
629 }
630
628
631 require([
629 require([
632 // might want to use CodeMirror.modeURL here
630 // might want to use CodeMirror.modeURL here
633 ['codemirror/mode', info.mode, info.mode].join('/'),
631 ['codemirror/mode', info.mode, info.mode].join('/'),
634 ], function() { callback(info.mime); }, errback
632 ], function() { callback(info.mime); }, errback
635 );
633 );
636 };
634 };
637
635
638 /** Error type for wrapped XHR errors. */
636 /** Error type for wrapped XHR errors. */
639 var XHR_ERROR = 'XhrError';
637 var XHR_ERROR = 'XhrError';
640
638
641 /**
639 /**
642 * Wraps an AJAX error as an Error object.
640 * Wraps an AJAX error as an Error object.
643 */
641 */
644 var wrap_ajax_error = function (jqXHR, status, error) {
642 var wrap_ajax_error = function (jqXHR, status, error) {
645 var wrapped_error = new Error(ajax_error_msg(jqXHR));
643 var wrapped_error = new Error(ajax_error_msg(jqXHR));
646 wrapped_error.name = XHR_ERROR;
644 wrapped_error.name = XHR_ERROR;
647 // provide xhr response
645 // provide xhr response
648 wrapped_error.xhr = jqXHR;
646 wrapped_error.xhr = jqXHR;
649 wrapped_error.xhr_status = status;
647 wrapped_error.xhr_status = status;
650 wrapped_error.xhr_error = error;
648 wrapped_error.xhr_error = error;
651 return wrapped_error;
649 return wrapped_error;
652 };
650 };
653
651
654 var promising_ajax = function(url, settings) {
652 var promising_ajax = function(url, settings) {
655 /**
653 /**
656 * Like $.ajax, but returning an ES6 promise. success and error settings
654 * Like $.ajax, but returning an ES6 promise. success and error settings
657 * will be ignored.
655 * will be ignored.
658 */
656 */
659 settings = settings || {};
657 settings = settings || {};
660 return new Promise(function(resolve, reject) {
658 return new Promise(function(resolve, reject) {
661 settings.success = function(data, status, jqXHR) {
659 settings.success = function(data, status, jqXHR) {
662 resolve(data);
660 resolve(data);
663 };
661 };
664 settings.error = function(jqXHR, status, error) {
662 settings.error = function(jqXHR, status, error) {
665 log_ajax_error(jqXHR, status, error);
663 log_ajax_error(jqXHR, status, error);
666 reject(wrap_ajax_error(jqXHR, status, error));
664 reject(wrap_ajax_error(jqXHR, status, error));
667 };
665 };
668 $.ajax(url, settings);
666 $.ajax(url, settings);
669 });
667 });
670 };
668 };
671
669
672 var WrappedError = function(message, error){
670 var WrappedError = function(message, error){
673 /**
671 /**
674 * Wrappable Error class
672 * Wrappable Error class
675 *
673 *
676 * The Error class doesn't actually act on `this`. Instead it always
674 * The Error class doesn't actually act on `this`. Instead it always
677 * returns a new instance of Error. Here we capture that instance so we
675 * returns a new instance of Error. Here we capture that instance so we
678 * can apply it's properties to `this`.
676 * can apply it's properties to `this`.
679 */
677 */
680 var tmp = Error.apply(this, [message]);
678 var tmp = Error.apply(this, [message]);
681
679
682 // Copy the properties of the error over to this.
680 // Copy the properties of the error over to this.
683 var properties = Object.getOwnPropertyNames(tmp);
681 var properties = Object.getOwnPropertyNames(tmp);
684 for (var i = 0; i < properties.length; i++) {
682 for (var i = 0; i < properties.length; i++) {
685 this[properties[i]] = tmp[properties[i]];
683 this[properties[i]] = tmp[properties[i]];
686 }
684 }
687
685
688 // Keep a stack of the original error messages.
686 // Keep a stack of the original error messages.
689 if (error instanceof WrappedError) {
687 if (error instanceof WrappedError) {
690 this.error_stack = error.error_stack;
688 this.error_stack = error.error_stack;
691 } else {
689 } else {
692 this.error_stack = [error];
690 this.error_stack = [error];
693 }
691 }
694 this.error_stack.push(tmp);
692 this.error_stack.push(tmp);
695
693
696 return this;
694 return this;
697 };
695 };
698
696
699 WrappedError.prototype = Object.create(Error.prototype, {});
697 WrappedError.prototype = Object.create(Error.prototype, {});
700
698
701
699
702 var load_class = function(class_name, module_name, registry) {
700 var load_class = function(class_name, module_name, registry) {
703 /**
701 /**
704 * Tries to load a class
702 * Tries to load a class
705 *
703 *
706 * Tries to load a class from a module using require.js, if a module
704 * Tries to load a class from a module using require.js, if a module
707 * is specified, otherwise tries to load a class from the global
705 * is specified, otherwise tries to load a class from the global
708 * registry, if the global registry is provided.
706 * registry, if the global registry is provided.
709 */
707 */
710 return new Promise(function(resolve, reject) {
708 return new Promise(function(resolve, reject) {
711
709
712 // Try loading the view module using require.js
710 // Try loading the view module using require.js
713 if (module_name) {
711 if (module_name) {
714 require([module_name], function(module) {
712 require([module_name], function(module) {
715 if (module[class_name] === undefined) {
713 if (module[class_name] === undefined) {
716 reject(new Error('Class '+class_name+' not found in module '+module_name));
714 reject(new Error('Class '+class_name+' not found in module '+module_name));
717 } else {
715 } else {
718 resolve(module[class_name]);
716 resolve(module[class_name]);
719 }
717 }
720 }, reject);
718 }, reject);
721 } else {
719 } else {
722 if (registry && registry[class_name]) {
720 if (registry && registry[class_name]) {
723 resolve(registry[class_name]);
721 resolve(registry[class_name]);
724 } else {
722 } else {
725 reject(new Error('Class '+class_name+' not found in registry '));
723 reject(new Error('Class '+class_name+' not found in registry '));
726 }
724 }
727 }
725 }
728 });
726 });
729 };
727 };
730
728
731 var resolve_promises_dict = function(d) {
729 var resolve_promises_dict = function(d) {
732 /**
730 /**
733 * Resolve a promiseful dictionary.
731 * Resolve a promiseful dictionary.
734 * Returns a single Promise.
732 * Returns a single Promise.
735 */
733 */
736 var keys = Object.keys(d);
734 var keys = Object.keys(d);
737 var values = [];
735 var values = [];
738 keys.forEach(function(key) {
736 keys.forEach(function(key) {
739 values.push(d[key]);
737 values.push(d[key]);
740 });
738 });
741 return Promise.all(values).then(function(v) {
739 return Promise.all(values).then(function(v) {
742 d = {};
740 d = {};
743 for(var i=0; i<keys.length; i++) {
741 for(var i=0; i<keys.length; i++) {
744 d[keys[i]] = v[i];
742 d[keys[i]] = v[i];
745 }
743 }
746 return d;
744 return d;
747 });
745 });
748 };
746 };
749
747
750 var WrappedError = function(message, error){
748 var WrappedError = function(message, error){
751 /**
749 /**
752 * Wrappable Error class
750 * Wrappable Error class
753 *
751 *
754 * The Error class doesn't actually act on `this`. Instead it always
752 * The Error class doesn't actually act on `this`. Instead it always
755 * returns a new instance of Error. Here we capture that instance so we
753 * returns a new instance of Error. Here we capture that instance so we
756 * can apply it's properties to `this`.
754 * can apply it's properties to `this`.
757 */
755 */
758 var tmp = Error.apply(this, [message]);
756 var tmp = Error.apply(this, [message]);
759
757
760 // Copy the properties of the error over to this.
758 // Copy the properties of the error over to this.
761 var properties = Object.getOwnPropertyNames(tmp);
759 var properties = Object.getOwnPropertyNames(tmp);
762 for (var i = 0; i < properties.length; i++) {
760 for (var i = 0; i < properties.length; i++) {
763 this[properties[i]] = tmp[properties[i]];
761 this[properties[i]] = tmp[properties[i]];
764 }
762 }
765
763
766 // Keep a stack of the original error messages.
764 // Keep a stack of the original error messages.
767 if (error instanceof WrappedError) {
765 if (error instanceof WrappedError) {
768 this.error_stack = error.error_stack;
766 this.error_stack = error.error_stack;
769 } else {
767 } else {
770 this.error_stack = [error];
768 this.error_stack = [error];
771 }
769 }
772 this.error_stack.push(tmp);
770 this.error_stack.push(tmp);
773
771
774 return this;
772 return this;
775 };
773 };
776
774
777 WrappedError.prototype = Object.create(Error.prototype, {});
775 WrappedError.prototype = Object.create(Error.prototype, {});
778
776
779 var reject = function(message, log) {
777 var reject = function(message, log) {
780 /**
778 /**
781 * Creates a wrappable Promise rejection function.
779 * Creates a wrappable Promise rejection function.
782 *
780 *
783 * Creates a function that returns a Promise.reject with a new WrappedError
781 * Creates a function that returns a Promise.reject with a new WrappedError
784 * that has the provided message and wraps the original error that
782 * that has the provided message and wraps the original error that
785 * caused the promise to reject.
783 * caused the promise to reject.
786 */
784 */
787 return function(error) {
785 return function(error) {
788 var wrapped_error = new WrappedError(message, error);
786 var wrapped_error = new WrappedError(message, error);
789 if (log) console.error(wrapped_error);
787 if (log) console.error(wrapped_error);
790 return Promise.reject(wrapped_error);
788 return Promise.reject(wrapped_error);
791 };
789 };
792 };
790 };
793
791
794 var typeset = function(element, text) {
792 var typeset = function(element, text) {
795 /**
793 /**
796 * Apply MathJax rendering to an element, and optionally set its text
794 * Apply MathJax rendering to an element, and optionally set its text
797 *
795 *
798 * If MathJax is not available, make no changes.
796 * If MathJax is not available, make no changes.
799 *
797 *
800 * Returns the output any number of typeset elements, or undefined if
798 * Returns the output any number of typeset elements, or undefined if
801 * MathJax was not available.
799 * MathJax was not available.
802 *
800 *
803 * Parameters
801 * Parameters
804 * ----------
802 * ----------
805 * element: Node, NodeList, or jQuery selection
803 * element: Node, NodeList, or jQuery selection
806 * text: option string
804 * text: option string
807 */
805 */
808 if(!window.MathJax){
806 if(!window.MathJax){
809 return;
807 return;
810 }
808 }
811 var $el = element.jquery ? element : $(element);
809 var $el = element.jquery ? element : $(element);
812 if(arguments.length > 1){
810 if(arguments.length > 1){
813 $el.text(text);
811 $el.text(text);
814 }
812 }
815 return $el.map(function(){
813 return $el.map(function(){
816 // MathJax takes a DOM node: $.map makes `this` the context
814 // MathJax takes a DOM node: $.map makes `this` the context
817 return MathJax.Hub.Queue(["Typeset", MathJax.Hub, this]);
815 return MathJax.Hub.Queue(["Typeset", MathJax.Hub, this]);
818 });
816 });
819 };
817 };
820
818
821 var utils = {
819 var utils = {
822 regex_split : regex_split,
820 regex_split : regex_split,
823 uuid : uuid,
821 uuid : uuid,
824 fixConsole : fixConsole,
822 fixConsole : fixConsole,
825 fixCarriageReturn : fixCarriageReturn,
823 fixCarriageReturn : fixCarriageReturn,
826 autoLinkUrls : autoLinkUrls,
824 autoLinkUrls : autoLinkUrls,
827 points_to_pixels : points_to_pixels,
825 points_to_pixels : points_to_pixels,
828 get_body_data : get_body_data,
826 get_body_data : get_body_data,
829 parse_url : parse_url,
827 parse_url : parse_url,
830 url_path_split : url_path_split,
828 url_path_split : url_path_split,
831 url_path_join : url_path_join,
829 url_path_join : url_path_join,
832 url_join_encode : url_join_encode,
830 url_join_encode : url_join_encode,
833 encode_uri_components : encode_uri_components,
831 encode_uri_components : encode_uri_components,
834 splitext : splitext,
832 splitext : splitext,
835 escape_html : escape_html,
833 escape_html : escape_html,
836 always_new : always_new,
834 always_new : always_new,
837 to_absolute_cursor_pos : to_absolute_cursor_pos,
835 to_absolute_cursor_pos : to_absolute_cursor_pos,
838 from_absolute_cursor_pos : from_absolute_cursor_pos,
836 from_absolute_cursor_pos : from_absolute_cursor_pos,
839 browser : browser,
837 browser : browser,
840 platform: platform,
838 platform: platform,
841 get_url_param: get_url_param,
839 get_url_param: get_url_param,
842 is_or_has : is_or_has,
840 is_or_has : is_or_has,
843 is_focused : is_focused,
841 is_focused : is_focused,
844 mergeopt: mergeopt,
842 mergeopt: mergeopt,
845 ajax_error_msg : ajax_error_msg,
843 ajax_error_msg : ajax_error_msg,
846 log_ajax_error : log_ajax_error,
844 log_ajax_error : log_ajax_error,
847 requireCodeMirrorMode : requireCodeMirrorMode,
845 requireCodeMirrorMode : requireCodeMirrorMode,
848 XHR_ERROR : XHR_ERROR,
846 XHR_ERROR : XHR_ERROR,
849 wrap_ajax_error : wrap_ajax_error,
847 wrap_ajax_error : wrap_ajax_error,
850 promising_ajax : promising_ajax,
848 promising_ajax : promising_ajax,
851 WrappedError: WrappedError,
849 WrappedError: WrappedError,
852 load_class: load_class,
850 load_class: load_class,
853 resolve_promises_dict: resolve_promises_dict,
851 resolve_promises_dict: resolve_promises_dict,
854 reject: reject,
852 reject: reject,
855 typeset: typeset,
853 typeset: typeset,
856 };
854 };
857
855
858 // Backwards compatability.
856 // Backwards compatability.
859 IPython.utils = utils;
857 IPython.utils = utils;
860
858
861 return utils;
859 return utils;
862 });
860 });
@@ -1,2527 +1,2526
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 'base/js/namespace',
5 'base/js/namespace',
6 'jquery',
6 'jquery',
7 'base/js/utils',
7 'base/js/utils',
8 'base/js/dialog',
8 'base/js/dialog',
9 'notebook/js/cell',
9 'notebook/js/cell',
10 'notebook/js/textcell',
10 'notebook/js/textcell',
11 'notebook/js/codecell',
11 'notebook/js/codecell',
12 'services/sessions/session',
12 'services/sessions/session',
13 'notebook/js/celltoolbar',
13 'notebook/js/celltoolbar',
14 'components/marked/lib/marked',
14 'components/marked/lib/marked',
15 'codemirror/lib/codemirror',
15 'codemirror/lib/codemirror',
16 'codemirror/addon/runmode/runmode',
16 'codemirror/addon/runmode/runmode',
17 'notebook/js/mathjaxutils',
17 'notebook/js/mathjaxutils',
18 'base/js/keyboard',
18 'base/js/keyboard',
19 'notebook/js/tooltip',
19 'notebook/js/tooltip',
20 'notebook/js/celltoolbarpresets/default',
20 'notebook/js/celltoolbarpresets/default',
21 'notebook/js/celltoolbarpresets/rawcell',
21 'notebook/js/celltoolbarpresets/rawcell',
22 'notebook/js/celltoolbarpresets/slideshow',
22 'notebook/js/celltoolbarpresets/slideshow',
23 'notebook/js/scrollmanager'
23 'notebook/js/scrollmanager'
24 ], function (
24 ], function (
25 IPython,
25 IPython,
26 $,
26 $,
27 utils,
27 utils,
28 dialog,
28 dialog,
29 cellmod,
29 cellmod,
30 textcell,
30 textcell,
31 codecell,
31 codecell,
32 session,
32 session,
33 celltoolbar,
33 celltoolbar,
34 marked,
34 marked,
35 CodeMirror,
35 CodeMirror,
36 runMode,
36 runMode,
37 mathjaxutils,
37 mathjaxutils,
38 keyboard,
38 keyboard,
39 tooltip,
39 tooltip,
40 default_celltoolbar,
40 default_celltoolbar,
41 rawcell_celltoolbar,
41 rawcell_celltoolbar,
42 slideshow_celltoolbar,
42 slideshow_celltoolbar,
43 scrollmanager
43 scrollmanager
44 ) {
44 ) {
45 "use strict";
45 "use strict";
46
46
47 var Notebook = function (selector, options) {
47 var Notebook = function (selector, options) {
48 /**
48 /**
49 * Constructor
49 * Constructor
50 *
50 *
51 * A notebook contains and manages cells.
51 * A notebook contains and manages cells.
52 *
52 *
53 * Parameters:
53 * Parameters:
54 * selector: string
54 * selector: string
55 * options: dictionary
55 * options: dictionary
56 * Dictionary of keyword arguments.
56 * Dictionary of keyword arguments.
57 * events: $(Events) instance
57 * events: $(Events) instance
58 * keyboard_manager: KeyboardManager instance
58 * keyboard_manager: KeyboardManager instance
59 * contents: Contents instance
59 * contents: Contents instance
60 * save_widget: SaveWidget instance
60 * save_widget: SaveWidget instance
61 * config: dictionary
61 * config: dictionary
62 * base_url : string
62 * base_url : string
63 * notebook_path : string
63 * notebook_path : string
64 * notebook_name : string
64 * notebook_name : string
65 */
65 */
66 this.config = utils.mergeopt(Notebook, options.config);
66 this.config = utils.mergeopt(Notebook, options.config);
67 this.base_url = options.base_url;
67 this.base_url = options.base_url;
68 this.notebook_path = options.notebook_path;
68 this.notebook_path = options.notebook_path;
69 this.notebook_name = options.notebook_name;
69 this.notebook_name = options.notebook_name;
70 this.events = options.events;
70 this.events = options.events;
71 this.keyboard_manager = options.keyboard_manager;
71 this.keyboard_manager = options.keyboard_manager;
72 this.contents = options.contents;
72 this.contents = options.contents;
73 this.save_widget = options.save_widget;
73 this.save_widget = options.save_widget;
74 this.tooltip = new tooltip.Tooltip(this.events);
74 this.tooltip = new tooltip.Tooltip(this.events);
75 this.ws_url = options.ws_url;
75 this.ws_url = options.ws_url;
76 this._session_starting = false;
76 this._session_starting = false;
77 this.default_cell_type = this.config.default_cell_type || 'code';
77 this.default_cell_type = this.config.default_cell_type || 'code';
78
78
79 // Create default scroll manager.
79 // Create default scroll manager.
80 this.scroll_manager = new scrollmanager.ScrollManager(this);
80 this.scroll_manager = new scrollmanager.ScrollManager(this);
81
81
82 // TODO: This code smells (and the other `= this` line a couple lines down)
82 // TODO: This code smells (and the other `= this` line a couple lines down)
83 // We need a better way to deal with circular instance references.
83 // We need a better way to deal with circular instance references.
84 this.keyboard_manager.notebook = this;
84 this.keyboard_manager.notebook = this;
85 this.save_widget.notebook = this;
85 this.save_widget.notebook = this;
86
86
87 mathjaxutils.init();
87 mathjaxutils.init();
88
88
89 if (marked) {
89 if (marked) {
90 marked.setOptions({
90 marked.setOptions({
91 gfm : true,
91 gfm : true,
92 tables: true,
92 tables: true,
93 // FIXME: probably want central config for CodeMirror theme when we have js config
93 // FIXME: probably want central config for CodeMirror theme when we have js config
94 langPrefix: "cm-s-ipython language-",
94 langPrefix: "cm-s-ipython language-",
95 highlight: function(code, lang, callback) {
95 highlight: function(code, lang, callback) {
96 if (!lang) {
96 if (!lang) {
97 // no language, no highlight
97 // no language, no highlight
98 if (callback) {
98 if (callback) {
99 callback(null, code);
99 callback(null, code);
100 return;
100 return;
101 } else {
101 } else {
102 return code;
102 return code;
103 }
103 }
104 }
104 }
105 utils.requireCodeMirrorMode(lang, function (spec) {
105 utils.requireCodeMirrorMode(lang, function (spec) {
106 var el = document.createElement("div");
106 var el = document.createElement("div");
107 var mode = CodeMirror.getMode({}, spec);
107 var mode = CodeMirror.getMode({}, spec);
108 if (!mode) {
108 if (!mode) {
109 console.log("No CodeMirror mode: " + lang);
109 console.log("No CodeMirror mode: " + lang);
110 callback(null, code);
110 callback(null, code);
111 return;
111 return;
112 }
112 }
113 try {
113 try {
114 CodeMirror.runMode(code, spec, el);
114 CodeMirror.runMode(code, spec, el);
115 callback(null, el.innerHTML);
115 callback(null, el.innerHTML);
116 } catch (err) {
116 } catch (err) {
117 console.log("Failed to highlight " + lang + " code", err);
117 console.log("Failed to highlight " + lang + " code", err);
118 callback(err, code);
118 callback(err, code);
119 }
119 }
120 }, function (err) {
120 }, function (err) {
121 console.log("No CodeMirror mode: " + lang);
121 console.log("No CodeMirror mode: " + lang);
122 callback(err, code);
122 callback(err, code);
123 });
123 });
124 }
124 }
125 });
125 });
126 }
126 }
127
127
128 this.element = $(selector);
128 this.element = $(selector);
129 this.element.scroll();
129 this.element.scroll();
130 this.element.data("notebook", this);
130 this.element.data("notebook", this);
131 this.next_prompt_number = 1;
131 this.next_prompt_number = 1;
132 this.session = null;
132 this.session = null;
133 this.kernel = null;
133 this.kernel = null;
134 this.clipboard = null;
134 this.clipboard = null;
135 this.undelete_backup = null;
135 this.undelete_backup = null;
136 this.undelete_index = null;
136 this.undelete_index = null;
137 this.undelete_below = false;
137 this.undelete_below = false;
138 this.paste_enabled = false;
138 this.paste_enabled = false;
139 this.writable = false;
139 this.writable = false;
140 // It is important to start out in command mode to match the intial mode
140 // It is important to start out in command mode to match the intial mode
141 // of the KeyboardManager.
141 // of the KeyboardManager.
142 this.mode = 'command';
142 this.mode = 'command';
143 this.set_dirty(false);
143 this.set_dirty(false);
144 this.metadata = {};
144 this.metadata = {};
145 this._checkpoint_after_save = false;
145 this._checkpoint_after_save = false;
146 this.last_checkpoint = null;
146 this.last_checkpoint = null;
147 this.checkpoints = [];
147 this.checkpoints = [];
148 this.autosave_interval = 0;
148 this.autosave_interval = 0;
149 this.autosave_timer = null;
149 this.autosave_timer = null;
150 // autosave *at most* every two minutes
150 // autosave *at most* every two minutes
151 this.minimum_autosave_interval = 120000;
151 this.minimum_autosave_interval = 120000;
152 this.notebook_name_blacklist_re = /[\/\\:]/;
152 this.notebook_name_blacklist_re = /[\/\\:]/;
153 this.nbformat = 4; // Increment this when changing the nbformat
153 this.nbformat = 4; // Increment this when changing the nbformat
154 this.nbformat_minor = this.current_nbformat_minor = 0; // Increment this when changing the nbformat
154 this.nbformat_minor = this.current_nbformat_minor = 0; // Increment this when changing the nbformat
155 this.codemirror_mode = 'ipython';
155 this.codemirror_mode = 'ipython';
156 this.create_elements();
156 this.create_elements();
157 this.bind_events();
157 this.bind_events();
158 this.kernel_selector = null;
158 this.kernel_selector = null;
159 this.dirty = null;
159 this.dirty = null;
160 this.trusted = null;
160 this.trusted = null;
161 this._fully_loaded = false;
161 this._fully_loaded = false;
162
162
163 // Trigger cell toolbar registration.
163 // Trigger cell toolbar registration.
164 default_celltoolbar.register(this);
164 default_celltoolbar.register(this);
165 rawcell_celltoolbar.register(this);
165 rawcell_celltoolbar.register(this);
166 slideshow_celltoolbar.register(this);
166 slideshow_celltoolbar.register(this);
167
167
168 // prevent assign to miss-typed properties.
168 // prevent assign to miss-typed properties.
169 Object.seal(this);
169 Object.seal(this);
170 };
170 };
171
171
172 Notebook.options_default = {
172 Notebook.options_default = {
173 // can be any cell type, or the special values of
173 // can be any cell type, or the special values of
174 // 'above', 'below', or 'selected' to get the value from another cell.
174 // 'above', 'below', or 'selected' to get the value from another cell.
175 Notebook: {
175 Notebook: {
176 default_cell_type: 'code'
176 default_cell_type: 'code'
177 }
177 }
178 };
178 };
179
179
180
180
181 /**
181 /**
182 * Create an HTML and CSS representation of the notebook.
182 * Create an HTML and CSS representation of the notebook.
183 *
183 *
184 * @method create_elements
184 * @method create_elements
185 */
185 */
186 Notebook.prototype.create_elements = function () {
186 Notebook.prototype.create_elements = function () {
187 var that = this;
187 var that = this;
188 this.element.attr('tabindex','-1');
188 this.element.attr('tabindex','-1');
189 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
189 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
190 // We add this end_space div to the end of the notebook div to:
190 // We add this end_space div to the end of the notebook div to:
191 // i) provide a margin between the last cell and the end of the notebook
191 // i) provide a margin between the last cell and the end of the notebook
192 // ii) to prevent the div from scrolling up when the last cell is being
192 // ii) to prevent the div from scrolling up when the last cell is being
193 // edited, but is too low on the page, which browsers will do automatically.
193 // edited, but is too low on the page, which browsers will do automatically.
194 var end_space = $('<div/>').addClass('end_space');
194 var end_space = $('<div/>').addClass('end_space');
195 end_space.dblclick(function (e) {
195 end_space.dblclick(function (e) {
196 var ncells = that.ncells();
196 var ncells = that.ncells();
197 that.insert_cell_below('code',ncells-1);
197 that.insert_cell_below('code',ncells-1);
198 });
198 });
199 this.element.append(this.container);
199 this.element.append(this.container);
200 this.container.append(end_space);
200 this.container.append(end_space);
201 };
201 };
202
202
203 /**
203 /**
204 * Bind JavaScript events: key presses and custom IPython events.
204 * Bind JavaScript events: key presses and custom IPython events.
205 *
205 *
206 * @method bind_events
206 * @method bind_events
207 */
207 */
208 Notebook.prototype.bind_events = function () {
208 Notebook.prototype.bind_events = function () {
209 var that = this;
209 var that = this;
210
210
211 this.events.on('set_next_input.Notebook', function (event, data) {
211 this.events.on('set_next_input.Notebook', function (event, data) {
212 if (data.replace) {
212 if (data.replace) {
213 data.cell.set_text(data.text);
213 data.cell.set_text(data.text);
214 data.cell.clear_output();
214 data.cell.clear_output();
215 } else {
215 } else {
216 var index = that.find_cell_index(data.cell);
216 var index = that.find_cell_index(data.cell);
217 var new_cell = that.insert_cell_below('code',index);
217 var new_cell = that.insert_cell_below('code',index);
218 new_cell.set_text(data.text);
218 new_cell.set_text(data.text);
219 }
219 }
220 that.dirty = true;
220 that.dirty = true;
221 });
221 });
222
222
223 this.events.on('unrecognized_cell.Cell', function () {
223 this.events.on('unrecognized_cell.Cell', function () {
224 that.warn_nbformat_minor();
224 that.warn_nbformat_minor();
225 });
225 });
226
226
227 this.events.on('unrecognized_output.OutputArea', function () {
227 this.events.on('unrecognized_output.OutputArea', function () {
228 that.warn_nbformat_minor();
228 that.warn_nbformat_minor();
229 });
229 });
230
230
231 this.events.on('set_dirty.Notebook', function (event, data) {
231 this.events.on('set_dirty.Notebook', function (event, data) {
232 that.dirty = data.value;
232 that.dirty = data.value;
233 });
233 });
234
234
235 this.events.on('trust_changed.Notebook', function (event, trusted) {
235 this.events.on('trust_changed.Notebook', function (event, trusted) {
236 that.trusted = trusted;
236 that.trusted = trusted;
237 });
237 });
238
238
239 this.events.on('select.Cell', function (event, data) {
239 this.events.on('select.Cell', function (event, data) {
240 var index = that.find_cell_index(data.cell);
240 var index = that.find_cell_index(data.cell);
241 that.select(index);
241 that.select(index);
242 });
242 });
243
243
244 this.events.on('edit_mode.Cell', function (event, data) {
244 this.events.on('edit_mode.Cell', function (event, data) {
245 that.handle_edit_mode(data.cell);
245 that.handle_edit_mode(data.cell);
246 });
246 });
247
247
248 this.events.on('command_mode.Cell', function (event, data) {
248 this.events.on('command_mode.Cell', function (event, data) {
249 that.handle_command_mode(data.cell);
249 that.handle_command_mode(data.cell);
250 });
250 });
251
251
252 this.events.on('spec_changed.Kernel', function(event, data) {
252 this.events.on('spec_changed.Kernel', function(event, data) {
253 that.metadata.kernelspec =
253 that.metadata.kernelspec =
254 {name: data.name, display_name: data.display_name};
254 {name: data.name, display_name: data.display_name};
255 });
255 });
256
256
257 this.events.on('kernel_ready.Kernel', function(event, data) {
257 this.events.on('kernel_ready.Kernel', function(event, data) {
258 var kinfo = data.kernel.info_reply;
258 var kinfo = data.kernel.info_reply;
259 var langinfo = kinfo.language_info || {};
259 var langinfo = kinfo.language_info || {};
260 that.metadata.language_info = langinfo;
260 that.metadata.language_info = langinfo;
261 // Mode 'null' should be plain, unhighlighted text.
261 // Mode 'null' should be plain, unhighlighted text.
262 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
262 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
263 that.set_codemirror_mode(cm_mode);
263 that.set_codemirror_mode(cm_mode);
264 });
264 });
265
265
266 var collapse_time = function (time) {
266 var collapse_time = function (time) {
267 var app_height = $('#ipython-main-app').height(); // content height
267 var app_height = $('#ipython-main-app').height(); // content height
268 var splitter_height = $('div#pager_splitter').outerHeight(true);
268 var splitter_height = $('div#pager_splitter').outerHeight(true);
269 var new_height = app_height - splitter_height;
269 var new_height = app_height - splitter_height;
270 that.element.animate({height : new_height + 'px'}, time);
270 that.element.animate({height : new_height + 'px'}, time);
271 };
271 };
272
272
273 this.element.bind('collapse_pager', function (event, extrap) {
273 this.element.bind('collapse_pager', function (event, extrap) {
274 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
274 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
275 collapse_time(time);
275 collapse_time(time);
276 });
276 });
277
277
278 var expand_time = function (time) {
278 var expand_time = function (time) {
279 var app_height = $('#ipython-main-app').height(); // content height
279 var app_height = $('#ipython-main-app').height(); // content height
280 var splitter_height = $('div#pager_splitter').outerHeight(true);
280 var splitter_height = $('div#pager_splitter').outerHeight(true);
281 var pager_height = $('div#pager').outerHeight(true);
281 var pager_height = $('div#pager').outerHeight(true);
282 var new_height = app_height - pager_height - splitter_height;
282 var new_height = app_height - pager_height - splitter_height;
283 that.element.animate({height : new_height + 'px'}, time);
283 that.element.animate({height : new_height + 'px'}, time);
284 };
284 };
285
285
286 this.element.bind('expand_pager', function (event, extrap) {
286 this.element.bind('expand_pager', function (event, extrap) {
287 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
287 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
288 expand_time(time);
288 expand_time(time);
289 });
289 });
290
290
291 // Firefox 22 broke $(window).on("beforeunload")
291 // Firefox 22 broke $(window).on("beforeunload")
292 // I'm not sure why or how.
292 // I'm not sure why or how.
293 window.onbeforeunload = function (e) {
293 window.onbeforeunload = function (e) {
294 // TODO: Make killing the kernel configurable.
294 // TODO: Make killing the kernel configurable.
295 var kill_kernel = false;
295 var kill_kernel = false;
296 if (kill_kernel) {
296 if (kill_kernel) {
297 that.session.delete();
297 that.session.delete();
298 }
298 }
299 // if we are autosaving, trigger an autosave on nav-away.
299 // if we are autosaving, trigger an autosave on nav-away.
300 // still warn, because if we don't the autosave may fail.
300 // still warn, because if we don't the autosave may fail.
301 if (that.dirty) {
301 if (that.dirty) {
302 if ( that.autosave_interval ) {
302 if ( that.autosave_interval ) {
303 // schedule autosave in a timeout
303 // schedule autosave in a timeout
304 // this gives you a chance to forcefully discard changes
304 // this gives you a chance to forcefully discard changes
305 // by reloading the page if you *really* want to.
305 // by reloading the page if you *really* want to.
306 // the timer doesn't start until you *dismiss* the dialog.
306 // the timer doesn't start until you *dismiss* the dialog.
307 setTimeout(function () {
307 setTimeout(function () {
308 if (that.dirty) {
308 if (that.dirty) {
309 that.save_notebook();
309 that.save_notebook();
310 }
310 }
311 }, 1000);
311 }, 1000);
312 return "Autosave in progress, latest changes may be lost.";
312 return "Autosave in progress, latest changes may be lost.";
313 } else {
313 } else {
314 return "Unsaved changes will be lost.";
314 return "Unsaved changes will be lost.";
315 }
315 }
316 }
316 }
317 // Null is the *only* return value that will make the browser not
317 // Null is the *only* return value that will make the browser not
318 // pop up the "don't leave" dialog.
318 // pop up the "don't leave" dialog.
319 return null;
319 return null;
320 };
320 };
321 };
321 };
322
322
323 Notebook.prototype.warn_nbformat_minor = function (event) {
323 Notebook.prototype.warn_nbformat_minor = function (event) {
324 /**
324 /**
325 * trigger a warning dialog about missing functionality from newer minor versions
325 * trigger a warning dialog about missing functionality from newer minor versions
326 */
326 */
327 var v = 'v' + this.nbformat + '.';
327 var v = 'v' + this.nbformat + '.';
328 var orig_vs = v + this.nbformat_minor;
328 var orig_vs = v + this.nbformat_minor;
329 var this_vs = v + this.current_nbformat_minor;
329 var this_vs = v + this.current_nbformat_minor;
330 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
330 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
331 this_vs + ". You can still work with this notebook, but cell and output types " +
331 this_vs + ". You can still work with this notebook, but cell and output types " +
332 "introduced in later notebook versions will not be available.";
332 "introduced in later notebook versions will not be available.";
333
333
334 dialog.modal({
334 dialog.modal({
335 notebook: this,
335 notebook: this,
336 keyboard_manager: this.keyboard_manager,
336 keyboard_manager: this.keyboard_manager,
337 title : "Newer Notebook",
337 title : "Newer Notebook",
338 body : msg,
338 body : msg,
339 buttons : {
339 buttons : {
340 OK : {
340 OK : {
341 "class" : "btn-danger"
341 "class" : "btn-danger"
342 }
342 }
343 }
343 }
344 });
344 });
345 }
345 }
346
346
347 /**
347 /**
348 * Set the dirty flag, and trigger the set_dirty.Notebook event
348 * Set the dirty flag, and trigger the set_dirty.Notebook event
349 *
349 *
350 * @method set_dirty
350 * @method set_dirty
351 */
351 */
352 Notebook.prototype.set_dirty = function (value) {
352 Notebook.prototype.set_dirty = function (value) {
353 if (value === undefined) {
353 if (value === undefined) {
354 value = true;
354 value = true;
355 }
355 }
356 if (this.dirty == value) {
356 if (this.dirty == value) {
357 return;
357 return;
358 }
358 }
359 this.events.trigger('set_dirty.Notebook', {value: value});
359 this.events.trigger('set_dirty.Notebook', {value: value});
360 };
360 };
361
361
362 /**
362 /**
363 * Scroll the top of the page to a given cell.
363 * Scroll the top of the page to a given cell.
364 *
364 *
365 * @method scroll_to_cell
365 * @method scroll_to_cell
366 * @param {Number} cell_number An index of the cell to view
366 * @param {Number} cell_number An index of the cell to view
367 * @param {Number} time Animation time in milliseconds
367 * @param {Number} time Animation time in milliseconds
368 * @return {Number} Pixel offset from the top of the container
368 * @return {Number} Pixel offset from the top of the container
369 */
369 */
370 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
370 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
371 var cells = this.get_cells();
371 var cells = this.get_cells();
372 time = time || 0;
372 time = time || 0;
373 cell_number = Math.min(cells.length-1,cell_number);
373 cell_number = Math.min(cells.length-1,cell_number);
374 cell_number = Math.max(0 ,cell_number);
374 cell_number = Math.max(0 ,cell_number);
375 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
375 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
376 this.element.animate({scrollTop:scroll_value}, time);
376 this.element.animate({scrollTop:scroll_value}, time);
377 return scroll_value;
377 return scroll_value;
378 };
378 };
379
379
380 /**
380 /**
381 * Scroll to the bottom of the page.
381 * Scroll to the bottom of the page.
382 *
382 *
383 * @method scroll_to_bottom
383 * @method scroll_to_bottom
384 */
384 */
385 Notebook.prototype.scroll_to_bottom = function () {
385 Notebook.prototype.scroll_to_bottom = function () {
386 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
386 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
387 };
387 };
388
388
389 /**
389 /**
390 * Scroll to the top of the page.
390 * Scroll to the top of the page.
391 *
391 *
392 * @method scroll_to_top
392 * @method scroll_to_top
393 */
393 */
394 Notebook.prototype.scroll_to_top = function () {
394 Notebook.prototype.scroll_to_top = function () {
395 this.element.animate({scrollTop:0}, 0);
395 this.element.animate({scrollTop:0}, 0);
396 };
396 };
397
397
398 // Edit Notebook metadata
398 // Edit Notebook metadata
399
399
400 Notebook.prototype.edit_metadata = function () {
400 Notebook.prototype.edit_metadata = function () {
401 var that = this;
401 var that = this;
402 dialog.edit_metadata({
402 dialog.edit_metadata({
403 md: this.metadata,
403 md: this.metadata,
404 callback: function (md) {
404 callback: function (md) {
405 that.metadata = md;
405 that.metadata = md;
406 },
406 },
407 name: 'Notebook',
407 name: 'Notebook',
408 notebook: this,
408 notebook: this,
409 keyboard_manager: this.keyboard_manager});
409 keyboard_manager: this.keyboard_manager});
410 };
410 };
411
411
412 // Cell indexing, retrieval, etc.
412 // Cell indexing, retrieval, etc.
413
413
414 /**
414 /**
415 * Get all cell elements in the notebook.
415 * Get all cell elements in the notebook.
416 *
416 *
417 * @method get_cell_elements
417 * @method get_cell_elements
418 * @return {jQuery} A selector of all cell elements
418 * @return {jQuery} A selector of all cell elements
419 */
419 */
420 Notebook.prototype.get_cell_elements = function () {
420 Notebook.prototype.get_cell_elements = function () {
421 return this.container.find(".cell").not('.cell .cell');
421 return this.container.find(".cell").not('.cell .cell');
422 };
422 };
423
423
424 /**
424 /**
425 * Get a particular cell element.
425 * Get a particular cell element.
426 *
426 *
427 * @method get_cell_element
427 * @method get_cell_element
428 * @param {Number} index An index of a cell to select
428 * @param {Number} index An index of a cell to select
429 * @return {jQuery} A selector of the given cell.
429 * @return {jQuery} A selector of the given cell.
430 */
430 */
431 Notebook.prototype.get_cell_element = function (index) {
431 Notebook.prototype.get_cell_element = function (index) {
432 var result = null;
432 var result = null;
433 var e = this.get_cell_elements().eq(index);
433 var e = this.get_cell_elements().eq(index);
434 if (e.length !== 0) {
434 if (e.length !== 0) {
435 result = e;
435 result = e;
436 }
436 }
437 return result;
437 return result;
438 };
438 };
439
439
440 /**
440 /**
441 * Try to get a particular cell by msg_id.
441 * Try to get a particular cell by msg_id.
442 *
442 *
443 * @method get_msg_cell
443 * @method get_msg_cell
444 * @param {String} msg_id A message UUID
444 * @param {String} msg_id A message UUID
445 * @return {Cell} Cell or null if no cell was found.
445 * @return {Cell} Cell or null if no cell was found.
446 */
446 */
447 Notebook.prototype.get_msg_cell = function (msg_id) {
447 Notebook.prototype.get_msg_cell = function (msg_id) {
448 return codecell.CodeCell.msg_cells[msg_id] || null;
448 return codecell.CodeCell.msg_cells[msg_id] || null;
449 };
449 };
450
450
451 /**
451 /**
452 * Count the cells in this notebook.
452 * Count the cells in this notebook.
453 *
453 *
454 * @method ncells
454 * @method ncells
455 * @return {Number} The number of cells in this notebook
455 * @return {Number} The number of cells in this notebook
456 */
456 */
457 Notebook.prototype.ncells = function () {
457 Notebook.prototype.ncells = function () {
458 return this.get_cell_elements().length;
458 return this.get_cell_elements().length;
459 };
459 };
460
460
461 /**
461 /**
462 * Get all Cell objects in this notebook.
462 * Get all Cell objects in this notebook.
463 *
463 *
464 * @method get_cells
464 * @method get_cells
465 * @return {Array} This notebook's Cell objects
465 * @return {Array} This notebook's Cell objects
466 */
466 */
467 // TODO: we are often calling cells as cells()[i], which we should optimize
467 // TODO: we are often calling cells as cells()[i], which we should optimize
468 // to cells(i) or a new method.
468 // to cells(i) or a new method.
469 Notebook.prototype.get_cells = function () {
469 Notebook.prototype.get_cells = function () {
470 return this.get_cell_elements().toArray().map(function (e) {
470 return this.get_cell_elements().toArray().map(function (e) {
471 return $(e).data("cell");
471 return $(e).data("cell");
472 });
472 });
473 };
473 };
474
474
475 /**
475 /**
476 * Get a Cell object from this notebook.
476 * Get a Cell object from this notebook.
477 *
477 *
478 * @method get_cell
478 * @method get_cell
479 * @param {Number} index An index of a cell to retrieve
479 * @param {Number} index An index of a cell to retrieve
480 * @return {Cell} Cell or null if no cell was found.
480 * @return {Cell} Cell or null if no cell was found.
481 */
481 */
482 Notebook.prototype.get_cell = function (index) {
482 Notebook.prototype.get_cell = function (index) {
483 var result = null;
483 var result = null;
484 var ce = this.get_cell_element(index);
484 var ce = this.get_cell_element(index);
485 if (ce !== null) {
485 if (ce !== null) {
486 result = ce.data('cell');
486 result = ce.data('cell');
487 }
487 }
488 return result;
488 return result;
489 };
489 };
490
490
491 /**
491 /**
492 * Get the cell below a given cell.
492 * Get the cell below a given cell.
493 *
493 *
494 * @method get_next_cell
494 * @method get_next_cell
495 * @param {Cell} cell The provided cell
495 * @param {Cell} cell The provided cell
496 * @return {Cell} the next cell or null if no cell was found.
496 * @return {Cell} the next cell or null if no cell was found.
497 */
497 */
498 Notebook.prototype.get_next_cell = function (cell) {
498 Notebook.prototype.get_next_cell = function (cell) {
499 var result = null;
499 var result = null;
500 var index = this.find_cell_index(cell);
500 var index = this.find_cell_index(cell);
501 if (this.is_valid_cell_index(index+1)) {
501 if (this.is_valid_cell_index(index+1)) {
502 result = this.get_cell(index+1);
502 result = this.get_cell(index+1);
503 }
503 }
504 return result;
504 return result;
505 };
505 };
506
506
507 /**
507 /**
508 * Get the cell above a given cell.
508 * Get the cell above a given cell.
509 *
509 *
510 * @method get_prev_cell
510 * @method get_prev_cell
511 * @param {Cell} cell The provided cell
511 * @param {Cell} cell The provided cell
512 * @return {Cell} The previous cell or null if no cell was found.
512 * @return {Cell} The previous cell or null if no cell was found.
513 */
513 */
514 Notebook.prototype.get_prev_cell = function (cell) {
514 Notebook.prototype.get_prev_cell = function (cell) {
515 var result = null;
515 var result = null;
516 var index = this.find_cell_index(cell);
516 var index = this.find_cell_index(cell);
517 if (index !== null && index > 0) {
517 if (index !== null && index > 0) {
518 result = this.get_cell(index-1);
518 result = this.get_cell(index-1);
519 }
519 }
520 return result;
520 return result;
521 };
521 };
522
522
523 /**
523 /**
524 * Get the numeric index of a given cell.
524 * Get the numeric index of a given cell.
525 *
525 *
526 * @method find_cell_index
526 * @method find_cell_index
527 * @param {Cell} cell The provided cell
527 * @param {Cell} cell The provided cell
528 * @return {Number} The cell's numeric index or null if no cell was found.
528 * @return {Number} The cell's numeric index or null if no cell was found.
529 */
529 */
530 Notebook.prototype.find_cell_index = function (cell) {
530 Notebook.prototype.find_cell_index = function (cell) {
531 var result = null;
531 var result = null;
532 this.get_cell_elements().filter(function (index) {
532 this.get_cell_elements().filter(function (index) {
533 if ($(this).data("cell") === cell) {
533 if ($(this).data("cell") === cell) {
534 result = index;
534 result = index;
535 }
535 }
536 });
536 });
537 return result;
537 return result;
538 };
538 };
539
539
540 /**
540 /**
541 * Get a given index , or the selected index if none is provided.
541 * Get a given index , or the selected index if none is provided.
542 *
542 *
543 * @method index_or_selected
543 * @method index_or_selected
544 * @param {Number} index A cell's index
544 * @param {Number} index A cell's index
545 * @return {Number} The given index, or selected index if none is provided.
545 * @return {Number} The given index, or selected index if none is provided.
546 */
546 */
547 Notebook.prototype.index_or_selected = function (index) {
547 Notebook.prototype.index_or_selected = function (index) {
548 var i;
548 var i;
549 if (index === undefined || index === null) {
549 if (index === undefined || index === null) {
550 i = this.get_selected_index();
550 i = this.get_selected_index();
551 if (i === null) {
551 if (i === null) {
552 i = 0;
552 i = 0;
553 }
553 }
554 } else {
554 } else {
555 i = index;
555 i = index;
556 }
556 }
557 return i;
557 return i;
558 };
558 };
559
559
560 /**
560 /**
561 * Get the currently selected cell.
561 * Get the currently selected cell.
562 * @method get_selected_cell
562 * @method get_selected_cell
563 * @return {Cell} The selected cell
563 * @return {Cell} The selected cell
564 */
564 */
565 Notebook.prototype.get_selected_cell = function () {
565 Notebook.prototype.get_selected_cell = function () {
566 var index = this.get_selected_index();
566 var index = this.get_selected_index();
567 return this.get_cell(index);
567 return this.get_cell(index);
568 };
568 };
569
569
570 /**
570 /**
571 * Check whether a cell index is valid.
571 * Check whether a cell index is valid.
572 *
572 *
573 * @method is_valid_cell_index
573 * @method is_valid_cell_index
574 * @param {Number} index A cell index
574 * @param {Number} index A cell index
575 * @return True if the index is valid, false otherwise
575 * @return True if the index is valid, false otherwise
576 */
576 */
577 Notebook.prototype.is_valid_cell_index = function (index) {
577 Notebook.prototype.is_valid_cell_index = function (index) {
578 if (index !== null && index >= 0 && index < this.ncells()) {
578 if (index !== null && index >= 0 && index < this.ncells()) {
579 return true;
579 return true;
580 } else {
580 } else {
581 return false;
581 return false;
582 }
582 }
583 };
583 };
584
584
585 /**
585 /**
586 * Get the index of the currently selected cell.
586 * Get the index of the currently selected cell.
587
587
588 * @method get_selected_index
588 * @method get_selected_index
589 * @return {Number} The selected cell's numeric index
589 * @return {Number} The selected cell's numeric index
590 */
590 */
591 Notebook.prototype.get_selected_index = function () {
591 Notebook.prototype.get_selected_index = function () {
592 var result = null;
592 var result = null;
593 this.get_cell_elements().filter(function (index) {
593 this.get_cell_elements().filter(function (index) {
594 if ($(this).data("cell").selected === true) {
594 if ($(this).data("cell").selected === true) {
595 result = index;
595 result = index;
596 }
596 }
597 });
597 });
598 return result;
598 return result;
599 };
599 };
600
600
601
601
602 // Cell selection.
602 // Cell selection.
603
603
604 /**
604 /**
605 * Programmatically select a cell.
605 * Programmatically select a cell.
606 *
606 *
607 * @method select
607 * @method select
608 * @param {Number} index A cell's index
608 * @param {Number} index A cell's index
609 * @return {Notebook} This notebook
609 * @return {Notebook} This notebook
610 */
610 */
611 Notebook.prototype.select = function (index) {
611 Notebook.prototype.select = function (index) {
612 if (this.is_valid_cell_index(index)) {
612 if (this.is_valid_cell_index(index)) {
613 var sindex = this.get_selected_index();
613 var sindex = this.get_selected_index();
614 if (sindex !== null && index !== sindex) {
614 if (sindex !== null && index !== sindex) {
615 // If we are about to select a different cell, make sure we are
615 // If we are about to select a different cell, make sure we are
616 // first in command mode.
616 // first in command mode.
617 if (this.mode !== 'command') {
617 if (this.mode !== 'command') {
618 this.command_mode();
618 this.command_mode();
619 }
619 }
620 this.get_cell(sindex).unselect();
620 this.get_cell(sindex).unselect();
621 }
621 }
622 var cell = this.get_cell(index);
622 var cell = this.get_cell(index);
623 cell.select();
623 cell.select();
624 if (cell.cell_type === 'heading') {
624 if (cell.cell_type === 'heading') {
625 this.events.trigger('selected_cell_type_changed.Notebook',
625 this.events.trigger('selected_cell_type_changed.Notebook',
626 {'cell_type':cell.cell_type,level:cell.level}
626 {'cell_type':cell.cell_type,level:cell.level}
627 );
627 );
628 } else {
628 } else {
629 this.events.trigger('selected_cell_type_changed.Notebook',
629 this.events.trigger('selected_cell_type_changed.Notebook',
630 {'cell_type':cell.cell_type}
630 {'cell_type':cell.cell_type}
631 );
631 );
632 }
632 }
633 }
633 }
634 return this;
634 return this;
635 };
635 };
636
636
637 /**
637 /**
638 * Programmatically select the next cell.
638 * Programmatically select the next cell.
639 *
639 *
640 * @method select_next
640 * @method select_next
641 * @return {Notebook} This notebook
641 * @return {Notebook} This notebook
642 */
642 */
643 Notebook.prototype.select_next = function () {
643 Notebook.prototype.select_next = function () {
644 var index = this.get_selected_index();
644 var index = this.get_selected_index();
645 this.select(index+1);
645 this.select(index+1);
646 return this;
646 return this;
647 };
647 };
648
648
649 /**
649 /**
650 * Programmatically select the previous cell.
650 * Programmatically select the previous cell.
651 *
651 *
652 * @method select_prev
652 * @method select_prev
653 * @return {Notebook} This notebook
653 * @return {Notebook} This notebook
654 */
654 */
655 Notebook.prototype.select_prev = function () {
655 Notebook.prototype.select_prev = function () {
656 var index = this.get_selected_index();
656 var index = this.get_selected_index();
657 this.select(index-1);
657 this.select(index-1);
658 return this;
658 return this;
659 };
659 };
660
660
661
661
662 // Edit/Command mode
662 // Edit/Command mode
663
663
664 /**
664 /**
665 * Gets the index of the cell that is in edit mode.
665 * Gets the index of the cell that is in edit mode.
666 *
666 *
667 * @method get_edit_index
667 * @method get_edit_index
668 *
668 *
669 * @return index {int}
669 * @return index {int}
670 **/
670 **/
671 Notebook.prototype.get_edit_index = function () {
671 Notebook.prototype.get_edit_index = function () {
672 var result = null;
672 var result = null;
673 this.get_cell_elements().filter(function (index) {
673 this.get_cell_elements().filter(function (index) {
674 if ($(this).data("cell").mode === 'edit') {
674 if ($(this).data("cell").mode === 'edit') {
675 result = index;
675 result = index;
676 }
676 }
677 });
677 });
678 return result;
678 return result;
679 };
679 };
680
680
681 /**
681 /**
682 * Handle when a a cell blurs and the notebook should enter command mode.
682 * Handle when a a cell blurs and the notebook should enter command mode.
683 *
683 *
684 * @method handle_command_mode
684 * @method handle_command_mode
685 * @param [cell] {Cell} Cell to enter command mode on.
685 * @param [cell] {Cell} Cell to enter command mode on.
686 **/
686 **/
687 Notebook.prototype.handle_command_mode = function (cell) {
687 Notebook.prototype.handle_command_mode = function (cell) {
688 if (this.mode !== 'command') {
688 if (this.mode !== 'command') {
689 cell.command_mode();
689 cell.command_mode();
690 this.mode = 'command';
690 this.mode = 'command';
691 this.events.trigger('command_mode.Notebook');
691 this.events.trigger('command_mode.Notebook');
692 this.keyboard_manager.command_mode();
692 this.keyboard_manager.command_mode();
693 }
693 }
694 };
694 };
695
695
696 /**
696 /**
697 * Make the notebook enter command mode.
697 * Make the notebook enter command mode.
698 *
698 *
699 * @method command_mode
699 * @method command_mode
700 **/
700 **/
701 Notebook.prototype.command_mode = function () {
701 Notebook.prototype.command_mode = function () {
702 var cell = this.get_cell(this.get_edit_index());
702 var cell = this.get_cell(this.get_edit_index());
703 if (cell && this.mode !== 'command') {
703 if (cell && this.mode !== 'command') {
704 // We don't call cell.command_mode, but rather call cell.focus_cell()
704 // We don't call cell.command_mode, but rather call cell.focus_cell()
705 // which will blur and CM editor and trigger the call to
705 // which will blur and CM editor and trigger the call to
706 // handle_command_mode.
706 // handle_command_mode.
707 cell.focus_cell();
707 cell.focus_cell();
708 }
708 }
709 };
709 };
710
710
711 /**
711 /**
712 * Handle when a cell fires it's edit_mode event.
712 * Handle when a cell fires it's edit_mode event.
713 *
713 *
714 * @method handle_edit_mode
714 * @method handle_edit_mode
715 * @param [cell] {Cell} Cell to enter edit mode on.
715 * @param [cell] {Cell} Cell to enter edit mode on.
716 **/
716 **/
717 Notebook.prototype.handle_edit_mode = function (cell) {
717 Notebook.prototype.handle_edit_mode = function (cell) {
718 if (cell && this.mode !== 'edit') {
718 if (cell && this.mode !== 'edit') {
719 cell.edit_mode();
719 cell.edit_mode();
720 this.mode = 'edit';
720 this.mode = 'edit';
721 this.events.trigger('edit_mode.Notebook');
721 this.events.trigger('edit_mode.Notebook');
722 this.keyboard_manager.edit_mode();
722 this.keyboard_manager.edit_mode();
723 }
723 }
724 };
724 };
725
725
726 /**
726 /**
727 * Make a cell enter edit mode.
727 * Make a cell enter edit mode.
728 *
728 *
729 * @method edit_mode
729 * @method edit_mode
730 **/
730 **/
731 Notebook.prototype.edit_mode = function () {
731 Notebook.prototype.edit_mode = function () {
732 var cell = this.get_selected_cell();
732 var cell = this.get_selected_cell();
733 if (cell && this.mode !== 'edit') {
733 if (cell && this.mode !== 'edit') {
734 cell.unrender();
734 cell.unrender();
735 cell.focus_editor();
735 cell.focus_editor();
736 }
736 }
737 };
737 };
738
738
739 /**
739 /**
740 * Focus the currently selected cell.
740 * Focus the currently selected cell.
741 *
741 *
742 * @method focus_cell
742 * @method focus_cell
743 **/
743 **/
744 Notebook.prototype.focus_cell = function () {
744 Notebook.prototype.focus_cell = function () {
745 var cell = this.get_selected_cell();
745 var cell = this.get_selected_cell();
746 if (cell === null) {return;} // No cell is selected
746 if (cell === null) {return;} // No cell is selected
747 cell.focus_cell();
747 cell.focus_cell();
748 };
748 };
749
749
750 // Cell movement
750 // Cell movement
751
751
752 /**
752 /**
753 * Move given (or selected) cell up and select it.
753 * Move given (or selected) cell up and select it.
754 *
754 *
755 * @method move_cell_up
755 * @method move_cell_up
756 * @param [index] {integer} cell index
756 * @param [index] {integer} cell index
757 * @return {Notebook} This notebook
757 * @return {Notebook} This notebook
758 **/
758 **/
759 Notebook.prototype.move_cell_up = function (index) {
759 Notebook.prototype.move_cell_up = function (index) {
760 var i = this.index_or_selected(index);
760 var i = this.index_or_selected(index);
761 if (this.is_valid_cell_index(i) && i > 0) {
761 if (this.is_valid_cell_index(i) && i > 0) {
762 var pivot = this.get_cell_element(i-1);
762 var pivot = this.get_cell_element(i-1);
763 var tomove = this.get_cell_element(i);
763 var tomove = this.get_cell_element(i);
764 if (pivot !== null && tomove !== null) {
764 if (pivot !== null && tomove !== null) {
765 tomove.detach();
765 tomove.detach();
766 pivot.before(tomove);
766 pivot.before(tomove);
767 this.select(i-1);
767 this.select(i-1);
768 var cell = this.get_selected_cell();
768 var cell = this.get_selected_cell();
769 cell.focus_cell();
769 cell.focus_cell();
770 }
770 }
771 this.set_dirty(true);
771 this.set_dirty(true);
772 }
772 }
773 return this;
773 return this;
774 };
774 };
775
775
776
776
777 /**
777 /**
778 * Move given (or selected) cell down and select it
778 * Move given (or selected) cell down and select it
779 *
779 *
780 * @method move_cell_down
780 * @method move_cell_down
781 * @param [index] {integer} cell index
781 * @param [index] {integer} cell index
782 * @return {Notebook} This notebook
782 * @return {Notebook} This notebook
783 **/
783 **/
784 Notebook.prototype.move_cell_down = function (index) {
784 Notebook.prototype.move_cell_down = function (index) {
785 var i = this.index_or_selected(index);
785 var i = this.index_or_selected(index);
786 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
786 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
787 var pivot = this.get_cell_element(i+1);
787 var pivot = this.get_cell_element(i+1);
788 var tomove = this.get_cell_element(i);
788 var tomove = this.get_cell_element(i);
789 if (pivot !== null && tomove !== null) {
789 if (pivot !== null && tomove !== null) {
790 tomove.detach();
790 tomove.detach();
791 pivot.after(tomove);
791 pivot.after(tomove);
792 this.select(i+1);
792 this.select(i+1);
793 var cell = this.get_selected_cell();
793 var cell = this.get_selected_cell();
794 cell.focus_cell();
794 cell.focus_cell();
795 }
795 }
796 }
796 }
797 this.set_dirty();
797 this.set_dirty();
798 return this;
798 return this;
799 };
799 };
800
800
801
801
802 // Insertion, deletion.
802 // Insertion, deletion.
803
803
804 /**
804 /**
805 * Delete a cell from the notebook.
805 * Delete a cell from the notebook.
806 *
806 *
807 * @method delete_cell
807 * @method delete_cell
808 * @param [index] A cell's numeric index
808 * @param [index] A cell's numeric index
809 * @return {Notebook} This notebook
809 * @return {Notebook} This notebook
810 */
810 */
811 Notebook.prototype.delete_cell = function (index) {
811 Notebook.prototype.delete_cell = function (index) {
812 var i = this.index_or_selected(index);
812 var i = this.index_or_selected(index);
813 var cell = this.get_cell(i);
813 var cell = this.get_cell(i);
814 if (!cell.is_deletable()) {
814 if (!cell.is_deletable()) {
815 return this;
815 return this;
816 }
816 }
817
817
818 this.undelete_backup = cell.toJSON();
818 this.undelete_backup = cell.toJSON();
819 $('#undelete_cell').removeClass('disabled');
819 $('#undelete_cell').removeClass('disabled');
820 if (this.is_valid_cell_index(i)) {
820 if (this.is_valid_cell_index(i)) {
821 var old_ncells = this.ncells();
821 var old_ncells = this.ncells();
822 var ce = this.get_cell_element(i);
822 var ce = this.get_cell_element(i);
823 ce.remove();
823 ce.remove();
824 if (i === 0) {
824 if (i === 0) {
825 // Always make sure we have at least one cell.
825 // Always make sure we have at least one cell.
826 if (old_ncells === 1) {
826 if (old_ncells === 1) {
827 this.insert_cell_below('code');
827 this.insert_cell_below('code');
828 }
828 }
829 this.select(0);
829 this.select(0);
830 this.undelete_index = 0;
830 this.undelete_index = 0;
831 this.undelete_below = false;
831 this.undelete_below = false;
832 } else if (i === old_ncells-1 && i !== 0) {
832 } else if (i === old_ncells-1 && i !== 0) {
833 this.select(i-1);
833 this.select(i-1);
834 this.undelete_index = i - 1;
834 this.undelete_index = i - 1;
835 this.undelete_below = true;
835 this.undelete_below = true;
836 } else {
836 } else {
837 this.select(i);
837 this.select(i);
838 this.undelete_index = i;
838 this.undelete_index = i;
839 this.undelete_below = false;
839 this.undelete_below = false;
840 }
840 }
841 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
841 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
842 this.set_dirty(true);
842 this.set_dirty(true);
843 }
843 }
844 return this;
844 return this;
845 };
845 };
846
846
847 /**
847 /**
848 * Restore the most recently deleted cell.
848 * Restore the most recently deleted cell.
849 *
849 *
850 * @method undelete
850 * @method undelete
851 */
851 */
852 Notebook.prototype.undelete_cell = function() {
852 Notebook.prototype.undelete_cell = function() {
853 if (this.undelete_backup !== null && this.undelete_index !== null) {
853 if (this.undelete_backup !== null && this.undelete_index !== null) {
854 var current_index = this.get_selected_index();
854 var current_index = this.get_selected_index();
855 if (this.undelete_index < current_index) {
855 if (this.undelete_index < current_index) {
856 current_index = current_index + 1;
856 current_index = current_index + 1;
857 }
857 }
858 if (this.undelete_index >= this.ncells()) {
858 if (this.undelete_index >= this.ncells()) {
859 this.select(this.ncells() - 1);
859 this.select(this.ncells() - 1);
860 }
860 }
861 else {
861 else {
862 this.select(this.undelete_index);
862 this.select(this.undelete_index);
863 }
863 }
864 var cell_data = this.undelete_backup;
864 var cell_data = this.undelete_backup;
865 var new_cell = null;
865 var new_cell = null;
866 if (this.undelete_below) {
866 if (this.undelete_below) {
867 new_cell = this.insert_cell_below(cell_data.cell_type);
867 new_cell = this.insert_cell_below(cell_data.cell_type);
868 } else {
868 } else {
869 new_cell = this.insert_cell_above(cell_data.cell_type);
869 new_cell = this.insert_cell_above(cell_data.cell_type);
870 }
870 }
871 new_cell.fromJSON(cell_data);
871 new_cell.fromJSON(cell_data);
872 if (this.undelete_below) {
872 if (this.undelete_below) {
873 this.select(current_index+1);
873 this.select(current_index+1);
874 } else {
874 } else {
875 this.select(current_index);
875 this.select(current_index);
876 }
876 }
877 this.undelete_backup = null;
877 this.undelete_backup = null;
878 this.undelete_index = null;
878 this.undelete_index = null;
879 }
879 }
880 $('#undelete_cell').addClass('disabled');
880 $('#undelete_cell').addClass('disabled');
881 };
881 };
882
882
883 /**
883 /**
884 * Insert a cell so that after insertion the cell is at given index.
884 * Insert a cell so that after insertion the cell is at given index.
885 *
885 *
886 * If cell type is not provided, it will default to the type of the
886 * If cell type is not provided, it will default to the type of the
887 * currently active cell.
887 * currently active cell.
888 *
888 *
889 * Similar to insert_above, but index parameter is mandatory
889 * Similar to insert_above, but index parameter is mandatory
890 *
890 *
891 * Index will be brought back into the accessible range [0,n]
891 * Index will be brought back into the accessible range [0,n]
892 *
892 *
893 * @method insert_cell_at_index
893 * @method insert_cell_at_index
894 * @param [type] {string} in ['code','markdown', 'raw'], defaults to 'code'
894 * @param [type] {string} in ['code','markdown', 'raw'], defaults to 'code'
895 * @param [index] {int} a valid index where to insert cell
895 * @param [index] {int} a valid index where to insert cell
896 *
896 *
897 * @return cell {cell|null} created cell or null
897 * @return cell {cell|null} created cell or null
898 **/
898 **/
899 Notebook.prototype.insert_cell_at_index = function(type, index){
899 Notebook.prototype.insert_cell_at_index = function(type, index){
900
900
901 var ncells = this.ncells();
901 var ncells = this.ncells();
902 index = Math.min(index, ncells);
902 index = Math.min(index, ncells);
903 index = Math.max(index, 0);
903 index = Math.max(index, 0);
904 var cell = null;
904 var cell = null;
905 type = type || this.default_cell_type;
905 type = type || this.default_cell_type;
906 if (type === 'above') {
906 if (type === 'above') {
907 if (index > 0) {
907 if (index > 0) {
908 type = this.get_cell(index-1).cell_type;
908 type = this.get_cell(index-1).cell_type;
909 } else {
909 } else {
910 type = 'code';
910 type = 'code';
911 }
911 }
912 } else if (type === 'below') {
912 } else if (type === 'below') {
913 if (index < ncells) {
913 if (index < ncells) {
914 type = this.get_cell(index).cell_type;
914 type = this.get_cell(index).cell_type;
915 } else {
915 } else {
916 type = 'code';
916 type = 'code';
917 }
917 }
918 } else if (type === 'selected') {
918 } else if (type === 'selected') {
919 type = this.get_selected_cell().cell_type;
919 type = this.get_selected_cell().cell_type;
920 }
920 }
921
921
922 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
922 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
923 var cell_options = {
923 var cell_options = {
924 events: this.events,
924 events: this.events,
925 config: this.config,
925 config: this.config,
926 keyboard_manager: this.keyboard_manager,
926 keyboard_manager: this.keyboard_manager,
927 notebook: this,
927 notebook: this,
928 tooltip: this.tooltip
928 tooltip: this.tooltip
929 };
929 };
930 switch(type) {
930 switch(type) {
931 case 'code':
931 case 'code':
932 cell = new codecell.CodeCell(this.kernel, cell_options);
932 cell = new codecell.CodeCell(this.kernel, cell_options);
933 cell.set_input_prompt();
933 cell.set_input_prompt();
934 break;
934 break;
935 case 'markdown':
935 case 'markdown':
936 cell = new textcell.MarkdownCell(cell_options);
936 cell = new textcell.MarkdownCell(cell_options);
937 break;
937 break;
938 case 'raw':
938 case 'raw':
939 cell = new textcell.RawCell(cell_options);
939 cell = new textcell.RawCell(cell_options);
940 break;
940 break;
941 default:
941 default:
942 console.log("Unrecognized cell type: ", type, cellmod);
942 console.log("Unrecognized cell type: ", type, cellmod);
943 cell = new cellmod.UnrecognizedCell(cell_options);
943 cell = new cellmod.UnrecognizedCell(cell_options);
944 }
944 }
945
945
946 if(this._insert_element_at_index(cell.element,index)) {
946 if(this._insert_element_at_index(cell.element,index)) {
947 cell.render();
947 cell.render();
948 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
948 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
949 cell.refresh();
949 cell.refresh();
950 // We used to select the cell after we refresh it, but there
950 // We used to select the cell after we refresh it, but there
951 // are now cases were this method is called where select is
951 // are now cases were this method is called where select is
952 // not appropriate. The selection logic should be handled by the
952 // not appropriate. The selection logic should be handled by the
953 // caller of the the top level insert_cell methods.
953 // caller of the the top level insert_cell methods.
954 this.set_dirty(true);
954 this.set_dirty(true);
955 }
955 }
956 }
956 }
957 return cell;
957 return cell;
958
958
959 };
959 };
960
960
961 /**
961 /**
962 * Insert an element at given cell index.
962 * Insert an element at given cell index.
963 *
963 *
964 * @method _insert_element_at_index
964 * @method _insert_element_at_index
965 * @param element {dom_element} a cell element
965 * @param element {dom_element} a cell element
966 * @param [index] {int} a valid index where to inser cell
966 * @param [index] {int} a valid index where to inser cell
967 * @private
967 * @private
968 *
968 *
969 * return true if everything whent fine.
969 * return true if everything whent fine.
970 **/
970 **/
971 Notebook.prototype._insert_element_at_index = function(element, index){
971 Notebook.prototype._insert_element_at_index = function(element, index){
972 if (element === undefined){
972 if (element === undefined){
973 return false;
973 return false;
974 }
974 }
975
975
976 var ncells = this.ncells();
976 var ncells = this.ncells();
977
977
978 if (ncells === 0) {
978 if (ncells === 0) {
979 // special case append if empty
979 // special case append if empty
980 this.element.find('div.end_space').before(element);
980 this.element.find('div.end_space').before(element);
981 } else if ( ncells === index ) {
981 } else if ( ncells === index ) {
982 // special case append it the end, but not empty
982 // special case append it the end, but not empty
983 this.get_cell_element(index-1).after(element);
983 this.get_cell_element(index-1).after(element);
984 } else if (this.is_valid_cell_index(index)) {
984 } else if (this.is_valid_cell_index(index)) {
985 // otherwise always somewhere to append to
985 // otherwise always somewhere to append to
986 this.get_cell_element(index).before(element);
986 this.get_cell_element(index).before(element);
987 } else {
987 } else {
988 return false;
988 return false;
989 }
989 }
990
990
991 if (this.undelete_index !== null && index <= this.undelete_index) {
991 if (this.undelete_index !== null && index <= this.undelete_index) {
992 this.undelete_index = this.undelete_index + 1;
992 this.undelete_index = this.undelete_index + 1;
993 this.set_dirty(true);
993 this.set_dirty(true);
994 }
994 }
995 return true;
995 return true;
996 };
996 };
997
997
998 /**
998 /**
999 * Insert a cell of given type above given index, or at top
999 * Insert a cell of given type above given index, or at top
1000 * of notebook if index smaller than 0.
1000 * of notebook if index smaller than 0.
1001 *
1001 *
1002 * default index value is the one of currently selected cell
1002 * default index value is the one of currently selected cell
1003 *
1003 *
1004 * @method insert_cell_above
1004 * @method insert_cell_above
1005 * @param [type] {string} cell type
1005 * @param [type] {string} cell type
1006 * @param [index] {integer}
1006 * @param [index] {integer}
1007 *
1007 *
1008 * @return handle to created cell or null
1008 * @return handle to created cell or null
1009 **/
1009 **/
1010 Notebook.prototype.insert_cell_above = function (type, index) {
1010 Notebook.prototype.insert_cell_above = function (type, index) {
1011 index = this.index_or_selected(index);
1011 index = this.index_or_selected(index);
1012 return this.insert_cell_at_index(type, index);
1012 return this.insert_cell_at_index(type, index);
1013 };
1013 };
1014
1014
1015 /**
1015 /**
1016 * Insert a cell of given type below given index, or at bottom
1016 * Insert a cell of given type below given index, or at bottom
1017 * of notebook if index greater than number of cells
1017 * of notebook if index greater than number of cells
1018 *
1018 *
1019 * default index value is the one of currently selected cell
1019 * default index value is the one of currently selected cell
1020 *
1020 *
1021 * @method insert_cell_below
1021 * @method insert_cell_below
1022 * @param [type] {string} cell type
1022 * @param [type] {string} cell type
1023 * @param [index] {integer}
1023 * @param [index] {integer}
1024 *
1024 *
1025 * @return handle to created cell or null
1025 * @return handle to created cell or null
1026 *
1026 *
1027 **/
1027 **/
1028 Notebook.prototype.insert_cell_below = function (type, index) {
1028 Notebook.prototype.insert_cell_below = function (type, index) {
1029 index = this.index_or_selected(index);
1029 index = this.index_or_selected(index);
1030 return this.insert_cell_at_index(type, index+1);
1030 return this.insert_cell_at_index(type, index+1);
1031 };
1031 };
1032
1032
1033
1033
1034 /**
1034 /**
1035 * Insert cell at end of notebook
1035 * Insert cell at end of notebook
1036 *
1036 *
1037 * @method insert_cell_at_bottom
1037 * @method insert_cell_at_bottom
1038 * @param {String} type cell type
1038 * @param {String} type cell type
1039 *
1039 *
1040 * @return the added cell; or null
1040 * @return the added cell; or null
1041 **/
1041 **/
1042 Notebook.prototype.insert_cell_at_bottom = function (type){
1042 Notebook.prototype.insert_cell_at_bottom = function (type){
1043 var len = this.ncells();
1043 var len = this.ncells();
1044 return this.insert_cell_below(type,len-1);
1044 return this.insert_cell_below(type,len-1);
1045 };
1045 };
1046
1046
1047 /**
1047 /**
1048 * Turn a cell into a code cell.
1048 * Turn a cell into a code cell.
1049 *
1049 *
1050 * @method to_code
1050 * @method to_code
1051 * @param {Number} [index] A cell's index
1051 * @param {Number} [index] A cell's index
1052 */
1052 */
1053 Notebook.prototype.to_code = function (index) {
1053 Notebook.prototype.to_code = function (index) {
1054 var i = this.index_or_selected(index);
1054 var i = this.index_or_selected(index);
1055 if (this.is_valid_cell_index(i)) {
1055 if (this.is_valid_cell_index(i)) {
1056 var source_cell = this.get_cell(i);
1056 var source_cell = this.get_cell(i);
1057 if (!(source_cell instanceof codecell.CodeCell)) {
1057 if (!(source_cell instanceof codecell.CodeCell)) {
1058 var target_cell = this.insert_cell_below('code',i);
1058 var target_cell = this.insert_cell_below('code',i);
1059 var text = source_cell.get_text();
1059 var text = source_cell.get_text();
1060 if (text === source_cell.placeholder) {
1060 if (text === source_cell.placeholder) {
1061 text = '';
1061 text = '';
1062 }
1062 }
1063 //metadata
1063 //metadata
1064 target_cell.metadata = source_cell.metadata;
1064 target_cell.metadata = source_cell.metadata;
1065
1065
1066 target_cell.set_text(text);
1066 target_cell.set_text(text);
1067 // make this value the starting point, so that we can only undo
1067 // make this value the starting point, so that we can only undo
1068 // to this state, instead of a blank cell
1068 // to this state, instead of a blank cell
1069 target_cell.code_mirror.clearHistory();
1069 target_cell.code_mirror.clearHistory();
1070 source_cell.element.remove();
1070 source_cell.element.remove();
1071 this.select(i);
1071 this.select(i);
1072 var cursor = source_cell.code_mirror.getCursor();
1072 var cursor = source_cell.code_mirror.getCursor();
1073 target_cell.code_mirror.setCursor(cursor);
1073 target_cell.code_mirror.setCursor(cursor);
1074 this.set_dirty(true);
1074 this.set_dirty(true);
1075 }
1075 }
1076 }
1076 }
1077 };
1077 };
1078
1078
1079 /**
1079 /**
1080 * Turn a cell into a Markdown cell.
1080 * Turn a cell into a Markdown cell.
1081 *
1081 *
1082 * @method to_markdown
1082 * @method to_markdown
1083 * @param {Number} [index] A cell's index
1083 * @param {Number} [index] A cell's index
1084 */
1084 */
1085 Notebook.prototype.to_markdown = function (index) {
1085 Notebook.prototype.to_markdown = function (index) {
1086 var i = this.index_or_selected(index);
1086 var i = this.index_or_selected(index);
1087 if (this.is_valid_cell_index(i)) {
1087 if (this.is_valid_cell_index(i)) {
1088 var source_cell = this.get_cell(i);
1088 var source_cell = this.get_cell(i);
1089
1089
1090 if (!(source_cell instanceof textcell.MarkdownCell)) {
1090 if (!(source_cell instanceof textcell.MarkdownCell)) {
1091 var target_cell = this.insert_cell_below('markdown',i);
1091 var target_cell = this.insert_cell_below('markdown',i);
1092 var text = source_cell.get_text();
1092 var text = source_cell.get_text();
1093
1093
1094 if (text === source_cell.placeholder) {
1094 if (text === source_cell.placeholder) {
1095 text = '';
1095 text = '';
1096 }
1096 }
1097 // metadata
1097 // metadata
1098 target_cell.metadata = source_cell.metadata;
1098 target_cell.metadata = source_cell.metadata;
1099 // We must show the editor before setting its contents
1099 // We must show the editor before setting its contents
1100 target_cell.unrender();
1100 target_cell.unrender();
1101 target_cell.set_text(text);
1101 target_cell.set_text(text);
1102 // make this value the starting point, so that we can only undo
1102 // make this value the starting point, so that we can only undo
1103 // to this state, instead of a blank cell
1103 // to this state, instead of a blank cell
1104 target_cell.code_mirror.clearHistory();
1104 target_cell.code_mirror.clearHistory();
1105 source_cell.element.remove();
1105 source_cell.element.remove();
1106 this.select(i);
1106 this.select(i);
1107 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1107 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1108 target_cell.render();
1108 target_cell.render();
1109 }
1109 }
1110 var cursor = source_cell.code_mirror.getCursor();
1110 var cursor = source_cell.code_mirror.getCursor();
1111 target_cell.code_mirror.setCursor(cursor);
1111 target_cell.code_mirror.setCursor(cursor);
1112 this.set_dirty(true);
1112 this.set_dirty(true);
1113 }
1113 }
1114 }
1114 }
1115 };
1115 };
1116
1116
1117 /**
1117 /**
1118 * Turn a cell into a raw text cell.
1118 * Turn a cell into a raw text cell.
1119 *
1119 *
1120 * @method to_raw
1120 * @method to_raw
1121 * @param {Number} [index] A cell's index
1121 * @param {Number} [index] A cell's index
1122 */
1122 */
1123 Notebook.prototype.to_raw = function (index) {
1123 Notebook.prototype.to_raw = function (index) {
1124 var i = this.index_or_selected(index);
1124 var i = this.index_or_selected(index);
1125 if (this.is_valid_cell_index(i)) {
1125 if (this.is_valid_cell_index(i)) {
1126 var target_cell = null;
1126 var target_cell = null;
1127 var source_cell = this.get_cell(i);
1127 var source_cell = this.get_cell(i);
1128
1128
1129 if (!(source_cell instanceof textcell.RawCell)) {
1129 if (!(source_cell instanceof textcell.RawCell)) {
1130 target_cell = this.insert_cell_below('raw',i);
1130 target_cell = this.insert_cell_below('raw',i);
1131 var text = source_cell.get_text();
1131 var text = source_cell.get_text();
1132 if (text === source_cell.placeholder) {
1132 if (text === source_cell.placeholder) {
1133 text = '';
1133 text = '';
1134 }
1134 }
1135 //metadata
1135 //metadata
1136 target_cell.metadata = source_cell.metadata;
1136 target_cell.metadata = source_cell.metadata;
1137 // We must show the editor before setting its contents
1137 // We must show the editor before setting its contents
1138 target_cell.unrender();
1138 target_cell.unrender();
1139 target_cell.set_text(text);
1139 target_cell.set_text(text);
1140 // make this value the starting point, so that we can only undo
1140 // make this value the starting point, so that we can only undo
1141 // to this state, instead of a blank cell
1141 // to this state, instead of a blank cell
1142 target_cell.code_mirror.clearHistory();
1142 target_cell.code_mirror.clearHistory();
1143 source_cell.element.remove();
1143 source_cell.element.remove();
1144 this.select(i);
1144 this.select(i);
1145 var cursor = source_cell.code_mirror.getCursor();
1145 var cursor = source_cell.code_mirror.getCursor();
1146 target_cell.code_mirror.setCursor(cursor);
1146 target_cell.code_mirror.setCursor(cursor);
1147 this.set_dirty(true);
1147 this.set_dirty(true);
1148 }
1148 }
1149 }
1149 }
1150 };
1150 };
1151
1151
1152 Notebook.prototype._warn_heading = function () {
1152 Notebook.prototype._warn_heading = function () {
1153 /**
1153 /**
1154 * warn about heading cells being removed
1154 * warn about heading cells being removed
1155 */
1155 */
1156 dialog.modal({
1156 dialog.modal({
1157 notebook: this,
1157 notebook: this,
1158 keyboard_manager: this.keyboard_manager,
1158 keyboard_manager: this.keyboard_manager,
1159 title : "Use markdown headings",
1159 title : "Use markdown headings",
1160 body : $("<p/>").text(
1160 body : $("<p/>").text(
1161 'IPython no longer uses special heading cells. ' +
1161 'IPython no longer uses special heading cells. ' +
1162 'Instead, write your headings in Markdown cells using # characters:'
1162 'Instead, write your headings in Markdown cells using # characters:'
1163 ).append($('<pre/>').text(
1163 ).append($('<pre/>').text(
1164 '## This is a level 2 heading'
1164 '## This is a level 2 heading'
1165 )),
1165 )),
1166 buttons : {
1166 buttons : {
1167 "OK" : {}
1167 "OK" : {}
1168 }
1168 }
1169 });
1169 });
1170 };
1170 };
1171
1171
1172 /**
1172 /**
1173 * Turn a cell into a markdown cell with a heading.
1173 * Turn a cell into a markdown cell with a heading.
1174 *
1174 *
1175 * @method to_heading
1175 * @method to_heading
1176 * @param {Number} [index] A cell's index
1176 * @param {Number} [index] A cell's index
1177 * @param {Number} [level] A heading level (e.g., 1 for h1)
1177 * @param {Number} [level] A heading level (e.g., 1 for h1)
1178 */
1178 */
1179 Notebook.prototype.to_heading = function (index, level) {
1179 Notebook.prototype.to_heading = function (index, level) {
1180 this.to_markdown(index);
1180 this.to_markdown(index);
1181 level = level || 1;
1181 level = level || 1;
1182 var i = this.index_or_selected(index);
1182 var i = this.index_or_selected(index);
1183 if (this.is_valid_cell_index(i)) {
1183 if (this.is_valid_cell_index(i)) {
1184 var cell = this.get_cell(i);
1184 var cell = this.get_cell(i);
1185 cell.set_heading_level(level);
1185 cell.set_heading_level(level);
1186 this.set_dirty(true);
1186 this.set_dirty(true);
1187 }
1187 }
1188 };
1188 };
1189
1189
1190
1190
1191 // Cut/Copy/Paste
1191 // Cut/Copy/Paste
1192
1192
1193 /**
1193 /**
1194 * Enable UI elements for pasting cells.
1194 * Enable UI elements for pasting cells.
1195 *
1195 *
1196 * @method enable_paste
1196 * @method enable_paste
1197 */
1197 */
1198 Notebook.prototype.enable_paste = function () {
1198 Notebook.prototype.enable_paste = function () {
1199 var that = this;
1199 var that = this;
1200 if (!this.paste_enabled) {
1200 if (!this.paste_enabled) {
1201 $('#paste_cell_replace').removeClass('disabled')
1201 $('#paste_cell_replace').removeClass('disabled')
1202 .on('click', function () {that.paste_cell_replace();});
1202 .on('click', function () {that.paste_cell_replace();});
1203 $('#paste_cell_above').removeClass('disabled')
1203 $('#paste_cell_above').removeClass('disabled')
1204 .on('click', function () {that.paste_cell_above();});
1204 .on('click', function () {that.paste_cell_above();});
1205 $('#paste_cell_below').removeClass('disabled')
1205 $('#paste_cell_below').removeClass('disabled')
1206 .on('click', function () {that.paste_cell_below();});
1206 .on('click', function () {that.paste_cell_below();});
1207 this.paste_enabled = true;
1207 this.paste_enabled = true;
1208 }
1208 }
1209 };
1209 };
1210
1210
1211 /**
1211 /**
1212 * Disable UI elements for pasting cells.
1212 * Disable UI elements for pasting cells.
1213 *
1213 *
1214 * @method disable_paste
1214 * @method disable_paste
1215 */
1215 */
1216 Notebook.prototype.disable_paste = function () {
1216 Notebook.prototype.disable_paste = function () {
1217 if (this.paste_enabled) {
1217 if (this.paste_enabled) {
1218 $('#paste_cell_replace').addClass('disabled').off('click');
1218 $('#paste_cell_replace').addClass('disabled').off('click');
1219 $('#paste_cell_above').addClass('disabled').off('click');
1219 $('#paste_cell_above').addClass('disabled').off('click');
1220 $('#paste_cell_below').addClass('disabled').off('click');
1220 $('#paste_cell_below').addClass('disabled').off('click');
1221 this.paste_enabled = false;
1221 this.paste_enabled = false;
1222 }
1222 }
1223 };
1223 };
1224
1224
1225 /**
1225 /**
1226 * Cut a cell.
1226 * Cut a cell.
1227 *
1227 *
1228 * @method cut_cell
1228 * @method cut_cell
1229 */
1229 */
1230 Notebook.prototype.cut_cell = function () {
1230 Notebook.prototype.cut_cell = function () {
1231 this.copy_cell();
1231 this.copy_cell();
1232 this.delete_cell();
1232 this.delete_cell();
1233 };
1233 };
1234
1234
1235 /**
1235 /**
1236 * Copy a cell.
1236 * Copy a cell.
1237 *
1237 *
1238 * @method copy_cell
1238 * @method copy_cell
1239 */
1239 */
1240 Notebook.prototype.copy_cell = function () {
1240 Notebook.prototype.copy_cell = function () {
1241 var cell = this.get_selected_cell();
1241 var cell = this.get_selected_cell();
1242 this.clipboard = cell.toJSON();
1242 this.clipboard = cell.toJSON();
1243 // remove undeletable status from the copied cell
1243 // remove undeletable status from the copied cell
1244 if (this.clipboard.metadata.deletable !== undefined) {
1244 if (this.clipboard.metadata.deletable !== undefined) {
1245 delete this.clipboard.metadata.deletable;
1245 delete this.clipboard.metadata.deletable;
1246 }
1246 }
1247 this.enable_paste();
1247 this.enable_paste();
1248 };
1248 };
1249
1249
1250 /**
1250 /**
1251 * Replace the selected cell with a cell in the clipboard.
1251 * Replace the selected cell with a cell in the clipboard.
1252 *
1252 *
1253 * @method paste_cell_replace
1253 * @method paste_cell_replace
1254 */
1254 */
1255 Notebook.prototype.paste_cell_replace = function () {
1255 Notebook.prototype.paste_cell_replace = function () {
1256 if (this.clipboard !== null && this.paste_enabled) {
1256 if (this.clipboard !== null && this.paste_enabled) {
1257 var cell_data = this.clipboard;
1257 var cell_data = this.clipboard;
1258 var new_cell = this.insert_cell_above(cell_data.cell_type);
1258 var new_cell = this.insert_cell_above(cell_data.cell_type);
1259 new_cell.fromJSON(cell_data);
1259 new_cell.fromJSON(cell_data);
1260 var old_cell = this.get_next_cell(new_cell);
1260 var old_cell = this.get_next_cell(new_cell);
1261 this.delete_cell(this.find_cell_index(old_cell));
1261 this.delete_cell(this.find_cell_index(old_cell));
1262 this.select(this.find_cell_index(new_cell));
1262 this.select(this.find_cell_index(new_cell));
1263 }
1263 }
1264 };
1264 };
1265
1265
1266 /**
1266 /**
1267 * Paste a cell from the clipboard above the selected cell.
1267 * Paste a cell from the clipboard above the selected cell.
1268 *
1268 *
1269 * @method paste_cell_above
1269 * @method paste_cell_above
1270 */
1270 */
1271 Notebook.prototype.paste_cell_above = function () {
1271 Notebook.prototype.paste_cell_above = function () {
1272 if (this.clipboard !== null && this.paste_enabled) {
1272 if (this.clipboard !== null && this.paste_enabled) {
1273 var cell_data = this.clipboard;
1273 var cell_data = this.clipboard;
1274 var new_cell = this.insert_cell_above(cell_data.cell_type);
1274 var new_cell = this.insert_cell_above(cell_data.cell_type);
1275 new_cell.fromJSON(cell_data);
1275 new_cell.fromJSON(cell_data);
1276 new_cell.focus_cell();
1276 new_cell.focus_cell();
1277 }
1277 }
1278 };
1278 };
1279
1279
1280 /**
1280 /**
1281 * Paste a cell from the clipboard below the selected cell.
1281 * Paste a cell from the clipboard below the selected cell.
1282 *
1282 *
1283 * @method paste_cell_below
1283 * @method paste_cell_below
1284 */
1284 */
1285 Notebook.prototype.paste_cell_below = function () {
1285 Notebook.prototype.paste_cell_below = function () {
1286 if (this.clipboard !== null && this.paste_enabled) {
1286 if (this.clipboard !== null && this.paste_enabled) {
1287 var cell_data = this.clipboard;
1287 var cell_data = this.clipboard;
1288 var new_cell = this.insert_cell_below(cell_data.cell_type);
1288 var new_cell = this.insert_cell_below(cell_data.cell_type);
1289 new_cell.fromJSON(cell_data);
1289 new_cell.fromJSON(cell_data);
1290 new_cell.focus_cell();
1290 new_cell.focus_cell();
1291 }
1291 }
1292 };
1292 };
1293
1293
1294 // Split/merge
1294 // Split/merge
1295
1295
1296 /**
1296 /**
1297 * Split the selected cell into two, at the cursor.
1297 * Split the selected cell into two, at the cursor.
1298 *
1298 *
1299 * @method split_cell
1299 * @method split_cell
1300 */
1300 */
1301 Notebook.prototype.split_cell = function () {
1301 Notebook.prototype.split_cell = function () {
1302 var cell = this.get_selected_cell();
1302 var cell = this.get_selected_cell();
1303 if (cell.is_splittable()) {
1303 if (cell.is_splittable()) {
1304 var texta = cell.get_pre_cursor();
1304 var texta = cell.get_pre_cursor();
1305 var textb = cell.get_post_cursor();
1305 var textb = cell.get_post_cursor();
1306 cell.set_text(textb);
1306 cell.set_text(textb);
1307 var new_cell = this.insert_cell_above(cell.cell_type);
1307 var new_cell = this.insert_cell_above(cell.cell_type);
1308 // Unrender the new cell so we can call set_text.
1308 // Unrender the new cell so we can call set_text.
1309 new_cell.unrender();
1309 new_cell.unrender();
1310 new_cell.set_text(texta);
1310 new_cell.set_text(texta);
1311 }
1311 }
1312 };
1312 };
1313
1313
1314 /**
1314 /**
1315 * Combine the selected cell into the cell above it.
1315 * Combine the selected cell into the cell above it.
1316 *
1316 *
1317 * @method merge_cell_above
1317 * @method merge_cell_above
1318 */
1318 */
1319 Notebook.prototype.merge_cell_above = function () {
1319 Notebook.prototype.merge_cell_above = function () {
1320 var index = this.get_selected_index();
1320 var index = this.get_selected_index();
1321 var cell = this.get_cell(index);
1321 var cell = this.get_cell(index);
1322 var render = cell.rendered;
1322 var render = cell.rendered;
1323 if (!cell.is_mergeable()) {
1323 if (!cell.is_mergeable()) {
1324 return;
1324 return;
1325 }
1325 }
1326 if (index > 0) {
1326 if (index > 0) {
1327 var upper_cell = this.get_cell(index-1);
1327 var upper_cell = this.get_cell(index-1);
1328 if (!upper_cell.is_mergeable()) {
1328 if (!upper_cell.is_mergeable()) {
1329 return;
1329 return;
1330 }
1330 }
1331 var upper_text = upper_cell.get_text();
1331 var upper_text = upper_cell.get_text();
1332 var text = cell.get_text();
1332 var text = cell.get_text();
1333 if (cell instanceof codecell.CodeCell) {
1333 if (cell instanceof codecell.CodeCell) {
1334 cell.set_text(upper_text+'\n'+text);
1334 cell.set_text(upper_text+'\n'+text);
1335 } else {
1335 } else {
1336 cell.unrender(); // Must unrender before we set_text.
1336 cell.unrender(); // Must unrender before we set_text.
1337 cell.set_text(upper_text+'\n\n'+text);
1337 cell.set_text(upper_text+'\n\n'+text);
1338 if (render) {
1338 if (render) {
1339 // The rendered state of the final cell should match
1339 // The rendered state of the final cell should match
1340 // that of the original selected cell;
1340 // that of the original selected cell;
1341 cell.render();
1341 cell.render();
1342 }
1342 }
1343 }
1343 }
1344 this.delete_cell(index-1);
1344 this.delete_cell(index-1);
1345 this.select(this.find_cell_index(cell));
1345 this.select(this.find_cell_index(cell));
1346 }
1346 }
1347 };
1347 };
1348
1348
1349 /**
1349 /**
1350 * Combine the selected cell into the cell below it.
1350 * Combine the selected cell into the cell below it.
1351 *
1351 *
1352 * @method merge_cell_below
1352 * @method merge_cell_below
1353 */
1353 */
1354 Notebook.prototype.merge_cell_below = function () {
1354 Notebook.prototype.merge_cell_below = function () {
1355 var index = this.get_selected_index();
1355 var index = this.get_selected_index();
1356 var cell = this.get_cell(index);
1356 var cell = this.get_cell(index);
1357 var render = cell.rendered;
1357 var render = cell.rendered;
1358 if (!cell.is_mergeable()) {
1358 if (!cell.is_mergeable()) {
1359 return;
1359 return;
1360 }
1360 }
1361 if (index < this.ncells()-1) {
1361 if (index < this.ncells()-1) {
1362 var lower_cell = this.get_cell(index+1);
1362 var lower_cell = this.get_cell(index+1);
1363 if (!lower_cell.is_mergeable()) {
1363 if (!lower_cell.is_mergeable()) {
1364 return;
1364 return;
1365 }
1365 }
1366 var lower_text = lower_cell.get_text();
1366 var lower_text = lower_cell.get_text();
1367 var text = cell.get_text();
1367 var text = cell.get_text();
1368 if (cell instanceof codecell.CodeCell) {
1368 if (cell instanceof codecell.CodeCell) {
1369 cell.set_text(text+'\n'+lower_text);
1369 cell.set_text(text+'\n'+lower_text);
1370 } else {
1370 } else {
1371 cell.unrender(); // Must unrender before we set_text.
1371 cell.unrender(); // Must unrender before we set_text.
1372 cell.set_text(text+'\n\n'+lower_text);
1372 cell.set_text(text+'\n\n'+lower_text);
1373 if (render) {
1373 if (render) {
1374 // The rendered state of the final cell should match
1374 // The rendered state of the final cell should match
1375 // that of the original selected cell;
1375 // that of the original selected cell;
1376 cell.render();
1376 cell.render();
1377 }
1377 }
1378 }
1378 }
1379 this.delete_cell(index+1);
1379 this.delete_cell(index+1);
1380 this.select(this.find_cell_index(cell));
1380 this.select(this.find_cell_index(cell));
1381 }
1381 }
1382 };
1382 };
1383
1383
1384
1384
1385 // Cell collapsing and output clearing
1385 // Cell collapsing and output clearing
1386
1386
1387 /**
1387 /**
1388 * Hide a cell's output.
1388 * Hide a cell's output.
1389 *
1389 *
1390 * @method collapse_output
1390 * @method collapse_output
1391 * @param {Number} index A cell's numeric index
1391 * @param {Number} index A cell's numeric index
1392 */
1392 */
1393 Notebook.prototype.collapse_output = function (index) {
1393 Notebook.prototype.collapse_output = function (index) {
1394 var i = this.index_or_selected(index);
1394 var i = this.index_or_selected(index);
1395 var cell = this.get_cell(i);
1395 var cell = this.get_cell(i);
1396 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1396 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1397 cell.collapse_output();
1397 cell.collapse_output();
1398 this.set_dirty(true);
1398 this.set_dirty(true);
1399 }
1399 }
1400 };
1400 };
1401
1401
1402 /**
1402 /**
1403 * Hide each code cell's output area.
1403 * Hide each code cell's output area.
1404 *
1404 *
1405 * @method collapse_all_output
1405 * @method collapse_all_output
1406 */
1406 */
1407 Notebook.prototype.collapse_all_output = function () {
1407 Notebook.prototype.collapse_all_output = function () {
1408 this.get_cells().map(function (cell, i) {
1408 this.get_cells().map(function (cell, i) {
1409 if (cell instanceof codecell.CodeCell) {
1409 if (cell instanceof codecell.CodeCell) {
1410 cell.collapse_output();
1410 cell.collapse_output();
1411 }
1411 }
1412 });
1412 });
1413 // this should not be set if the `collapse` key is removed from nbformat
1413 // this should not be set if the `collapse` key is removed from nbformat
1414 this.set_dirty(true);
1414 this.set_dirty(true);
1415 };
1415 };
1416
1416
1417 /**
1417 /**
1418 * Show a cell's output.
1418 * Show a cell's output.
1419 *
1419 *
1420 * @method expand_output
1420 * @method expand_output
1421 * @param {Number} index A cell's numeric index
1421 * @param {Number} index A cell's numeric index
1422 */
1422 */
1423 Notebook.prototype.expand_output = function (index) {
1423 Notebook.prototype.expand_output = function (index) {
1424 var i = this.index_or_selected(index);
1424 var i = this.index_or_selected(index);
1425 var cell = this.get_cell(i);
1425 var cell = this.get_cell(i);
1426 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1426 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1427 cell.expand_output();
1427 cell.expand_output();
1428 this.set_dirty(true);
1428 this.set_dirty(true);
1429 }
1429 }
1430 };
1430 };
1431
1431
1432 /**
1432 /**
1433 * Expand each code cell's output area, and remove scrollbars.
1433 * Expand each code cell's output area, and remove scrollbars.
1434 *
1434 *
1435 * @method expand_all_output
1435 * @method expand_all_output
1436 */
1436 */
1437 Notebook.prototype.expand_all_output = function () {
1437 Notebook.prototype.expand_all_output = function () {
1438 this.get_cells().map(function (cell, i) {
1438 this.get_cells().map(function (cell, i) {
1439 if (cell instanceof codecell.CodeCell) {
1439 if (cell instanceof codecell.CodeCell) {
1440 cell.expand_output();
1440 cell.expand_output();
1441 }
1441 }
1442 });
1442 });
1443 // this should not be set if the `collapse` key is removed from nbformat
1443 // this should not be set if the `collapse` key is removed from nbformat
1444 this.set_dirty(true);
1444 this.set_dirty(true);
1445 };
1445 };
1446
1446
1447 /**
1447 /**
1448 * Clear the selected CodeCell's output area.
1448 * Clear the selected CodeCell's output area.
1449 *
1449 *
1450 * @method clear_output
1450 * @method clear_output
1451 * @param {Number} index A cell's numeric index
1451 * @param {Number} index A cell's numeric index
1452 */
1452 */
1453 Notebook.prototype.clear_output = function (index) {
1453 Notebook.prototype.clear_output = function (index) {
1454 var i = this.index_or_selected(index);
1454 var i = this.index_or_selected(index);
1455 var cell = this.get_cell(i);
1455 var cell = this.get_cell(i);
1456 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1456 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1457 cell.clear_output();
1457 cell.clear_output();
1458 this.set_dirty(true);
1458 this.set_dirty(true);
1459 }
1459 }
1460 };
1460 };
1461
1461
1462 /**
1462 /**
1463 * Clear each code cell's output area.
1463 * Clear each code cell's output area.
1464 *
1464 *
1465 * @method clear_all_output
1465 * @method clear_all_output
1466 */
1466 */
1467 Notebook.prototype.clear_all_output = function () {
1467 Notebook.prototype.clear_all_output = function () {
1468 this.get_cells().map(function (cell, i) {
1468 this.get_cells().map(function (cell, i) {
1469 if (cell instanceof codecell.CodeCell) {
1469 if (cell instanceof codecell.CodeCell) {
1470 cell.clear_output();
1470 cell.clear_output();
1471 }
1471 }
1472 });
1472 });
1473 this.set_dirty(true);
1473 this.set_dirty(true);
1474 };
1474 };
1475
1475
1476 /**
1476 /**
1477 * Scroll the selected CodeCell's output area.
1477 * Scroll the selected CodeCell's output area.
1478 *
1478 *
1479 * @method scroll_output
1479 * @method scroll_output
1480 * @param {Number} index A cell's numeric index
1480 * @param {Number} index A cell's numeric index
1481 */
1481 */
1482 Notebook.prototype.scroll_output = function (index) {
1482 Notebook.prototype.scroll_output = function (index) {
1483 var i = this.index_or_selected(index);
1483 var i = this.index_or_selected(index);
1484 var cell = this.get_cell(i);
1484 var cell = this.get_cell(i);
1485 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1485 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1486 cell.scroll_output();
1486 cell.scroll_output();
1487 this.set_dirty(true);
1487 this.set_dirty(true);
1488 }
1488 }
1489 };
1489 };
1490
1490
1491 /**
1491 /**
1492 * Expand each code cell's output area, and add a scrollbar for long output.
1492 * Expand each code cell's output area, and add a scrollbar for long output.
1493 *
1493 *
1494 * @method scroll_all_output
1494 * @method scroll_all_output
1495 */
1495 */
1496 Notebook.prototype.scroll_all_output = function () {
1496 Notebook.prototype.scroll_all_output = function () {
1497 this.get_cells().map(function (cell, i) {
1497 this.get_cells().map(function (cell, i) {
1498 if (cell instanceof codecell.CodeCell) {
1498 if (cell instanceof codecell.CodeCell) {
1499 cell.scroll_output();
1499 cell.scroll_output();
1500 }
1500 }
1501 });
1501 });
1502 // this should not be set if the `collapse` key is removed from nbformat
1502 // this should not be set if the `collapse` key is removed from nbformat
1503 this.set_dirty(true);
1503 this.set_dirty(true);
1504 };
1504 };
1505
1505
1506 /** Toggle whether a cell's output is collapsed or expanded.
1506 /** Toggle whether a cell's output is collapsed or expanded.
1507 *
1507 *
1508 * @method toggle_output
1508 * @method toggle_output
1509 * @param {Number} index A cell's numeric index
1509 * @param {Number} index A cell's numeric index
1510 */
1510 */
1511 Notebook.prototype.toggle_output = function (index) {
1511 Notebook.prototype.toggle_output = function (index) {
1512 var i = this.index_or_selected(index);
1512 var i = this.index_or_selected(index);
1513 var cell = this.get_cell(i);
1513 var cell = this.get_cell(i);
1514 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1514 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1515 cell.toggle_output();
1515 cell.toggle_output();
1516 this.set_dirty(true);
1516 this.set_dirty(true);
1517 }
1517 }
1518 };
1518 };
1519
1519
1520 /**
1520 /**
1521 * Hide/show the output of all cells.
1521 * Hide/show the output of all cells.
1522 *
1522 *
1523 * @method toggle_all_output
1523 * @method toggle_all_output
1524 */
1524 */
1525 Notebook.prototype.toggle_all_output = function () {
1525 Notebook.prototype.toggle_all_output = function () {
1526 this.get_cells().map(function (cell, i) {
1526 this.get_cells().map(function (cell, i) {
1527 if (cell instanceof codecell.CodeCell) {
1527 if (cell instanceof codecell.CodeCell) {
1528 cell.toggle_output();
1528 cell.toggle_output();
1529 }
1529 }
1530 });
1530 });
1531 // this should not be set if the `collapse` key is removed from nbformat
1531 // this should not be set if the `collapse` key is removed from nbformat
1532 this.set_dirty(true);
1532 this.set_dirty(true);
1533 };
1533 };
1534
1534
1535 /**
1535 /**
1536 * Toggle a scrollbar for long cell outputs.
1536 * Toggle a scrollbar for long cell outputs.
1537 *
1537 *
1538 * @method toggle_output_scroll
1538 * @method toggle_output_scroll
1539 * @param {Number} index A cell's numeric index
1539 * @param {Number} index A cell's numeric index
1540 */
1540 */
1541 Notebook.prototype.toggle_output_scroll = function (index) {
1541 Notebook.prototype.toggle_output_scroll = function (index) {
1542 var i = this.index_or_selected(index);
1542 var i = this.index_or_selected(index);
1543 var cell = this.get_cell(i);
1543 var cell = this.get_cell(i);
1544 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1544 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1545 cell.toggle_output_scroll();
1545 cell.toggle_output_scroll();
1546 this.set_dirty(true);
1546 this.set_dirty(true);
1547 }
1547 }
1548 };
1548 };
1549
1549
1550 /**
1550 /**
1551 * Toggle the scrolling of long output on all cells.
1551 * Toggle the scrolling of long output on all cells.
1552 *
1552 *
1553 * @method toggle_all_output_scrolling
1553 * @method toggle_all_output_scrolling
1554 */
1554 */
1555 Notebook.prototype.toggle_all_output_scroll = function () {
1555 Notebook.prototype.toggle_all_output_scroll = function () {
1556 this.get_cells().map(function (cell, i) {
1556 this.get_cells().map(function (cell, i) {
1557 if (cell instanceof codecell.CodeCell) {
1557 if (cell instanceof codecell.CodeCell) {
1558 cell.toggle_output_scroll();
1558 cell.toggle_output_scroll();
1559 }
1559 }
1560 });
1560 });
1561 // this should not be set if the `collapse` key is removed from nbformat
1561 // this should not be set if the `collapse` key is removed from nbformat
1562 this.set_dirty(true);
1562 this.set_dirty(true);
1563 };
1563 };
1564
1564
1565 // Other cell functions: line numbers, ...
1565 // Other cell functions: line numbers, ...
1566
1566
1567 /**
1567 /**
1568 * Toggle line numbers in the selected cell's input area.
1568 * Toggle line numbers in the selected cell's input area.
1569 *
1569 *
1570 * @method cell_toggle_line_numbers
1570 * @method cell_toggle_line_numbers
1571 */
1571 */
1572 Notebook.prototype.cell_toggle_line_numbers = function() {
1572 Notebook.prototype.cell_toggle_line_numbers = function() {
1573 this.get_selected_cell().toggle_line_numbers();
1573 this.get_selected_cell().toggle_line_numbers();
1574 };
1574 };
1575
1575
1576 /**
1576 /**
1577 * Set the codemirror mode for all code cells, including the default for
1577 * Set the codemirror mode for all code cells, including the default for
1578 * new code cells.
1578 * new code cells.
1579 *
1579 *
1580 * @method set_codemirror_mode
1580 * @method set_codemirror_mode
1581 */
1581 */
1582 Notebook.prototype.set_codemirror_mode = function(newmode){
1582 Notebook.prototype.set_codemirror_mode = function(newmode){
1583 if (newmode === this.codemirror_mode) {
1583 if (newmode === this.codemirror_mode) {
1584 return;
1584 return;
1585 }
1585 }
1586 this.codemirror_mode = newmode;
1586 this.codemirror_mode = newmode;
1587 codecell.CodeCell.options_default.cm_config.mode = newmode;
1587 codecell.CodeCell.options_default.cm_config.mode = newmode;
1588 var modename = newmode.mode || newmode.name || newmode;
1589
1588
1590 var that = this;
1589 var that = this;
1591 utils.requireCodeMirrorMode(modename, function () {
1590 utils.requireCodeMirrorMode(newmode, function () {
1592 that.get_cells().map(function(cell, i) {
1591 that.get_cells().map(function(cell, i) {
1593 if (cell.cell_type === 'code'){
1592 if (cell.cell_type === 'code'){
1594 cell.code_mirror.setOption('mode', newmode);
1593 cell.code_mirror.setOption('mode', newmode);
1595 // This is currently redundant, because cm_config ends up as
1594 // This is currently redundant, because cm_config ends up as
1596 // codemirror's own .options object, but I don't want to
1595 // codemirror's own .options object, but I don't want to
1597 // rely on that.
1596 // rely on that.
1598 cell.cm_config.mode = newmode;
1597 cell.cm_config.mode = newmode;
1599 }
1598 }
1600 });
1599 });
1601 });
1600 });
1602 };
1601 };
1603
1602
1604 // Session related things
1603 // Session related things
1605
1604
1606 /**
1605 /**
1607 * Start a new session and set it on each code cell.
1606 * Start a new session and set it on each code cell.
1608 *
1607 *
1609 * @method start_session
1608 * @method start_session
1610 */
1609 */
1611 Notebook.prototype.start_session = function (kernel_name) {
1610 Notebook.prototype.start_session = function (kernel_name) {
1612 if (this._session_starting) {
1611 if (this._session_starting) {
1613 throw new session.SessionAlreadyStarting();
1612 throw new session.SessionAlreadyStarting();
1614 }
1613 }
1615 this._session_starting = true;
1614 this._session_starting = true;
1616
1615
1617 var options = {
1616 var options = {
1618 base_url: this.base_url,
1617 base_url: this.base_url,
1619 ws_url: this.ws_url,
1618 ws_url: this.ws_url,
1620 notebook_path: this.notebook_path,
1619 notebook_path: this.notebook_path,
1621 notebook_name: this.notebook_name,
1620 notebook_name: this.notebook_name,
1622 kernel_name: kernel_name,
1621 kernel_name: kernel_name,
1623 notebook: this
1622 notebook: this
1624 };
1623 };
1625
1624
1626 var success = $.proxy(this._session_started, this);
1625 var success = $.proxy(this._session_started, this);
1627 var failure = $.proxy(this._session_start_failed, this);
1626 var failure = $.proxy(this._session_start_failed, this);
1628
1627
1629 if (this.session !== null) {
1628 if (this.session !== null) {
1630 this.session.restart(options, success, failure);
1629 this.session.restart(options, success, failure);
1631 } else {
1630 } else {
1632 this.session = new session.Session(options);
1631 this.session = new session.Session(options);
1633 this.session.start(success, failure);
1632 this.session.start(success, failure);
1634 }
1633 }
1635 };
1634 };
1636
1635
1637
1636
1638 /**
1637 /**
1639 * Once a session is started, link the code cells to the kernel and pass the
1638 * Once a session is started, link the code cells to the kernel and pass the
1640 * comm manager to the widget manager
1639 * comm manager to the widget manager
1641 *
1640 *
1642 */
1641 */
1643 Notebook.prototype._session_started = function (){
1642 Notebook.prototype._session_started = function (){
1644 this._session_starting = false;
1643 this._session_starting = false;
1645 this.kernel = this.session.kernel;
1644 this.kernel = this.session.kernel;
1646 var ncells = this.ncells();
1645 var ncells = this.ncells();
1647 for (var i=0; i<ncells; i++) {
1646 for (var i=0; i<ncells; i++) {
1648 var cell = this.get_cell(i);
1647 var cell = this.get_cell(i);
1649 if (cell instanceof codecell.CodeCell) {
1648 if (cell instanceof codecell.CodeCell) {
1650 cell.set_kernel(this.session.kernel);
1649 cell.set_kernel(this.session.kernel);
1651 }
1650 }
1652 }
1651 }
1653 };
1652 };
1654 Notebook.prototype._session_start_failed = function (jqxhr, status, error){
1653 Notebook.prototype._session_start_failed = function (jqxhr, status, error){
1655 this._session_starting = false;
1654 this._session_starting = false;
1656 utils.log_ajax_error(jqxhr, status, error);
1655 utils.log_ajax_error(jqxhr, status, error);
1657 };
1656 };
1658
1657
1659 /**
1658 /**
1660 * Prompt the user to restart the IPython kernel.
1659 * Prompt the user to restart the IPython kernel.
1661 *
1660 *
1662 * @method restart_kernel
1661 * @method restart_kernel
1663 */
1662 */
1664 Notebook.prototype.restart_kernel = function () {
1663 Notebook.prototype.restart_kernel = function () {
1665 var that = this;
1664 var that = this;
1666 dialog.modal({
1665 dialog.modal({
1667 notebook: this,
1666 notebook: this,
1668 keyboard_manager: this.keyboard_manager,
1667 keyboard_manager: this.keyboard_manager,
1669 title : "Restart kernel or continue running?",
1668 title : "Restart kernel or continue running?",
1670 body : $("<p/>").text(
1669 body : $("<p/>").text(
1671 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1670 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1672 ),
1671 ),
1673 buttons : {
1672 buttons : {
1674 "Continue running" : {},
1673 "Continue running" : {},
1675 "Restart" : {
1674 "Restart" : {
1676 "class" : "btn-danger",
1675 "class" : "btn-danger",
1677 "click" : function() {
1676 "click" : function() {
1678 that.kernel.restart();
1677 that.kernel.restart();
1679 }
1678 }
1680 }
1679 }
1681 }
1680 }
1682 });
1681 });
1683 };
1682 };
1684
1683
1685 /**
1684 /**
1686 * Execute or render cell outputs and go into command mode.
1685 * Execute or render cell outputs and go into command mode.
1687 *
1686 *
1688 * @method execute_cell
1687 * @method execute_cell
1689 */
1688 */
1690 Notebook.prototype.execute_cell = function () {
1689 Notebook.prototype.execute_cell = function () {
1691 /**
1690 /**
1692 * mode = shift, ctrl, alt
1691 * mode = shift, ctrl, alt
1693 */
1692 */
1694 var cell = this.get_selected_cell();
1693 var cell = this.get_selected_cell();
1695
1694
1696 cell.execute();
1695 cell.execute();
1697 this.command_mode();
1696 this.command_mode();
1698 this.set_dirty(true);
1697 this.set_dirty(true);
1699 };
1698 };
1700
1699
1701 /**
1700 /**
1702 * Execute or render cell outputs and insert a new cell below.
1701 * Execute or render cell outputs and insert a new cell below.
1703 *
1702 *
1704 * @method execute_cell_and_insert_below
1703 * @method execute_cell_and_insert_below
1705 */
1704 */
1706 Notebook.prototype.execute_cell_and_insert_below = function () {
1705 Notebook.prototype.execute_cell_and_insert_below = function () {
1707 var cell = this.get_selected_cell();
1706 var cell = this.get_selected_cell();
1708 var cell_index = this.find_cell_index(cell);
1707 var cell_index = this.find_cell_index(cell);
1709
1708
1710 cell.execute();
1709 cell.execute();
1711
1710
1712 // If we are at the end always insert a new cell and return
1711 // If we are at the end always insert a new cell and return
1713 if (cell_index === (this.ncells()-1)) {
1712 if (cell_index === (this.ncells()-1)) {
1714 this.command_mode();
1713 this.command_mode();
1715 this.insert_cell_below();
1714 this.insert_cell_below();
1716 this.select(cell_index+1);
1715 this.select(cell_index+1);
1717 this.edit_mode();
1716 this.edit_mode();
1718 this.scroll_to_bottom();
1717 this.scroll_to_bottom();
1719 this.set_dirty(true);
1718 this.set_dirty(true);
1720 return;
1719 return;
1721 }
1720 }
1722
1721
1723 this.command_mode();
1722 this.command_mode();
1724 this.insert_cell_below();
1723 this.insert_cell_below();
1725 this.select(cell_index+1);
1724 this.select(cell_index+1);
1726 this.edit_mode();
1725 this.edit_mode();
1727 this.set_dirty(true);
1726 this.set_dirty(true);
1728 };
1727 };
1729
1728
1730 /**
1729 /**
1731 * Execute or render cell outputs and select the next cell.
1730 * Execute or render cell outputs and select the next cell.
1732 *
1731 *
1733 * @method execute_cell_and_select_below
1732 * @method execute_cell_and_select_below
1734 */
1733 */
1735 Notebook.prototype.execute_cell_and_select_below = function () {
1734 Notebook.prototype.execute_cell_and_select_below = function () {
1736
1735
1737 var cell = this.get_selected_cell();
1736 var cell = this.get_selected_cell();
1738 var cell_index = this.find_cell_index(cell);
1737 var cell_index = this.find_cell_index(cell);
1739
1738
1740 cell.execute();
1739 cell.execute();
1741
1740
1742 // If we are at the end always insert a new cell and return
1741 // If we are at the end always insert a new cell and return
1743 if (cell_index === (this.ncells()-1)) {
1742 if (cell_index === (this.ncells()-1)) {
1744 this.command_mode();
1743 this.command_mode();
1745 this.insert_cell_below();
1744 this.insert_cell_below();
1746 this.select(cell_index+1);
1745 this.select(cell_index+1);
1747 this.edit_mode();
1746 this.edit_mode();
1748 this.scroll_to_bottom();
1747 this.scroll_to_bottom();
1749 this.set_dirty(true);
1748 this.set_dirty(true);
1750 return;
1749 return;
1751 }
1750 }
1752
1751
1753 this.command_mode();
1752 this.command_mode();
1754 this.select(cell_index+1);
1753 this.select(cell_index+1);
1755 this.focus_cell();
1754 this.focus_cell();
1756 this.set_dirty(true);
1755 this.set_dirty(true);
1757 };
1756 };
1758
1757
1759 /**
1758 /**
1760 * Execute all cells below the selected cell.
1759 * Execute all cells below the selected cell.
1761 *
1760 *
1762 * @method execute_cells_below
1761 * @method execute_cells_below
1763 */
1762 */
1764 Notebook.prototype.execute_cells_below = function () {
1763 Notebook.prototype.execute_cells_below = function () {
1765 this.execute_cell_range(this.get_selected_index(), this.ncells());
1764 this.execute_cell_range(this.get_selected_index(), this.ncells());
1766 this.scroll_to_bottom();
1765 this.scroll_to_bottom();
1767 };
1766 };
1768
1767
1769 /**
1768 /**
1770 * Execute all cells above the selected cell.
1769 * Execute all cells above the selected cell.
1771 *
1770 *
1772 * @method execute_cells_above
1771 * @method execute_cells_above
1773 */
1772 */
1774 Notebook.prototype.execute_cells_above = function () {
1773 Notebook.prototype.execute_cells_above = function () {
1775 this.execute_cell_range(0, this.get_selected_index());
1774 this.execute_cell_range(0, this.get_selected_index());
1776 };
1775 };
1777
1776
1778 /**
1777 /**
1779 * Execute all cells.
1778 * Execute all cells.
1780 *
1779 *
1781 * @method execute_all_cells
1780 * @method execute_all_cells
1782 */
1781 */
1783 Notebook.prototype.execute_all_cells = function () {
1782 Notebook.prototype.execute_all_cells = function () {
1784 this.execute_cell_range(0, this.ncells());
1783 this.execute_cell_range(0, this.ncells());
1785 this.scroll_to_bottom();
1784 this.scroll_to_bottom();
1786 };
1785 };
1787
1786
1788 /**
1787 /**
1789 * Execute a contiguous range of cells.
1788 * Execute a contiguous range of cells.
1790 *
1789 *
1791 * @method execute_cell_range
1790 * @method execute_cell_range
1792 * @param {Number} start Index of the first cell to execute (inclusive)
1791 * @param {Number} start Index of the first cell to execute (inclusive)
1793 * @param {Number} end Index of the last cell to execute (exclusive)
1792 * @param {Number} end Index of the last cell to execute (exclusive)
1794 */
1793 */
1795 Notebook.prototype.execute_cell_range = function (start, end) {
1794 Notebook.prototype.execute_cell_range = function (start, end) {
1796 this.command_mode();
1795 this.command_mode();
1797 for (var i=start; i<end; i++) {
1796 for (var i=start; i<end; i++) {
1798 this.select(i);
1797 this.select(i);
1799 this.execute_cell();
1798 this.execute_cell();
1800 }
1799 }
1801 };
1800 };
1802
1801
1803 // Persistance and loading
1802 // Persistance and loading
1804
1803
1805 /**
1804 /**
1806 * Getter method for this notebook's name.
1805 * Getter method for this notebook's name.
1807 *
1806 *
1808 * @method get_notebook_name
1807 * @method get_notebook_name
1809 * @return {String} This notebook's name (excluding file extension)
1808 * @return {String} This notebook's name (excluding file extension)
1810 */
1809 */
1811 Notebook.prototype.get_notebook_name = function () {
1810 Notebook.prototype.get_notebook_name = function () {
1812 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1811 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1813 return nbname;
1812 return nbname;
1814 };
1813 };
1815
1814
1816 /**
1815 /**
1817 * Setter method for this notebook's name.
1816 * Setter method for this notebook's name.
1818 *
1817 *
1819 * @method set_notebook_name
1818 * @method set_notebook_name
1820 * @param {String} name A new name for this notebook
1819 * @param {String} name A new name for this notebook
1821 */
1820 */
1822 Notebook.prototype.set_notebook_name = function (name) {
1821 Notebook.prototype.set_notebook_name = function (name) {
1823 var parent = utils.url_path_split(this.notebook_path)[0];
1822 var parent = utils.url_path_split(this.notebook_path)[0];
1824 this.notebook_name = name;
1823 this.notebook_name = name;
1825 this.notebook_path = utils.url_path_join(parent, name);
1824 this.notebook_path = utils.url_path_join(parent, name);
1826 };
1825 };
1827
1826
1828 /**
1827 /**
1829 * Check that a notebook's name is valid.
1828 * Check that a notebook's name is valid.
1830 *
1829 *
1831 * @method test_notebook_name
1830 * @method test_notebook_name
1832 * @param {String} nbname A name for this notebook
1831 * @param {String} nbname A name for this notebook
1833 * @return {Boolean} True if the name is valid, false if invalid
1832 * @return {Boolean} True if the name is valid, false if invalid
1834 */
1833 */
1835 Notebook.prototype.test_notebook_name = function (nbname) {
1834 Notebook.prototype.test_notebook_name = function (nbname) {
1836 nbname = nbname || '';
1835 nbname = nbname || '';
1837 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1836 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1838 return true;
1837 return true;
1839 } else {
1838 } else {
1840 return false;
1839 return false;
1841 }
1840 }
1842 };
1841 };
1843
1842
1844 /**
1843 /**
1845 * Load a notebook from JSON (.ipynb).
1844 * Load a notebook from JSON (.ipynb).
1846 *
1845 *
1847 * @method fromJSON
1846 * @method fromJSON
1848 * @param {Object} data JSON representation of a notebook
1847 * @param {Object} data JSON representation of a notebook
1849 */
1848 */
1850 Notebook.prototype.fromJSON = function (data) {
1849 Notebook.prototype.fromJSON = function (data) {
1851
1850
1852 var content = data.content;
1851 var content = data.content;
1853 var ncells = this.ncells();
1852 var ncells = this.ncells();
1854 var i;
1853 var i;
1855 for (i=0; i<ncells; i++) {
1854 for (i=0; i<ncells; i++) {
1856 // Always delete cell 0 as they get renumbered as they are deleted.
1855 // Always delete cell 0 as they get renumbered as they are deleted.
1857 this.delete_cell(0);
1856 this.delete_cell(0);
1858 }
1857 }
1859 // Save the metadata and name.
1858 // Save the metadata and name.
1860 this.metadata = content.metadata;
1859 this.metadata = content.metadata;
1861 this.notebook_name = data.name;
1860 this.notebook_name = data.name;
1862 this.notebook_path = data.path;
1861 this.notebook_path = data.path;
1863 var trusted = true;
1862 var trusted = true;
1864
1863
1865 // Trigger an event changing the kernel spec - this will set the default
1864 // Trigger an event changing the kernel spec - this will set the default
1866 // codemirror mode
1865 // codemirror mode
1867 if (this.metadata.kernelspec !== undefined) {
1866 if (this.metadata.kernelspec !== undefined) {
1868 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1867 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1869 }
1868 }
1870
1869
1871 // Set the codemirror mode from language_info metadata
1870 // Set the codemirror mode from language_info metadata
1872 if (this.metadata.language_info !== undefined) {
1871 if (this.metadata.language_info !== undefined) {
1873 var langinfo = this.metadata.language_info;
1872 var langinfo = this.metadata.language_info;
1874 // Mode 'null' should be plain, unhighlighted text.
1873 // Mode 'null' should be plain, unhighlighted text.
1875 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
1874 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
1876 this.set_codemirror_mode(cm_mode);
1875 this.set_codemirror_mode(cm_mode);
1877 }
1876 }
1878
1877
1879 var new_cells = content.cells;
1878 var new_cells = content.cells;
1880 ncells = new_cells.length;
1879 ncells = new_cells.length;
1881 var cell_data = null;
1880 var cell_data = null;
1882 var new_cell = null;
1881 var new_cell = null;
1883 for (i=0; i<ncells; i++) {
1882 for (i=0; i<ncells; i++) {
1884 cell_data = new_cells[i];
1883 cell_data = new_cells[i];
1885 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1884 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1886 new_cell.fromJSON(cell_data);
1885 new_cell.fromJSON(cell_data);
1887 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1886 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1888 trusted = false;
1887 trusted = false;
1889 }
1888 }
1890 }
1889 }
1891 if (trusted !== this.trusted) {
1890 if (trusted !== this.trusted) {
1892 this.trusted = trusted;
1891 this.trusted = trusted;
1893 this.events.trigger("trust_changed.Notebook", trusted);
1892 this.events.trigger("trust_changed.Notebook", trusted);
1894 }
1893 }
1895 };
1894 };
1896
1895
1897 /**
1896 /**
1898 * Dump this notebook into a JSON-friendly object.
1897 * Dump this notebook into a JSON-friendly object.
1899 *
1898 *
1900 * @method toJSON
1899 * @method toJSON
1901 * @return {Object} A JSON-friendly representation of this notebook.
1900 * @return {Object} A JSON-friendly representation of this notebook.
1902 */
1901 */
1903 Notebook.prototype.toJSON = function () {
1902 Notebook.prototype.toJSON = function () {
1904 /**
1903 /**
1905 * remove the conversion indicator, which only belongs in-memory
1904 * remove the conversion indicator, which only belongs in-memory
1906 */
1905 */
1907 delete this.metadata.orig_nbformat;
1906 delete this.metadata.orig_nbformat;
1908 delete this.metadata.orig_nbformat_minor;
1907 delete this.metadata.orig_nbformat_minor;
1909
1908
1910 var cells = this.get_cells();
1909 var cells = this.get_cells();
1911 var ncells = cells.length;
1910 var ncells = cells.length;
1912 var cell_array = new Array(ncells);
1911 var cell_array = new Array(ncells);
1913 var trusted = true;
1912 var trusted = true;
1914 for (var i=0; i<ncells; i++) {
1913 for (var i=0; i<ncells; i++) {
1915 var cell = cells[i];
1914 var cell = cells[i];
1916 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1915 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1917 trusted = false;
1916 trusted = false;
1918 }
1917 }
1919 cell_array[i] = cell.toJSON();
1918 cell_array[i] = cell.toJSON();
1920 }
1919 }
1921 var data = {
1920 var data = {
1922 cells: cell_array,
1921 cells: cell_array,
1923 metadata: this.metadata,
1922 metadata: this.metadata,
1924 nbformat: this.nbformat,
1923 nbformat: this.nbformat,
1925 nbformat_minor: this.nbformat_minor
1924 nbformat_minor: this.nbformat_minor
1926 };
1925 };
1927 if (trusted != this.trusted) {
1926 if (trusted != this.trusted) {
1928 this.trusted = trusted;
1927 this.trusted = trusted;
1929 this.events.trigger("trust_changed.Notebook", trusted);
1928 this.events.trigger("trust_changed.Notebook", trusted);
1930 }
1929 }
1931 return data;
1930 return data;
1932 };
1931 };
1933
1932
1934 /**
1933 /**
1935 * Start an autosave timer, for periodically saving the notebook.
1934 * Start an autosave timer, for periodically saving the notebook.
1936 *
1935 *
1937 * @method set_autosave_interval
1936 * @method set_autosave_interval
1938 * @param {Integer} interval the autosave interval in milliseconds
1937 * @param {Integer} interval the autosave interval in milliseconds
1939 */
1938 */
1940 Notebook.prototype.set_autosave_interval = function (interval) {
1939 Notebook.prototype.set_autosave_interval = function (interval) {
1941 var that = this;
1940 var that = this;
1942 // clear previous interval, so we don't get simultaneous timers
1941 // clear previous interval, so we don't get simultaneous timers
1943 if (this.autosave_timer) {
1942 if (this.autosave_timer) {
1944 clearInterval(this.autosave_timer);
1943 clearInterval(this.autosave_timer);
1945 }
1944 }
1946 if (!this.writable) {
1945 if (!this.writable) {
1947 // disable autosave if not writable
1946 // disable autosave if not writable
1948 interval = 0;
1947 interval = 0;
1949 }
1948 }
1950
1949
1951 this.autosave_interval = this.minimum_autosave_interval = interval;
1950 this.autosave_interval = this.minimum_autosave_interval = interval;
1952 if (interval) {
1951 if (interval) {
1953 this.autosave_timer = setInterval(function() {
1952 this.autosave_timer = setInterval(function() {
1954 if (that.dirty) {
1953 if (that.dirty) {
1955 that.save_notebook();
1954 that.save_notebook();
1956 }
1955 }
1957 }, interval);
1956 }, interval);
1958 this.events.trigger("autosave_enabled.Notebook", interval);
1957 this.events.trigger("autosave_enabled.Notebook", interval);
1959 } else {
1958 } else {
1960 this.autosave_timer = null;
1959 this.autosave_timer = null;
1961 this.events.trigger("autosave_disabled.Notebook");
1960 this.events.trigger("autosave_disabled.Notebook");
1962 }
1961 }
1963 };
1962 };
1964
1963
1965 /**
1964 /**
1966 * Save this notebook on the server. This becomes a notebook instance's
1965 * Save this notebook on the server. This becomes a notebook instance's
1967 * .save_notebook method *after* the entire notebook has been loaded.
1966 * .save_notebook method *after* the entire notebook has been loaded.
1968 *
1967 *
1969 * @method save_notebook
1968 * @method save_notebook
1970 */
1969 */
1971 Notebook.prototype.save_notebook = function () {
1970 Notebook.prototype.save_notebook = function () {
1972 if (!this._fully_loaded) {
1971 if (!this._fully_loaded) {
1973 this.events.trigger('notebook_save_failed.Notebook',
1972 this.events.trigger('notebook_save_failed.Notebook',
1974 new Error("Load failed, save is disabled")
1973 new Error("Load failed, save is disabled")
1975 );
1974 );
1976 return;
1975 return;
1977 } else if (!this.writable) {
1976 } else if (!this.writable) {
1978 this.events.trigger('notebook_save_failed.Notebook',
1977 this.events.trigger('notebook_save_failed.Notebook',
1979 new Error("Notebook is read-only")
1978 new Error("Notebook is read-only")
1980 );
1979 );
1981 return;
1980 return;
1982 }
1981 }
1983
1982
1984 // Create a JSON model to be sent to the server.
1983 // Create a JSON model to be sent to the server.
1985 var model = {
1984 var model = {
1986 type : "notebook",
1985 type : "notebook",
1987 content : this.toJSON()
1986 content : this.toJSON()
1988 };
1987 };
1989 // time the ajax call for autosave tuning purposes.
1988 // time the ajax call for autosave tuning purposes.
1990 var start = new Date().getTime();
1989 var start = new Date().getTime();
1991
1990
1992 var that = this;
1991 var that = this;
1993 return this.contents.save(this.notebook_path, model).then(
1992 return this.contents.save(this.notebook_path, model).then(
1994 $.proxy(this.save_notebook_success, this, start),
1993 $.proxy(this.save_notebook_success, this, start),
1995 function (error) {
1994 function (error) {
1996 that.events.trigger('notebook_save_failed.Notebook', error);
1995 that.events.trigger('notebook_save_failed.Notebook', error);
1997 }
1996 }
1998 );
1997 );
1999 };
1998 };
2000
1999
2001 /**
2000 /**
2002 * Success callback for saving a notebook.
2001 * Success callback for saving a notebook.
2003 *
2002 *
2004 * @method save_notebook_success
2003 * @method save_notebook_success
2005 * @param {Integer} start Time when the save request start
2004 * @param {Integer} start Time when the save request start
2006 * @param {Object} data JSON representation of a notebook
2005 * @param {Object} data JSON representation of a notebook
2007 */
2006 */
2008 Notebook.prototype.save_notebook_success = function (start, data) {
2007 Notebook.prototype.save_notebook_success = function (start, data) {
2009 this.set_dirty(false);
2008 this.set_dirty(false);
2010 if (data.message) {
2009 if (data.message) {
2011 // save succeeded, but validation failed.
2010 // save succeeded, but validation failed.
2012 var body = $("<div>");
2011 var body = $("<div>");
2013 var title = "Notebook validation failed";
2012 var title = "Notebook validation failed";
2014
2013
2015 body.append($("<p>").text(
2014 body.append($("<p>").text(
2016 "The save operation succeeded," +
2015 "The save operation succeeded," +
2017 " but the notebook does not appear to be valid." +
2016 " but the notebook does not appear to be valid." +
2018 " The validation error was:"
2017 " The validation error was:"
2019 )).append($("<div>").addClass("validation-error").append(
2018 )).append($("<div>").addClass("validation-error").append(
2020 $("<pre>").text(data.message)
2019 $("<pre>").text(data.message)
2021 ));
2020 ));
2022 dialog.modal({
2021 dialog.modal({
2023 notebook: this,
2022 notebook: this,
2024 keyboard_manager: this.keyboard_manager,
2023 keyboard_manager: this.keyboard_manager,
2025 title: title,
2024 title: title,
2026 body: body,
2025 body: body,
2027 buttons : {
2026 buttons : {
2028 OK : {
2027 OK : {
2029 "class" : "btn-primary"
2028 "class" : "btn-primary"
2030 }
2029 }
2031 }
2030 }
2032 });
2031 });
2033 }
2032 }
2034 this.events.trigger('notebook_saved.Notebook');
2033 this.events.trigger('notebook_saved.Notebook');
2035 this._update_autosave_interval(start);
2034 this._update_autosave_interval(start);
2036 if (this._checkpoint_after_save) {
2035 if (this._checkpoint_after_save) {
2037 this.create_checkpoint();
2036 this.create_checkpoint();
2038 this._checkpoint_after_save = false;
2037 this._checkpoint_after_save = false;
2039 }
2038 }
2040 };
2039 };
2041
2040
2042 /**
2041 /**
2043 * update the autosave interval based on how long the last save took
2042 * update the autosave interval based on how long the last save took
2044 *
2043 *
2045 * @method _update_autosave_interval
2044 * @method _update_autosave_interval
2046 * @param {Integer} timestamp when the save request started
2045 * @param {Integer} timestamp when the save request started
2047 */
2046 */
2048 Notebook.prototype._update_autosave_interval = function (start) {
2047 Notebook.prototype._update_autosave_interval = function (start) {
2049 var duration = (new Date().getTime() - start);
2048 var duration = (new Date().getTime() - start);
2050 if (this.autosave_interval) {
2049 if (this.autosave_interval) {
2051 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
2050 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
2052 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
2051 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
2053 // round to 10 seconds, otherwise we will be setting a new interval too often
2052 // round to 10 seconds, otherwise we will be setting a new interval too often
2054 interval = 10000 * Math.round(interval / 10000);
2053 interval = 10000 * Math.round(interval / 10000);
2055 // set new interval, if it's changed
2054 // set new interval, if it's changed
2056 if (interval != this.autosave_interval) {
2055 if (interval != this.autosave_interval) {
2057 this.set_autosave_interval(interval);
2056 this.set_autosave_interval(interval);
2058 }
2057 }
2059 }
2058 }
2060 };
2059 };
2061
2060
2062 /**
2061 /**
2063 * Explicitly trust the output of this notebook.
2062 * Explicitly trust the output of this notebook.
2064 *
2063 *
2065 * @method trust_notebook
2064 * @method trust_notebook
2066 */
2065 */
2067 Notebook.prototype.trust_notebook = function () {
2066 Notebook.prototype.trust_notebook = function () {
2068 var body = $("<div>").append($("<p>")
2067 var body = $("<div>").append($("<p>")
2069 .text("A trusted IPython notebook may execute hidden malicious code ")
2068 .text("A trusted IPython notebook may execute hidden malicious code ")
2070 .append($("<strong>")
2069 .append($("<strong>")
2071 .append(
2070 .append(
2072 $("<em>").text("when you open it")
2071 $("<em>").text("when you open it")
2073 )
2072 )
2074 ).append(".").append(
2073 ).append(".").append(
2075 " Selecting trust will immediately reload this notebook in a trusted state."
2074 " Selecting trust will immediately reload this notebook in a trusted state."
2076 ).append(
2075 ).append(
2077 " For more information, see the "
2076 " For more information, see the "
2078 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
2077 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
2079 .text("IPython security documentation")
2078 .text("IPython security documentation")
2080 ).append(".")
2079 ).append(".")
2081 );
2080 );
2082
2081
2083 var nb = this;
2082 var nb = this;
2084 dialog.modal({
2083 dialog.modal({
2085 notebook: this,
2084 notebook: this,
2086 keyboard_manager: this.keyboard_manager,
2085 keyboard_manager: this.keyboard_manager,
2087 title: "Trust this notebook?",
2086 title: "Trust this notebook?",
2088 body: body,
2087 body: body,
2089
2088
2090 buttons: {
2089 buttons: {
2091 Cancel : {},
2090 Cancel : {},
2092 Trust : {
2091 Trust : {
2093 class : "btn-danger",
2092 class : "btn-danger",
2094 click : function () {
2093 click : function () {
2095 var cells = nb.get_cells();
2094 var cells = nb.get_cells();
2096 for (var i = 0; i < cells.length; i++) {
2095 for (var i = 0; i < cells.length; i++) {
2097 var cell = cells[i];
2096 var cell = cells[i];
2098 if (cell.cell_type == 'code') {
2097 if (cell.cell_type == 'code') {
2099 cell.output_area.trusted = true;
2098 cell.output_area.trusted = true;
2100 }
2099 }
2101 }
2100 }
2102 nb.events.on('notebook_saved.Notebook', function () {
2101 nb.events.on('notebook_saved.Notebook', function () {
2103 window.location.reload();
2102 window.location.reload();
2104 });
2103 });
2105 nb.save_notebook();
2104 nb.save_notebook();
2106 }
2105 }
2107 }
2106 }
2108 }
2107 }
2109 });
2108 });
2110 };
2109 };
2111
2110
2112 Notebook.prototype.copy_notebook = function () {
2111 Notebook.prototype.copy_notebook = function () {
2113 var that = this;
2112 var that = this;
2114 var base_url = this.base_url;
2113 var base_url = this.base_url;
2115 var w = window.open();
2114 var w = window.open();
2116 var parent = utils.url_path_split(this.notebook_path)[0];
2115 var parent = utils.url_path_split(this.notebook_path)[0];
2117 this.contents.copy(this.notebook_path, parent).then(
2116 this.contents.copy(this.notebook_path, parent).then(
2118 function (data) {
2117 function (data) {
2119 w.location = utils.url_join_encode(
2118 w.location = utils.url_join_encode(
2120 base_url, 'notebooks', data.path
2119 base_url, 'notebooks', data.path
2121 );
2120 );
2122 },
2121 },
2123 function(error) {
2122 function(error) {
2124 w.close();
2123 w.close();
2125 that.events.trigger('notebook_copy_failed', error);
2124 that.events.trigger('notebook_copy_failed', error);
2126 }
2125 }
2127 );
2126 );
2128 };
2127 };
2129
2128
2130 Notebook.prototype.rename = function (new_name) {
2129 Notebook.prototype.rename = function (new_name) {
2131 if (!new_name.match(/\.ipynb$/)) {
2130 if (!new_name.match(/\.ipynb$/)) {
2132 new_name = new_name + ".ipynb";
2131 new_name = new_name + ".ipynb";
2133 }
2132 }
2134
2133
2135 var that = this;
2134 var that = this;
2136 var parent = utils.url_path_split(this.notebook_path)[0];
2135 var parent = utils.url_path_split(this.notebook_path)[0];
2137 var new_path = utils.url_path_join(parent, new_name);
2136 var new_path = utils.url_path_join(parent, new_name);
2138 return this.contents.rename(this.notebook_path, new_path).then(
2137 return this.contents.rename(this.notebook_path, new_path).then(
2139 function (json) {
2138 function (json) {
2140 that.notebook_name = json.name;
2139 that.notebook_name = json.name;
2141 that.notebook_path = json.path;
2140 that.notebook_path = json.path;
2142 that.session.rename_notebook(json.path);
2141 that.session.rename_notebook(json.path);
2143 that.events.trigger('notebook_renamed.Notebook', json);
2142 that.events.trigger('notebook_renamed.Notebook', json);
2144 }
2143 }
2145 );
2144 );
2146 };
2145 };
2147
2146
2148 Notebook.prototype.delete = function () {
2147 Notebook.prototype.delete = function () {
2149 this.contents.delete(this.notebook_path);
2148 this.contents.delete(this.notebook_path);
2150 };
2149 };
2151
2150
2152 /**
2151 /**
2153 * Request a notebook's data from the server.
2152 * Request a notebook's data from the server.
2154 *
2153 *
2155 * @method load_notebook
2154 * @method load_notebook
2156 * @param {String} notebook_path A notebook to load
2155 * @param {String} notebook_path A notebook to load
2157 */
2156 */
2158 Notebook.prototype.load_notebook = function (notebook_path) {
2157 Notebook.prototype.load_notebook = function (notebook_path) {
2159 this.notebook_path = notebook_path;
2158 this.notebook_path = notebook_path;
2160 this.notebook_name = utils.url_path_split(this.notebook_path)[1];
2159 this.notebook_name = utils.url_path_split(this.notebook_path)[1];
2161 this.events.trigger('notebook_loading.Notebook');
2160 this.events.trigger('notebook_loading.Notebook');
2162 this.contents.get(notebook_path, {type: 'notebook'}).then(
2161 this.contents.get(notebook_path, {type: 'notebook'}).then(
2163 $.proxy(this.load_notebook_success, this),
2162 $.proxy(this.load_notebook_success, this),
2164 $.proxy(this.load_notebook_error, this)
2163 $.proxy(this.load_notebook_error, this)
2165 );
2164 );
2166 };
2165 };
2167
2166
2168 /**
2167 /**
2169 * Success callback for loading a notebook from the server.
2168 * Success callback for loading a notebook from the server.
2170 *
2169 *
2171 * Load notebook data from the JSON response.
2170 * Load notebook data from the JSON response.
2172 *
2171 *
2173 * @method load_notebook_success
2172 * @method load_notebook_success
2174 * @param {Object} data JSON representation of a notebook
2173 * @param {Object} data JSON representation of a notebook
2175 */
2174 */
2176 Notebook.prototype.load_notebook_success = function (data) {
2175 Notebook.prototype.load_notebook_success = function (data) {
2177 var failed, msg;
2176 var failed, msg;
2178 try {
2177 try {
2179 this.fromJSON(data);
2178 this.fromJSON(data);
2180 } catch (e) {
2179 } catch (e) {
2181 failed = e;
2180 failed = e;
2182 console.log("Notebook failed to load from JSON:", e);
2181 console.log("Notebook failed to load from JSON:", e);
2183 }
2182 }
2184 if (failed || data.message) {
2183 if (failed || data.message) {
2185 // *either* fromJSON failed or validation failed
2184 // *either* fromJSON failed or validation failed
2186 var body = $("<div>");
2185 var body = $("<div>");
2187 var title;
2186 var title;
2188 if (failed) {
2187 if (failed) {
2189 title = "Notebook failed to load";
2188 title = "Notebook failed to load";
2190 body.append($("<p>").text(
2189 body.append($("<p>").text(
2191 "The error was: "
2190 "The error was: "
2192 )).append($("<div>").addClass("js-error").text(
2191 )).append($("<div>").addClass("js-error").text(
2193 failed.toString()
2192 failed.toString()
2194 )).append($("<p>").text(
2193 )).append($("<p>").text(
2195 "See the error console for details."
2194 "See the error console for details."
2196 ));
2195 ));
2197 } else {
2196 } else {
2198 title = "Notebook validation failed";
2197 title = "Notebook validation failed";
2199 }
2198 }
2200
2199
2201 if (data.message) {
2200 if (data.message) {
2202 if (failed) {
2201 if (failed) {
2203 msg = "The notebook also failed validation:";
2202 msg = "The notebook also failed validation:";
2204 } else {
2203 } else {
2205 msg = "An invalid notebook may not function properly." +
2204 msg = "An invalid notebook may not function properly." +
2206 " The validation error was:";
2205 " The validation error was:";
2207 }
2206 }
2208 body.append($("<p>").text(
2207 body.append($("<p>").text(
2209 msg
2208 msg
2210 )).append($("<div>").addClass("validation-error").append(
2209 )).append($("<div>").addClass("validation-error").append(
2211 $("<pre>").text(data.message)
2210 $("<pre>").text(data.message)
2212 ));
2211 ));
2213 }
2212 }
2214
2213
2215 dialog.modal({
2214 dialog.modal({
2216 notebook: this,
2215 notebook: this,
2217 keyboard_manager: this.keyboard_manager,
2216 keyboard_manager: this.keyboard_manager,
2218 title: title,
2217 title: title,
2219 body: body,
2218 body: body,
2220 buttons : {
2219 buttons : {
2221 OK : {
2220 OK : {
2222 "class" : "btn-primary"
2221 "class" : "btn-primary"
2223 }
2222 }
2224 }
2223 }
2225 });
2224 });
2226 }
2225 }
2227 if (this.ncells() === 0) {
2226 if (this.ncells() === 0) {
2228 this.insert_cell_below('code');
2227 this.insert_cell_below('code');
2229 this.edit_mode(0);
2228 this.edit_mode(0);
2230 } else {
2229 } else {
2231 this.select(0);
2230 this.select(0);
2232 this.handle_command_mode(this.get_cell(0));
2231 this.handle_command_mode(this.get_cell(0));
2233 }
2232 }
2234 this.set_dirty(false);
2233 this.set_dirty(false);
2235 this.scroll_to_top();
2234 this.scroll_to_top();
2236 this.writable = data.writable || false;
2235 this.writable = data.writable || false;
2237 var nbmodel = data.content;
2236 var nbmodel = data.content;
2238 var orig_nbformat = nbmodel.metadata.orig_nbformat;
2237 var orig_nbformat = nbmodel.metadata.orig_nbformat;
2239 var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor;
2238 var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor;
2240 if (orig_nbformat !== undefined && nbmodel.nbformat !== orig_nbformat) {
2239 if (orig_nbformat !== undefined && nbmodel.nbformat !== orig_nbformat) {
2241 var src;
2240 var src;
2242 if (nbmodel.nbformat > orig_nbformat) {
2241 if (nbmodel.nbformat > orig_nbformat) {
2243 src = " an older notebook format ";
2242 src = " an older notebook format ";
2244 } else {
2243 } else {
2245 src = " a newer notebook format ";
2244 src = " a newer notebook format ";
2246 }
2245 }
2247
2246
2248 msg = "This notebook has been converted from" + src +
2247 msg = "This notebook has been converted from" + src +
2249 "(v"+orig_nbformat+") to the current notebook " +
2248 "(v"+orig_nbformat+") to the current notebook " +
2250 "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
2249 "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
2251 "current notebook format will be used.";
2250 "current notebook format will be used.";
2252
2251
2253 if (nbmodel.nbformat > orig_nbformat) {
2252 if (nbmodel.nbformat > orig_nbformat) {
2254 msg += " Older versions of IPython may not be able to read the new format.";
2253 msg += " Older versions of IPython may not be able to read the new format.";
2255 } else {
2254 } else {
2256 msg += " Some features of the original notebook may not be available.";
2255 msg += " Some features of the original notebook may not be available.";
2257 }
2256 }
2258 msg += " To preserve the original version, close the " +
2257 msg += " To preserve the original version, close the " +
2259 "notebook without saving it.";
2258 "notebook without saving it.";
2260 dialog.modal({
2259 dialog.modal({
2261 notebook: this,
2260 notebook: this,
2262 keyboard_manager: this.keyboard_manager,
2261 keyboard_manager: this.keyboard_manager,
2263 title : "Notebook converted",
2262 title : "Notebook converted",
2264 body : msg,
2263 body : msg,
2265 buttons : {
2264 buttons : {
2266 OK : {
2265 OK : {
2267 class : "btn-primary"
2266 class : "btn-primary"
2268 }
2267 }
2269 }
2268 }
2270 });
2269 });
2271 } else if (this.nbformat_minor < nbmodel.nbformat_minor) {
2270 } else if (this.nbformat_minor < nbmodel.nbformat_minor) {
2272 this.nbformat_minor = nbmodel.nbformat_minor;
2271 this.nbformat_minor = nbmodel.nbformat_minor;
2273 }
2272 }
2274
2273
2275 // Create the session after the notebook is completely loaded to prevent
2274 // Create the session after the notebook is completely loaded to prevent
2276 // code execution upon loading, which is a security risk.
2275 // code execution upon loading, which is a security risk.
2277 if (this.session === null) {
2276 if (this.session === null) {
2278 var kernel_name;
2277 var kernel_name;
2279 if (this.metadata.kernelspec) {
2278 if (this.metadata.kernelspec) {
2280 var kernelspec = this.metadata.kernelspec || {};
2279 var kernelspec = this.metadata.kernelspec || {};
2281 kernel_name = kernelspec.name;
2280 kernel_name = kernelspec.name;
2282 } else {
2281 } else {
2283 kernel_name = utils.get_url_param('kernel_name');
2282 kernel_name = utils.get_url_param('kernel_name');
2284 }
2283 }
2285 this.start_session(kernel_name);
2284 this.start_session(kernel_name);
2286 }
2285 }
2287 // load our checkpoint list
2286 // load our checkpoint list
2288 this.list_checkpoints();
2287 this.list_checkpoints();
2289
2288
2290 // load toolbar state
2289 // load toolbar state
2291 if (this.metadata.celltoolbar) {
2290 if (this.metadata.celltoolbar) {
2292 celltoolbar.CellToolbar.global_show();
2291 celltoolbar.CellToolbar.global_show();
2293 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2292 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2294 } else {
2293 } else {
2295 celltoolbar.CellToolbar.global_hide();
2294 celltoolbar.CellToolbar.global_hide();
2296 }
2295 }
2297
2296
2298 if (!this.writable) {
2297 if (!this.writable) {
2299 this.set_autosave_interval(0);
2298 this.set_autosave_interval(0);
2300 this.events.trigger('notebook_read_only.Notebook');
2299 this.events.trigger('notebook_read_only.Notebook');
2301 }
2300 }
2302
2301
2303 // now that we're fully loaded, it is safe to restore save functionality
2302 // now that we're fully loaded, it is safe to restore save functionality
2304 this._fully_loaded = true;
2303 this._fully_loaded = true;
2305 this.events.trigger('notebook_loaded.Notebook');
2304 this.events.trigger('notebook_loaded.Notebook');
2306 };
2305 };
2307
2306
2308 /**
2307 /**
2309 * Failure callback for loading a notebook from the server.
2308 * Failure callback for loading a notebook from the server.
2310 *
2309 *
2311 * @method load_notebook_error
2310 * @method load_notebook_error
2312 * @param {Error} error
2311 * @param {Error} error
2313 */
2312 */
2314 Notebook.prototype.load_notebook_error = function (error) {
2313 Notebook.prototype.load_notebook_error = function (error) {
2315 this.events.trigger('notebook_load_failed.Notebook', error);
2314 this.events.trigger('notebook_load_failed.Notebook', error);
2316 var msg;
2315 var msg;
2317 if (error.name === utils.XHR_ERROR && error.xhr.status === 500) {
2316 if (error.name === utils.XHR_ERROR && error.xhr.status === 500) {
2318 utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
2317 utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
2319 msg = "An unknown error occurred while loading this notebook. " +
2318 msg = "An unknown error occurred while loading this notebook. " +
2320 "This version can load notebook formats " +
2319 "This version can load notebook formats " +
2321 "v" + this.nbformat + " or earlier. See the server log for details.";
2320 "v" + this.nbformat + " or earlier. See the server log for details.";
2322 } else {
2321 } else {
2323 msg = error.message;
2322 msg = error.message;
2324 }
2323 }
2325 dialog.modal({
2324 dialog.modal({
2326 notebook: this,
2325 notebook: this,
2327 keyboard_manager: this.keyboard_manager,
2326 keyboard_manager: this.keyboard_manager,
2328 title: "Error loading notebook",
2327 title: "Error loading notebook",
2329 body : msg,
2328 body : msg,
2330 buttons : {
2329 buttons : {
2331 "OK": {}
2330 "OK": {}
2332 }
2331 }
2333 });
2332 });
2334 };
2333 };
2335
2334
2336 /********************* checkpoint-related *********************/
2335 /********************* checkpoint-related *********************/
2337
2336
2338 /**
2337 /**
2339 * Save the notebook then immediately create a checkpoint.
2338 * Save the notebook then immediately create a checkpoint.
2340 *
2339 *
2341 * @method save_checkpoint
2340 * @method save_checkpoint
2342 */
2341 */
2343 Notebook.prototype.save_checkpoint = function () {
2342 Notebook.prototype.save_checkpoint = function () {
2344 this._checkpoint_after_save = true;
2343 this._checkpoint_after_save = true;
2345 this.save_notebook();
2344 this.save_notebook();
2346 };
2345 };
2347
2346
2348 /**
2347 /**
2349 * Add a checkpoint for this notebook.
2348 * Add a checkpoint for this notebook.
2350 * for use as a callback from checkpoint creation.
2349 * for use as a callback from checkpoint creation.
2351 *
2350 *
2352 * @method add_checkpoint
2351 * @method add_checkpoint
2353 */
2352 */
2354 Notebook.prototype.add_checkpoint = function (checkpoint) {
2353 Notebook.prototype.add_checkpoint = function (checkpoint) {
2355 var found = false;
2354 var found = false;
2356 for (var i = 0; i < this.checkpoints.length; i++) {
2355 for (var i = 0; i < this.checkpoints.length; i++) {
2357 var existing = this.checkpoints[i];
2356 var existing = this.checkpoints[i];
2358 if (existing.id == checkpoint.id) {
2357 if (existing.id == checkpoint.id) {
2359 found = true;
2358 found = true;
2360 this.checkpoints[i] = checkpoint;
2359 this.checkpoints[i] = checkpoint;
2361 break;
2360 break;
2362 }
2361 }
2363 }
2362 }
2364 if (!found) {
2363 if (!found) {
2365 this.checkpoints.push(checkpoint);
2364 this.checkpoints.push(checkpoint);
2366 }
2365 }
2367 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2366 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2368 };
2367 };
2369
2368
2370 /**
2369 /**
2371 * List checkpoints for this notebook.
2370 * List checkpoints for this notebook.
2372 *
2371 *
2373 * @method list_checkpoints
2372 * @method list_checkpoints
2374 */
2373 */
2375 Notebook.prototype.list_checkpoints = function () {
2374 Notebook.prototype.list_checkpoints = function () {
2376 var that = this;
2375 var that = this;
2377 this.contents.list_checkpoints(this.notebook_path).then(
2376 this.contents.list_checkpoints(this.notebook_path).then(
2378 $.proxy(this.list_checkpoints_success, this),
2377 $.proxy(this.list_checkpoints_success, this),
2379 function(error) {
2378 function(error) {
2380 that.events.trigger('list_checkpoints_failed.Notebook', error);
2379 that.events.trigger('list_checkpoints_failed.Notebook', error);
2381 }
2380 }
2382 );
2381 );
2383 };
2382 };
2384
2383
2385 /**
2384 /**
2386 * Success callback for listing checkpoints.
2385 * Success callback for listing checkpoints.
2387 *
2386 *
2388 * @method list_checkpoint_success
2387 * @method list_checkpoint_success
2389 * @param {Object} data JSON representation of a checkpoint
2388 * @param {Object} data JSON representation of a checkpoint
2390 */
2389 */
2391 Notebook.prototype.list_checkpoints_success = function (data) {
2390 Notebook.prototype.list_checkpoints_success = function (data) {
2392 this.checkpoints = data;
2391 this.checkpoints = data;
2393 if (data.length) {
2392 if (data.length) {
2394 this.last_checkpoint = data[data.length - 1];
2393 this.last_checkpoint = data[data.length - 1];
2395 } else {
2394 } else {
2396 this.last_checkpoint = null;
2395 this.last_checkpoint = null;
2397 }
2396 }
2398 this.events.trigger('checkpoints_listed.Notebook', [data]);
2397 this.events.trigger('checkpoints_listed.Notebook', [data]);
2399 };
2398 };
2400
2399
2401 /**
2400 /**
2402 * Create a checkpoint of this notebook on the server from the most recent save.
2401 * Create a checkpoint of this notebook on the server from the most recent save.
2403 *
2402 *
2404 * @method create_checkpoint
2403 * @method create_checkpoint
2405 */
2404 */
2406 Notebook.prototype.create_checkpoint = function () {
2405 Notebook.prototype.create_checkpoint = function () {
2407 var that = this;
2406 var that = this;
2408 this.contents.create_checkpoint(this.notebook_path).then(
2407 this.contents.create_checkpoint(this.notebook_path).then(
2409 $.proxy(this.create_checkpoint_success, this),
2408 $.proxy(this.create_checkpoint_success, this),
2410 function (error) {
2409 function (error) {
2411 that.events.trigger('checkpoint_failed.Notebook', error);
2410 that.events.trigger('checkpoint_failed.Notebook', error);
2412 }
2411 }
2413 );
2412 );
2414 };
2413 };
2415
2414
2416 /**
2415 /**
2417 * Success callback for creating a checkpoint.
2416 * Success callback for creating a checkpoint.
2418 *
2417 *
2419 * @method create_checkpoint_success
2418 * @method create_checkpoint_success
2420 * @param {Object} data JSON representation of a checkpoint
2419 * @param {Object} data JSON representation of a checkpoint
2421 */
2420 */
2422 Notebook.prototype.create_checkpoint_success = function (data) {
2421 Notebook.prototype.create_checkpoint_success = function (data) {
2423 this.add_checkpoint(data);
2422 this.add_checkpoint(data);
2424 this.events.trigger('checkpoint_created.Notebook', data);
2423 this.events.trigger('checkpoint_created.Notebook', data);
2425 };
2424 };
2426
2425
2427 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2426 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2428 var that = this;
2427 var that = this;
2429 checkpoint = checkpoint || this.last_checkpoint;
2428 checkpoint = checkpoint || this.last_checkpoint;
2430 if ( ! checkpoint ) {
2429 if ( ! checkpoint ) {
2431 console.log("restore dialog, but no checkpoint to restore to!");
2430 console.log("restore dialog, but no checkpoint to restore to!");
2432 return;
2431 return;
2433 }
2432 }
2434 var body = $('<div/>').append(
2433 var body = $('<div/>').append(
2435 $('<p/>').addClass("p-space").text(
2434 $('<p/>').addClass("p-space").text(
2436 "Are you sure you want to revert the notebook to " +
2435 "Are you sure you want to revert the notebook to " +
2437 "the latest checkpoint?"
2436 "the latest checkpoint?"
2438 ).append(
2437 ).append(
2439 $("<strong/>").text(
2438 $("<strong/>").text(
2440 " This cannot be undone."
2439 " This cannot be undone."
2441 )
2440 )
2442 )
2441 )
2443 ).append(
2442 ).append(
2444 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2443 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2445 ).append(
2444 ).append(
2446 $('<p/>').addClass("p-space").text(
2445 $('<p/>').addClass("p-space").text(
2447 Date(checkpoint.last_modified)
2446 Date(checkpoint.last_modified)
2448 ).css("text-align", "center")
2447 ).css("text-align", "center")
2449 );
2448 );
2450
2449
2451 dialog.modal({
2450 dialog.modal({
2452 notebook: this,
2451 notebook: this,
2453 keyboard_manager: this.keyboard_manager,
2452 keyboard_manager: this.keyboard_manager,
2454 title : "Revert notebook to checkpoint",
2453 title : "Revert notebook to checkpoint",
2455 body : body,
2454 body : body,
2456 buttons : {
2455 buttons : {
2457 Revert : {
2456 Revert : {
2458 class : "btn-danger",
2457 class : "btn-danger",
2459 click : function () {
2458 click : function () {
2460 that.restore_checkpoint(checkpoint.id);
2459 that.restore_checkpoint(checkpoint.id);
2461 }
2460 }
2462 },
2461 },
2463 Cancel : {}
2462 Cancel : {}
2464 }
2463 }
2465 });
2464 });
2466 };
2465 };
2467
2466
2468 /**
2467 /**
2469 * Restore the notebook to a checkpoint state.
2468 * Restore the notebook to a checkpoint state.
2470 *
2469 *
2471 * @method restore_checkpoint
2470 * @method restore_checkpoint
2472 * @param {String} checkpoint ID
2471 * @param {String} checkpoint ID
2473 */
2472 */
2474 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2473 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2475 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2474 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2476 var that = this;
2475 var that = this;
2477 this.contents.restore_checkpoint(this.notebook_path, checkpoint).then(
2476 this.contents.restore_checkpoint(this.notebook_path, checkpoint).then(
2478 $.proxy(this.restore_checkpoint_success, this),
2477 $.proxy(this.restore_checkpoint_success, this),
2479 function (error) {
2478 function (error) {
2480 that.events.trigger('checkpoint_restore_failed.Notebook', error);
2479 that.events.trigger('checkpoint_restore_failed.Notebook', error);
2481 }
2480 }
2482 );
2481 );
2483 };
2482 };
2484
2483
2485 /**
2484 /**
2486 * Success callback for restoring a notebook to a checkpoint.
2485 * Success callback for restoring a notebook to a checkpoint.
2487 *
2486 *
2488 * @method restore_checkpoint_success
2487 * @method restore_checkpoint_success
2489 */
2488 */
2490 Notebook.prototype.restore_checkpoint_success = function () {
2489 Notebook.prototype.restore_checkpoint_success = function () {
2491 this.events.trigger('checkpoint_restored.Notebook');
2490 this.events.trigger('checkpoint_restored.Notebook');
2492 this.load_notebook(this.notebook_path);
2491 this.load_notebook(this.notebook_path);
2493 };
2492 };
2494
2493
2495 /**
2494 /**
2496 * Delete a notebook checkpoint.
2495 * Delete a notebook checkpoint.
2497 *
2496 *
2498 * @method delete_checkpoint
2497 * @method delete_checkpoint
2499 * @param {String} checkpoint ID
2498 * @param {String} checkpoint ID
2500 */
2499 */
2501 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2500 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2502 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2501 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2503 var that = this;
2502 var that = this;
2504 this.contents.delete_checkpoint(this.notebook_path, checkpoint).then(
2503 this.contents.delete_checkpoint(this.notebook_path, checkpoint).then(
2505 $.proxy(this.delete_checkpoint_success, this),
2504 $.proxy(this.delete_checkpoint_success, this),
2506 function (error) {
2505 function (error) {
2507 that.events.trigger('checkpoint_delete_failed.Notebook', error);
2506 that.events.trigger('checkpoint_delete_failed.Notebook', error);
2508 }
2507 }
2509 );
2508 );
2510 };
2509 };
2511
2510
2512 /**
2511 /**
2513 * Success callback for deleting a notebook checkpoint
2512 * Success callback for deleting a notebook checkpoint
2514 *
2513 *
2515 * @method delete_checkpoint_success
2514 * @method delete_checkpoint_success
2516 */
2515 */
2517 Notebook.prototype.delete_checkpoint_success = function () {
2516 Notebook.prototype.delete_checkpoint_success = function () {
2518 this.events.trigger('checkpoint_deleted.Notebook');
2517 this.events.trigger('checkpoint_deleted.Notebook');
2519 this.load_notebook(this.notebook_path);
2518 this.load_notebook(this.notebook_path);
2520 };
2519 };
2521
2520
2522
2521
2523 // For backwards compatability.
2522 // For backwards compatability.
2524 IPython.Notebook = Notebook;
2523 IPython.Notebook = Notebook;
2525
2524
2526 return {'Notebook': Notebook};
2525 return {'Notebook': Notebook};
2527 });
2526 });
General Comments 0
You need to be logged in to leave comments. Login now