##// END OF EJS Templates
Merge pull request #5076 from ellisonbg/keyboard...
Min RK -
r15625:b721408d merge
parent child Browse files
Show More
@@ -0,0 +1,251 b''
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2011 The IPython Development Team
3 //
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
7
8 //============================================================================
9 // Keyboard management
10 //============================================================================
11
12 IPython.namespace('IPython.keyboard');
13
14 IPython.keyboard = (function (IPython) {
15 "use strict";
16
17 // Setup global keycodes and inverse keycodes.
18
19 // See http://unixpapa.com/js/key.html for a complete description. The short of
20 // it is that there are different keycode sets. Firefox uses the "Mozilla keycodes"
21 // and Webkit/IE use the "IE keycodes". These keycode sets are mostly the same
22 // but have minor differences.
23
24 // These apply to Firefox, (Webkit and IE)
25 var _keycodes = {
26 'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73,
27 'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81, 'r': 82,
28 's': 83, 't': 84, 'u': 85, 'v': 86, 'w': 87, 'x': 88, 'y': 89, 'z': 90,
29 '1 !': 49, '2 @': 50, '3 #': 51, '4 $': 52, '5 %': 53, '6 ^': 54,
30 '7 &': 55, '8 *': 56, '9 (': 57, '0 )': 48,
31 '[ {': 219, '] }': 221, '` ~': 192, ', <': 188, '. >': 190, '/ ?': 191,
32 '\\ |': 220, '\' "': 222,
33 'numpad0': 96, 'numpad1': 97, 'numpad2': 98, 'numpad3': 99, 'numpad4': 100,
34 'numpad5': 101, 'numpad6': 102, 'numpad7': 103, 'numpad8': 104, 'numpad9': 105,
35 'multiply': 106, 'add': 107, 'subtract': 109, 'decimal': 110, 'divide': 111,
36 'f1': 112, 'f2': 113, 'f3': 114, 'f4': 115, 'f5': 116, 'f6': 117, 'f7': 118,
37 'f8': 119, 'f9': 120, 'f11': 122, 'f12': 123, 'f13': 124, 'f14': 125, 'f15': 126,
38 'backspace': 8, 'tab': 9, 'enter': 13, 'shift': 16, 'ctrl': 17, 'alt': 18,
39 'meta': 91, 'capslock': 20, 'esc': 27, 'space': 32, 'pageup': 33, 'pagedown': 34,
40 'end': 35, 'home': 36, 'left': 37, 'up': 38, 'right': 39, 'down': 40,
41 'insert': 45, 'delete': 46, 'numlock': 144,
42 };
43
44 // These apply to Firefox and Opera
45 var _mozilla_keycodes = {
46 '; :': 59, '= +': 61, '- _': 173, 'meta': 224
47 }
48
49 // This apply to Webkit and IE
50 var _ie_keycodes = {
51 '; :': 186, '= +': 187, '- _': 189,
52 }
53
54 var browser = IPython.utils.browser[0];
55 var platform = IPython.utils.platform;
56
57 if (browser === 'Firefox' || browser === 'Opera') {
58 $.extend(_keycodes, _mozilla_keycodes);
59 } else if (browser === 'Safari' || browser === 'Chrome' || browser === 'MSIE') {
60 $.extend(_keycodes, _ie_keycodes);
61 }
62
63 var keycodes = {};
64 var inv_keycodes = {};
65 for (var name in _keycodes) {
66 var names = name.split(' ');
67 if (names.length === 1) {
68 var n = names[0]
69 keycodes[n] = _keycodes[n]
70 inv_keycodes[_keycodes[n]] = n
71 } else {
72 var primary = names[0];
73 var secondary = names[1];
74 keycodes[primary] = _keycodes[name]
75 keycodes[secondary] = _keycodes[name]
76 inv_keycodes[_keycodes[name]] = primary
77 }
78 }
79
80 var normalize_key = function (key) {
81 return inv_keycodes[keycodes[key]];
82 }
83
84 var normalize_shortcut = function (shortcut) {
85 // Put a shortcut into normalized form:
86 // 1. Make lowercase
87 // 2. Replace cmd by meta
88 // 3. Sort '+' separated modifiers into the order alt+ctrl+meta+shift
89 // 4. Normalize keys
90 shortcut = shortcut.toLowerCase().replace('cmd', 'meta');
91 var values = shortcut.split("+");
92 if (values.length === 1) {
93 return normalize_key(values[0])
94 } else {
95 var modifiers = values.slice(0,-1);
96 var key = normalize_key(values[values.length-1]);
97 modifiers.sort();
98 return modifiers.join('+') + '+' + key;
99 }
100 }
101
102 var shortcut_to_event = function (shortcut, type) {
103 // Convert a shortcut (shift+r) to a jQuery Event object
104 type = type || 'keydown';
105 shortcut = normalize_shortcut(shortcut);
106 var values = shortcut.split("+");
107 var modifiers = values.slice(0,-1);
108 var key = values[values.length-1];
109 var opts = {which: keycodes[key]};
110 if (modifiers.indexOf('alt') !== -1) {opts.altKey = true;}
111 if (modifiers.indexOf('ctrl') !== -1) {opts.ctrlKey = true;}
112 if (modifiers.indexOf('meta') !== -1) {opts.metaKey = true;}
113 if (modifiers.indexOf('shift') !== -1) {opts.shiftKey = true;}
114 return $.Event(type, opts);
115 }
116
117 var event_to_shortcut = function (event) {
118 // Convert a jQuery Event object to a shortcut (shift+r)
119 var shortcut = '';
120 var key = inv_keycodes[event.which]
121 if (event.altKey && key !== 'alt') {shortcut += 'alt+';}
122 if (event.ctrlKey && key !== 'ctrl') {shortcut += 'ctrl+';}
123 if (event.metaKey && key !== 'meta') {shortcut += 'meta+';}
124 if (event.shiftKey && key !== 'shift') {shortcut += 'shift+';}
125 shortcut += key;
126 return shortcut
127 }
128
129 var trigger_keydown = function (shortcut, element) {
130 // Trigger shortcut keydown on an element
131 element = element || document;
132 element = $(element);
133 var event = shortcut_to_event(shortcut, 'keydown');
134 element.trigger(event);
135 }
136
137
138 // Shortcut manager class
139
140 var ShortcutManager = function (delay) {
141 this._shortcuts = {}
142 this._counts = {}
143 this._timers = {}
144 this.delay = delay || 800; // delay in milliseconds
145 }
146
147 ShortcutManager.prototype.help = function () {
148 var help = [];
149 for (var shortcut in this._shortcuts) {
150 var help_string = this._shortcuts[shortcut]['help'];
151 var help_index = this._shortcuts[shortcut]['help_index'];
152 if (help_string) {
153 if (platform === 'MacOS') {
154 shortcut = shortcut.replace('meta', 'cmd');
155 }
156 help.push({
157 shortcut: shortcut,
158 help: help_string,
159 help_index: help_index}
160 );
161 }
162 }
163 help.sort(function (a, b) {
164 if (a.help_index > b.help_index)
165 return 1;
166 if (a.help_index < b.help_index)
167 return -1;
168 return 0;
169 });
170 return help;
171 }
172
173 ShortcutManager.prototype.clear_shortcuts = function () {
174 this._shortcuts = {};
175 }
176
177 ShortcutManager.prototype.add_shortcut = function (shortcut, data) {
178 if (typeof(data) === 'function') {
179 data = {help: '', help_index: '', handler: data}
180 }
181 data.help_index = data.help_index || '';
182 data.help = data.help || '';
183 data.count = data.count || 1;
184 if (data.help_index === '') {
185 data.help_index = 'zz';
186 }
187 shortcut = normalize_shortcut(shortcut);
188 this._counts[shortcut] = 0;
189 this._shortcuts[shortcut] = data;
190 }
191
192 ShortcutManager.prototype.add_shortcuts = function (data) {
193 for (var shortcut in data) {
194 this.add_shortcut(shortcut, data[shortcut]);
195 }
196 }
197
198 ShortcutManager.prototype.remove_shortcut = function (shortcut) {
199 shortcut = normalize_shortcut(shortcut);
200 delete this._counts[shortcut];
201 delete this._shortcuts[shortcut];
202 }
203
204 ShortcutManager.prototype.count_handler = function (shortcut, event, data) {
205 var that = this;
206 var c = this._counts;
207 var t = this._timers;
208 var timer = null;
209 if (c[shortcut] === data.count-1) {
210 c[shortcut] = 0;
211 var timer = t[shortcut];
212 if (timer) {clearTimeout(timer); delete t[shortcut];}
213 return data.handler(event);
214 } else {
215 c[shortcut] = c[shortcut] + 1;
216 timer = setTimeout(function () {
217 c[shortcut] = 0;
218 }, that.delay);
219 t[shortcut] = timer;
220 }
221 return false;
222 }
223
224 ShortcutManager.prototype.call_handler = function (event) {
225 var shortcut = event_to_shortcut(event);
226 var data = this._shortcuts[shortcut];
227 if (data) {
228 var handler = data['handler'];
229 if (handler) {
230 if (data.count === 1) {
231 return handler(event);
232 } else if (data.count > 1) {
233 return this.count_handler(shortcut, event, data);
234 }
235 }
236 }
237 return true;
238 }
239
240 return {
241 keycodes : keycodes,
242 inv_keycodes : inv_keycodes,
243 ShortcutManager : ShortcutManager,
244 normalize_key : normalize_key,
245 normalize_shortcut : normalize_shortcut,
246 shortcut_to_event : shortcut_to_event,
247 event_to_shortcut : event_to_shortcut,
248 trigger_keydown : trigger_keydown
249 }
250
251 }(IPython));
@@ -0,0 +1,49 b''
1
2
3 var normalized_shortcuts = [
4 'ctrl+shift+m',
5 'alt+meta+p',
6 ];
7
8 var to_normalize = [
9 ['shift+%', 'shift+5'],
10 ['ShiFT+MeTa+CtRl+AlT+m', 'alt+ctrl+meta+shift+m'],
11 ];
12
13 var unshifted = "` 1 2 3 4 5 6 7 8 9 0 - = q w e r t y u i o p [ ] \\ a s d f g h j k l ; ' z x c v b n m , . /";
14 // shifted = '~ ! @ # $ % ^ & * ( ) _ + Q W E R T Y U I O P { } | A S D F G H J K L : " Z X C V B N M < > ?';
15
16 casper.notebook_test(function () {
17
18 this.then(function () {
19 this.each(unshifted.split(' '), function (self, item) {
20 var result = this.evaluate(function (sc) {
21 var e = IPython.keyboard.shortcut_to_event(sc);
22 var sc2 = IPython.keyboard.event_to_shortcut(e);
23 return sc2;
24 }, item);
25 this.test.assertEquals(result, item, 'Shortcut to event roundtrip: '+item);
26 });
27 });
28
29 this.then(function () {
30 this.each(to_normalize, function (self, item) {
31 var result = this.evaluate(function (pair) {
32 return IPython.keyboard.normalize_shortcut(pair[0]);
33 }, item);
34 this.test.assertEquals(result, item[1], 'Normalize shortcut: '+item[0]);
35 });
36 })
37
38 this.then(function () {
39 this.each(normalized_shortcuts, function (self, item) {
40 var result = this.evaluate(function (sc) {
41 var e = IPython.keyboard.shortcut_to_event(sc);
42 var sc2 = IPython.keyboard.event_to_shortcut(e);
43 return sc2;
44 }, item);
45 this.test.assertEquals(result, item, 'Shortcut to event roundtrip: '+item);
46 });
47 });
48
49 }); No newline at end of file
@@ -1,582 +1,514 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2012 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Utilities
10 10 //============================================================================
11 11 IPython.namespace('IPython.utils');
12 12
13 13 IPython.utils = (function (IPython) {
14 14 "use strict";
15 15
16 16 IPython.load_extensions = function () {
17 17 // load one or more IPython notebook extensions with requirejs
18 18
19 19 var extensions = [];
20 20 var extension_names = arguments;
21 21 for (var i = 0; i < extension_names.length; i++) {
22 22 extensions.push("nbextensions/" + arguments[i]);
23 23 }
24 24
25 25 require(extensions,
26 26 function () {
27 27 for (var i = 0; i < arguments.length; i++) {
28 28 var ext = arguments[i];
29 29 var ext_name = extension_names[i];
30 30 // success callback
31 31 console.log("Loaded extension: " + ext_name);
32 32 if (ext && ext.load_ipython_extension !== undefined) {
33 33 ext.load_ipython_extension();
34 34 }
35 35 }
36 36 },
37 37 function (err) {
38 38 // failure callback
39 39 console.log("Failed to load extension(s):", err.requireModules, err);
40 40 }
41 41 );
42 42 };
43 43
44 44 //============================================================================
45 45 // Cross-browser RegEx Split
46 46 //============================================================================
47 47
48 48 // This code has been MODIFIED from the code licensed below to not replace the
49 49 // default browser split. The license is reproduced here.
50 50
51 51 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
52 52 /*!
53 53 * Cross-Browser Split 1.1.1
54 54 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
55 55 * Available under the MIT License
56 56 * ECMAScript compliant, uniform cross-browser split method
57 57 */
58 58
59 59 /**
60 60 * Splits a string into an array of strings using a regex or string
61 61 * separator. Matches of the separator are not included in the result array.
62 62 * However, if `separator` is a regex that contains capturing groups,
63 63 * backreferences are spliced into the result each time `separator` is
64 64 * matched. Fixes browser bugs compared to the native
65 65 * `String.prototype.split` and can be used reliably cross-browser.
66 66 * @param {String} str String to split.
67 67 * @param {RegExp|String} separator Regex or string to use for separating
68 68 * the string.
69 69 * @param {Number} [limit] Maximum number of items to include in the result
70 70 * array.
71 71 * @returns {Array} Array of substrings.
72 72 * @example
73 73 *
74 74 * // Basic use
75 75 * regex_split('a b c d', ' ');
76 76 * // -> ['a', 'b', 'c', 'd']
77 77 *
78 78 * // With limit
79 79 * regex_split('a b c d', ' ', 2);
80 80 * // -> ['a', 'b']
81 81 *
82 82 * // Backreferences in result array
83 83 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
84 84 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
85 85 */
86 86 var regex_split = function (str, separator, limit) {
87 87 // If `separator` is not a regex, use `split`
88 88 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
89 89 return split.call(str, separator, limit);
90 90 }
91 91 var output = [],
92 92 flags = (separator.ignoreCase ? "i" : "") +
93 93 (separator.multiline ? "m" : "") +
94 94 (separator.extended ? "x" : "") + // Proposed for ES6
95 95 (separator.sticky ? "y" : ""), // Firefox 3+
96 96 lastLastIndex = 0,
97 97 // Make `global` and avoid `lastIndex` issues by working with a copy
98 98 separator = new RegExp(separator.source, flags + "g"),
99 99 separator2, match, lastIndex, lastLength;
100 100 str += ""; // Type-convert
101 101
102 102 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
103 103 if (!compliantExecNpcg) {
104 104 // Doesn't need flags gy, but they don't hurt
105 105 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
106 106 }
107 107 /* Values for `limit`, per the spec:
108 108 * If undefined: 4294967295 // Math.pow(2, 32) - 1
109 109 * If 0, Infinity, or NaN: 0
110 110 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
111 111 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
112 112 * If other: Type-convert, then use the above rules
113 113 */
114 114 limit = typeof(limit) === "undefined" ?
115 115 -1 >>> 0 : // Math.pow(2, 32) - 1
116 116 limit >>> 0; // ToUint32(limit)
117 117 while (match = separator.exec(str)) {
118 118 // `separator.lastIndex` is not reliable cross-browser
119 119 lastIndex = match.index + match[0].length;
120 120 if (lastIndex > lastLastIndex) {
121 121 output.push(str.slice(lastLastIndex, match.index));
122 122 // Fix browsers whose `exec` methods don't consistently return `undefined` for
123 123 // nonparticipating capturing groups
124 124 if (!compliantExecNpcg && match.length > 1) {
125 125 match[0].replace(separator2, function () {
126 126 for (var i = 1; i < arguments.length - 2; i++) {
127 127 if (typeof(arguments[i]) === "undefined") {
128 128 match[i] = undefined;
129 129 }
130 130 }
131 131 });
132 132 }
133 133 if (match.length > 1 && match.index < str.length) {
134 134 Array.prototype.push.apply(output, match.slice(1));
135 135 }
136 136 lastLength = match[0].length;
137 137 lastLastIndex = lastIndex;
138 138 if (output.length >= limit) {
139 139 break;
140 140 }
141 141 }
142 142 if (separator.lastIndex === match.index) {
143 143 separator.lastIndex++; // Avoid an infinite loop
144 144 }
145 145 }
146 146 if (lastLastIndex === str.length) {
147 147 if (lastLength || !separator.test("")) {
148 148 output.push("");
149 149 }
150 150 } else {
151 151 output.push(str.slice(lastLastIndex));
152 152 }
153 153 return output.length > limit ? output.slice(0, limit) : output;
154 154 };
155 155
156 156 //============================================================================
157 157 // End contributed Cross-browser RegEx Split
158 158 //============================================================================
159 159
160 160
161 161 var uuid = function () {
162 162 // http://www.ietf.org/rfc/rfc4122.txt
163 163 var s = [];
164 164 var hexDigits = "0123456789ABCDEF";
165 165 for (var i = 0; i < 32; i++) {
166 166 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
167 167 }
168 168 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
169 169 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
170 170
171 171 var uuid = s.join("");
172 172 return uuid;
173 173 };
174 174
175 175
176 176 //Fix raw text to parse correctly in crazy XML
177 177 function xmlencode(string) {
178 178 return string.replace(/\&/g,'&'+'amp;')
179 179 .replace(/</g,'&'+'lt;')
180 180 .replace(/>/g,'&'+'gt;')
181 181 .replace(/\'/g,'&'+'apos;')
182 182 .replace(/\"/g,'&'+'quot;')
183 183 .replace(/`/g,'&'+'#96;');
184 184 }
185 185
186 186
187 187 //Map from terminal commands to CSS classes
188 188 var ansi_colormap = {
189 189 "01":"ansibold",
190 190
191 191 "30":"ansiblack",
192 192 "31":"ansired",
193 193 "32":"ansigreen",
194 194 "33":"ansiyellow",
195 195 "34":"ansiblue",
196 196 "35":"ansipurple",
197 197 "36":"ansicyan",
198 198 "37":"ansigray",
199 199
200 200 "40":"ansibgblack",
201 201 "41":"ansibgred",
202 202 "42":"ansibggreen",
203 203 "43":"ansibgyellow",
204 204 "44":"ansibgblue",
205 205 "45":"ansibgpurple",
206 206 "46":"ansibgcyan",
207 207 "47":"ansibggray"
208 208 };
209 209
210 210 function _process_numbers(attrs, numbers) {
211 211 // process ansi escapes
212 212 var n = numbers.shift();
213 213 if (ansi_colormap[n]) {
214 214 if ( ! attrs["class"] ) {
215 215 attrs["class"] = ansi_colormap[n];
216 216 } else {
217 217 attrs["class"] += " " + ansi_colormap[n];
218 218 }
219 219 } else if (n == "38" || n == "48") {
220 220 // VT100 256 color or 24 bit RGB
221 221 if (numbers.length < 2) {
222 222 console.log("Not enough fields for VT100 color", numbers);
223 223 return;
224 224 }
225 225
226 226 var index_or_rgb = numbers.shift();
227 227 var r,g,b;
228 228 if (index_or_rgb == "5") {
229 229 // 256 color
230 230 var idx = parseInt(numbers.shift());
231 231 if (idx < 16) {
232 232 // indexed ANSI
233 233 // ignore bright / non-bright distinction
234 234 idx = idx % 8;
235 235 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
236 236 if ( ! attrs["class"] ) {
237 237 attrs["class"] = ansiclass;
238 238 } else {
239 239 attrs["class"] += " " + ansiclass;
240 240 }
241 241 return;
242 242 } else if (idx < 232) {
243 243 // 216 color 6x6x6 RGB
244 244 idx = idx - 16;
245 245 b = idx % 6;
246 246 g = Math.floor(idx / 6) % 6;
247 247 r = Math.floor(idx / 36) % 6;
248 248 // convert to rgb
249 249 r = (r * 51);
250 250 g = (g * 51);
251 251 b = (b * 51);
252 252 } else {
253 253 // grayscale
254 254 idx = idx - 231;
255 255 // it's 1-24 and should *not* include black or white,
256 256 // so a 26 point scale
257 257 r = g = b = Math.floor(idx * 256 / 26);
258 258 }
259 259 } else if (index_or_rgb == "2") {
260 260 // Simple 24 bit RGB
261 261 if (numbers.length > 3) {
262 262 console.log("Not enough fields for RGB", numbers);
263 263 return;
264 264 }
265 265 r = numbers.shift();
266 266 g = numbers.shift();
267 267 b = numbers.shift();
268 268 } else {
269 269 console.log("unrecognized control", numbers);
270 270 return;
271 271 }
272 272 if (r !== undefined) {
273 273 // apply the rgb color
274 274 var line;
275 275 if (n == "38") {
276 276 line = "color: ";
277 277 } else {
278 278 line = "background-color: ";
279 279 }
280 280 line = line + "rgb(" + r + "," + g + "," + b + ");"
281 281 if ( !attrs["style"] ) {
282 282 attrs["style"] = line;
283 283 } else {
284 284 attrs["style"] += " " + line;
285 285 }
286 286 }
287 287 }
288 288 }
289 289
290 290 function ansispan(str) {
291 291 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
292 292 // regular ansi escapes (using the table above)
293 293 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
294 294 if (!pattern) {
295 295 // [(01|22|39|)m close spans
296 296 return "</span>";
297 297 }
298 298 // consume sequence of color escapes
299 299 var numbers = pattern.match(/\d+/g);
300 300 var attrs = {};
301 301 while (numbers.length > 0) {
302 302 _process_numbers(attrs, numbers);
303 303 }
304 304
305 305 var span = "<span ";
306 306 for (var attr in attrs) {
307 307 var value = attrs[attr];
308 308 span = span + " " + attr + '="' + attrs[attr] + '"';
309 309 }
310 310 return span + ">";
311 311 });
312 312 };
313 313
314 314 // Transform ANSI color escape codes into HTML <span> tags with css
315 315 // classes listed in the above ansi_colormap object. The actual color used
316 316 // are set in the css file.
317 317 function fixConsole(txt) {
318 318 txt = xmlencode(txt);
319 319 var re = /\033\[([\dA-Fa-f;]*?)m/;
320 320 var opened = false;
321 321 var cmds = [];
322 322 var opener = "";
323 323 var closer = "";
324 324
325 325 // Strip all ANSI codes that are not color related. Matches
326 326 // all ANSI codes that do not end with "m".
327 327 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
328 328 txt = txt.replace(ignored_re, "");
329 329
330 330 // color ansi codes
331 331 txt = ansispan(txt);
332 332 return txt;
333 333 }
334 334
335 335 // Remove chunks that should be overridden by the effect of
336 336 // carriage return characters
337 337 function fixCarriageReturn(txt) {
338 338 var tmp = txt;
339 339 do {
340 340 txt = tmp;
341 341 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
342 342 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
343 343 } while (tmp.length < txt.length);
344 344 return txt;
345 345 }
346 346
347 347 // Locate any URLs and convert them to a anchor tag
348 348 function autoLinkUrls(txt) {
349 349 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
350 350 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
351 351 }
352 352
353 // some keycodes that seem to be platform/browser independent
354 var keycodes = {
355 BACKSPACE: 8,
356 TAB : 9,
357 ENTER : 13,
358 SHIFT : 16,
359 CTRL : 17,
360 CONTROL : 17,
361 ALT : 18,
362 CAPS_LOCK: 20,
363 ESC : 27,
364 SPACE : 32,
365 PGUP : 33,
366 PGDOWN : 34,
367 END : 35,
368 HOME : 36,
369 LEFT_ARROW: 37,
370 LEFTARROW: 37,
371 LEFT : 37,
372 UP_ARROW : 38,
373 UPARROW : 38,
374 UP : 38,
375 RIGHT_ARROW:39,
376 RIGHTARROW:39,
377 RIGHT : 39,
378 DOWN_ARROW: 40,
379 DOWNARROW: 40,
380 DOWN : 40,
381 I : 73,
382 M : 77,
383 // all three of these keys may be COMMAND on OS X:
384 LEFT_SUPER : 91,
385 RIGHT_SUPER : 92,
386 COMMAND : 93,
387 };
388
389 // trigger a key press event
390 var press = function (key) {
391 var key_press = $.Event('keydown', {which: key});
392 $(document).trigger(key_press);
393 }
394
395 var press_up = function() { press(keycodes.UP); };
396 var press_down = function() { press(keycodes.DOWN); };
397
398 var press_ctrl_enter = function() {
399 $(document).trigger($.Event('keydown', {which: keycodes.ENTER, ctrlKey: true}));
400 };
401
402 var press_shift_enter = function() {
403 $(document).trigger($.Event('keydown', {which: keycodes.ENTER, shiftKey: true}));
404 };
405
406 // trigger the ctrl-m shortcut followed by one of our keys
407 var press_ghetto = function(key) {
408 $(document).trigger($.Event('keydown', {which: keycodes.M, ctrlKey: true}));
409 press(key);
410 };
411
412
413 353 var points_to_pixels = function (points) {
414 354 // A reasonably good way of converting between points and pixels.
415 355 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
416 356 $(body).append(test);
417 357 var pixel_per_point = test.width()/10000;
418 358 test.remove();
419 359 return Math.floor(points*pixel_per_point);
420 360 };
421 361
422 362 var always_new = function (constructor) {
423 363 // wrapper around contructor to avoid requiring `var a = new constructor()`
424 364 // useful for passing constructors as callbacks,
425 365 // not for programmer laziness.
426 366 // from http://programmers.stackexchange.com/questions/118798
427 367 return function () {
428 368 var obj = Object.create(constructor.prototype);
429 369 constructor.apply(obj, arguments);
430 370 return obj;
431 371 };
432 372 };
433 373
434
435 374 var url_path_join = function () {
436 375 // join a sequence of url components with '/'
437 376 var url = '';
438 377 for (var i = 0; i < arguments.length; i++) {
439 378 if (arguments[i] === '') {
440 379 continue;
441 380 }
442 381 if (url.length > 0 && url[url.length-1] != '/') {
443 382 url = url + '/' + arguments[i];
444 383 } else {
445 384 url = url + arguments[i];
446 385 }
447 386 }
448 387 url = url.replace(/\/\/+/, '/');
449 388 return url;
450 389 };
451 390
452 391 var parse_url = function (url) {
453 392 // an `a` element with an href allows attr-access to the parsed segments of a URL
454 393 // a = parse_url("http://localhost:8888/path/name#hash")
455 394 // a.protocol = "http:"
456 395 // a.host = "localhost:8888"
457 396 // a.hostname = "localhost"
458 397 // a.port = 8888
459 398 // a.pathname = "/path/name"
460 399 // a.hash = "#hash"
461 400 var a = document.createElement("a");
462 401 a.href = url;
463 402 return a;
464 403 };
465 404
466 405 var encode_uri_components = function (uri) {
467 406 // encode just the components of a multi-segment uri,
468 407 // leaving '/' separators
469 408 return uri.split('/').map(encodeURIComponent).join('/');
470 409 };
471 410
472 411 var url_join_encode = function () {
473 412 // join a sequence of url components with '/',
474 413 // encoding each component with encodeURIComponent
475 414 return encode_uri_components(url_path_join.apply(null, arguments));
476 415 };
477 416
478 417
479 418 var splitext = function (filename) {
480 419 // mimic Python os.path.splitext
481 420 // Returns ['base', '.ext']
482 421 var idx = filename.lastIndexOf('.');
483 422 if (idx > 0) {
484 423 return [filename.slice(0, idx), filename.slice(idx)];
485 424 } else {
486 425 return [filename, ''];
487 426 }
488 427 };
489 428
490 429
491 430 var escape_html = function (text) {
492 431 // escape text to HTML
493 432 return $("<div/>").text(text).html();
494 433 }
495 434
496 435
497 436 var get_body_data = function(key) {
498 437 // get a url-encoded item from body.data and decode it
499 438 // we should never have any encoded URLs anywhere else in code
500 439 // until we are building an actual request
501 440 return decodeURIComponent($('body').data(key));
502 441 };
503 442
504 443
505 444 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
506 445 var browser = (function() {
507 446 if (typeof navigator === 'undefined') {
508 447 // navigator undefined in node
509 448 return 'None';
510 449 }
511 450 var N= navigator.appName, ua= navigator.userAgent, tem;
512 451 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
513 452 if (M && (tem= ua.match(/version\/([\.\d]+)/i))!= null) M[2]= tem[1];
514 453 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
515 454 return M;
516 455 })();
517 456
518 457 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
519 458 var platform = (function () {
520 459 if (typeof navigator === 'undefined') {
521 460 // navigator undefined in node
522 461 return 'None';
523 462 }
524 463 var OSName="None";
525 464 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
526 465 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
527 466 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
528 467 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
529 468 return OSName
530 469 })();
531 470
532 471 var is_or_has = function (a, b) {
533 472 // Is b a child of a or a itself?
534 473 return a.has(b).length !==0 || a.is(b);
535 474 }
536 475
537 476 var is_focused = function (e) {
538 477 // Is element e, or one of its children focused?
539 478 e = $(e);
540 479 var target = $(document.activeElement);
541 480 if (target.length > 0) {
542 481 if (is_or_has(e, target)) {
543 482 return true;
544 483 } else {
545 484 return false;
546 485 }
547 486 } else {
548 487 return false;
549 488 }
550 489 }
551 490
552 491
553 492 return {
554 493 regex_split : regex_split,
555 494 uuid : uuid,
556 495 fixConsole : fixConsole,
557 keycodes : keycodes,
558 press : press,
559 press_up : press_up,
560 press_down : press_down,
561 press_ctrl_enter : press_ctrl_enter,
562 press_shift_enter : press_shift_enter,
563 press_ghetto : press_ghetto,
564 496 fixCarriageReturn : fixCarriageReturn,
565 497 autoLinkUrls : autoLinkUrls,
566 498 points_to_pixels : points_to_pixels,
567 499 get_body_data : get_body_data,
568 500 parse_url : parse_url,
569 501 url_path_join : url_path_join,
570 502 url_join_encode : url_join_encode,
571 503 encode_uri_components : encode_uri_components,
572 504 splitext : splitext,
573 505 escape_html : escape_html,
574 506 always_new : always_new,
575 507 browser : browser,
576 508 platform: platform,
577 509 is_or_has : is_or_has,
578 510 is_focused : is_focused
579 511 };
580 512
581 513 }(IPython));
582 514
@@ -1,90 +1,89 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2012 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // CellToolbar Example
10 10 //============================================================================
11 11
12 12 (function(IPython) {
13 13 "use strict";
14 14
15 15 var CellToolbar = IPython.CellToolbar;
16 16 var raw_cell_preset = [];
17 var utils = IPython.utils;
18 17
19 18 var select_type = CellToolbar.utils.select_ui_generator([
20 19 ["None", "-"],
21 20 ["LaTeX", "text/latex"],
22 21 ["reST", "text/restructuredtext"],
23 22 ["HTML", "text/html"],
24 23 ["Markdown", "text/markdown"],
25 24 ["Python", "text/x-python"],
26 25 ["Custom", "dialog"],
27 26
28 27 ],
29 28 // setter
30 29 function(cell, value) {
31 30 if (value === "-") {
32 31 delete cell.metadata.raw_mimetype;
33 32 } else if (value === 'dialog'){
34 33 var dialog = $('<div/>').append(
35 34 $("<p/>")
36 35 .text("Set the MIME type of the raw cell:")
37 36 ).append(
38 37 $("<br/>")
39 38 ).append(
40 39 $('<input/>').attr('type','text').attr('size','25')
41 40 .val(cell.metadata.raw_mimetype || "-")
42 41 );
43 42 IPython.dialog.modal({
44 43 title: "Raw Cell MIME Type",
45 44 body: dialog,
46 45 buttons : {
47 46 "Cancel": {},
48 47 "OK": {
49 48 class: "btn-primary",
50 49 click: function () {
51 50 console.log(cell);
52 51 cell.metadata.raw_mimetype = $(this).find('input').val();
53 52 console.log(cell.metadata);
54 53 }
55 54 }
56 55 },
57 56 open : function (event, ui) {
58 57 var that = $(this);
59 58 // Upon ENTER, click the OK button.
60 59 that.find('input[type="text"]').keydown(function (event, ui) {
61 if (event.which === utils.keycodes.ENTER) {
60 if (event.which === IPython.keyboard.keycodes.enter) {
62 61 that.find('.btn-primary').first().click();
63 62 return false;
64 63 }
65 64 });
66 65 that.find('input[type="text"]').focus().select();
67 66 }
68 67 });
69 68 } else {
70 69 cell.metadata.raw_mimetype = value;
71 70 }
72 71 },
73 72 //getter
74 73 function(cell) {
75 74 return cell.metadata.raw_mimetype || "";
76 75 },
77 76 // name
78 77 "Raw NBConvert Format",
79 78 // cell_types
80 79 ["raw"]
81 80 );
82 81
83 82 CellToolbar.register_callback('raw_cell.select', select_type);
84 83
85 84 raw_cell_preset.push('raw_cell.select');
86 85
87 86 CellToolbar.register_preset('Raw Cell Format', raw_cell_preset);
88 87 console.log('Raw Cell Format toolbar preset loaded.');
89 88
90 89 }(IPython));
@@ -1,579 +1,579 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // CodeCell
10 10 //============================================================================
11 11 /**
12 12 * An extendable module that provide base functionnality to create cell for notebook.
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule CodeCell
16 16 */
17 17
18 18
19 19 /* local util for codemirror */
20 20 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;};
21 21
22 22 /**
23 23 *
24 24 * function to delete until previous non blanking space character
25 25 * or first multiple of 4 tabstop.
26 26 * @private
27 27 */
28 28 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
29 29 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
30 30 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
31 31 var cur = cm.getCursor(), line = cm.getLine(cur.line);
32 32 var tabsize = cm.getOption('tabSize');
33 33 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
34 34 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
35 35 var select = cm.getRange(from,cur);
36 36 if( select.match(/^\ +$/) !== null){
37 37 cm.replaceRange("",from,cur);
38 38 } else {
39 39 cm.deleteH(-1,"char");
40 40 }
41 41 };
42 42
43 43
44 44 var IPython = (function (IPython) {
45 45 "use strict";
46 46
47 47 var utils = IPython.utils;
48 var key = IPython.utils.keycodes;
48 var keycodes = IPython.keyboard.keycodes;
49 49
50 50 /**
51 51 * A Cell conceived to write code.
52 52 *
53 53 * The kernel doesn't have to be set at creation time, in that case
54 54 * it will be null and set_kernel has to be called later.
55 55 * @class CodeCell
56 56 * @extends IPython.Cell
57 57 *
58 58 * @constructor
59 59 * @param {Object|null} kernel
60 60 * @param {object|undefined} [options]
61 61 * @param [options.cm_config] {object} config to pass to CodeMirror
62 62 */
63 63 var CodeCell = function (kernel, options) {
64 64 this.kernel = kernel || null;
65 65 this.collapsed = false;
66 66
67 67 // create all attributed in constructor function
68 68 // even if null for V8 VM optimisation
69 69 this.input_prompt_number = null;
70 70 this.celltoolbar = null;
71 71 this.output_area = null;
72 72 this.last_msg_id = null;
73 73 this.completer = null;
74 74
75 75
76 76 var cm_overwrite_options = {
77 77 onKeyEvent: $.proxy(this.handle_keyevent,this)
78 78 };
79 79
80 80 options = this.mergeopt(CodeCell, options, {cm_config:cm_overwrite_options});
81 81
82 82 IPython.Cell.apply(this,[options]);
83 83
84 84 // Attributes we want to override in this subclass.
85 85 this.cell_type = "code";
86 86
87 87 var that = this;
88 88 this.element.focusout(
89 89 function() { that.auto_highlight(); }
90 90 );
91 91 };
92 92
93 93 CodeCell.options_default = {
94 94 cm_config : {
95 95 extraKeys: {
96 96 "Tab" : "indentMore",
97 97 "Shift-Tab" : "indentLess",
98 98 "Backspace" : "delSpaceToPrevTabStop",
99 99 "Cmd-/" : "toggleComment",
100 100 "Ctrl-/" : "toggleComment"
101 101 },
102 102 mode: 'ipython',
103 103 theme: 'ipython',
104 104 matchBrackets: true,
105 105 autoCloseBrackets: true
106 106 }
107 107 };
108 108
109 109 CodeCell.msg_cells = {};
110 110
111 111 CodeCell.prototype = new IPython.Cell();
112 112
113 113 /**
114 114 * @method auto_highlight
115 115 */
116 116 CodeCell.prototype.auto_highlight = function () {
117 117 this._auto_highlight(IPython.config.cell_magic_highlight);
118 118 };
119 119
120 120 /** @method create_element */
121 121 CodeCell.prototype.create_element = function () {
122 122 IPython.Cell.prototype.create_element.apply(this, arguments);
123 123
124 124 var cell = $('<div></div>').addClass('cell border-box-sizing code_cell');
125 125 cell.attr('tabindex','2');
126 126
127 127 var input = $('<div></div>').addClass('input');
128 128 var prompt = $('<div/>').addClass('prompt input_prompt');
129 129 var inner_cell = $('<div/>').addClass('inner_cell');
130 130 this.celltoolbar = new IPython.CellToolbar(this);
131 131 inner_cell.append(this.celltoolbar.element);
132 132 var input_area = $('<div/>').addClass('input_area');
133 133 this.code_mirror = CodeMirror(input_area.get(0), this.cm_config);
134 134 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
135 135 inner_cell.append(input_area);
136 136 input.append(prompt).append(inner_cell);
137 137
138 138 var widget_area = $('<div/>')
139 139 .addClass('widget-area')
140 140 .hide();
141 141 this.widget_area = widget_area;
142 142 var widget_prompt = $('<div/>')
143 143 .addClass('prompt')
144 144 .appendTo(widget_area);
145 145 var widget_subarea = $('<div/>')
146 146 .addClass('widget-subarea')
147 147 .appendTo(widget_area);
148 148 this.widget_subarea = widget_subarea;
149 149 var widget_clear_buton = $('<button />')
150 150 .addClass('close')
151 151 .html('&times;')
152 152 .click(function() {
153 153 widget_area.slideUp('', function(){ widget_subarea.html(''); });
154 154 })
155 155 .appendTo(widget_prompt);
156 156
157 157 var output = $('<div></div>');
158 158 cell.append(input).append(widget_area).append(output);
159 159 this.element = cell;
160 160 this.output_area = new IPython.OutputArea(output, true);
161 161 this.completer = new IPython.Completer(this);
162 162 };
163 163
164 164 /** @method bind_events */
165 165 CodeCell.prototype.bind_events = function () {
166 166 IPython.Cell.prototype.bind_events.apply(this);
167 167 var that = this;
168 168
169 169 this.element.focusout(
170 170 function() { that.auto_highlight(); }
171 171 );
172 172 };
173 173
174 174 CodeCell.prototype.handle_keyevent = function (editor, event) {
175 175
176 176 // console.log('CM', this.mode, event.which, event.type)
177 177
178 178 if (this.mode === 'command') {
179 179 return true;
180 180 } else if (this.mode === 'edit') {
181 181 return this.handle_codemirror_keyevent(editor, event);
182 182 }
183 183 };
184 184
185 185 /**
186 186 * This method gets called in CodeMirror's onKeyDown/onKeyPress
187 187 * handlers and is used to provide custom key handling. Its return
188 188 * value is used to determine if CodeMirror should ignore the event:
189 189 * true = ignore, false = don't ignore.
190 190 * @method handle_codemirror_keyevent
191 191 */
192 192 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
193 193
194 194 var that = this;
195 195 // whatever key is pressed, first, cancel the tooltip request before
196 196 // they are sent, and remove tooltip if any, except for tab again
197 197 var tooltip_closed = null;
198 if (event.type === 'keydown' && event.which != key.TAB ) {
198 if (event.type === 'keydown' && event.which != keycodes.tab ) {
199 199 tooltip_closed = IPython.tooltip.remove_and_cancel_tooltip();
200 200 }
201 201
202 202 var cur = editor.getCursor();
203 if (event.keyCode === key.ENTER){
203 if (event.keyCode === keycodes.enter){
204 204 this.auto_highlight();
205 205 }
206 206
207 if (event.keyCode === key.ENTER && (event.shiftKey || event.ctrlKey || event.altKey)) {
207 if (event.keyCode === keycodes.enter && (event.shiftKey || event.ctrlKey || event.altKey)) {
208 208 // Always ignore shift-enter in CodeMirror as we handle it.
209 209 return true;
210 210 } else if (event.which === 40 && event.type === 'keypress' && IPython.tooltip.time_before_tooltip >= 0) {
211 211 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
212 212 // browser and keyboard layout !
213 213 // Pressing '(' , request tooltip, don't forget to reappend it
214 214 // The second argument says to hide the tooltip if the docstring
215 215 // is actually empty
216 216 IPython.tooltip.pending(that, true);
217 } else if (event.which === key.UPARROW && event.type === 'keydown') {
217 } else if (event.which === keycodes.up && event.type === 'keydown') {
218 218 // If we are not at the top, let CM handle the up arrow and
219 219 // prevent the global keydown handler from handling it.
220 220 if (!that.at_top()) {
221 221 event.stop();
222 222 return false;
223 223 } else {
224 224 return true;
225 225 }
226 } else if (event.which === key.ESC && event.type === 'keydown') {
226 } else if (event.which === keycodes.esc && event.type === 'keydown') {
227 227 // First see if the tooltip is active and if so cancel it.
228 228 if (tooltip_closed) {
229 229 // The call to remove_and_cancel_tooltip above in L177 doesn't pass
230 230 // force=true. Because of this it won't actually close the tooltip
231 231 // if it is in sticky mode. Thus, we have to check again if it is open
232 232 // and close it with force=true.
233 233 if (!IPython.tooltip._hidden) {
234 234 IPython.tooltip.remove_and_cancel_tooltip(true);
235 235 }
236 236 // If we closed the tooltip, don't let CM or the global handlers
237 237 // handle this event.
238 238 event.stop();
239 239 return true;
240 240 }
241 241 if (that.code_mirror.options.keyMap === "vim-insert") {
242 242 // vim keyMap is active and in insert mode. In this case we leave vim
243 243 // insert mode, but remain in notebook edit mode.
244 244 // Let' CM handle this event and prevent global handling.
245 245 event.stop();
246 246 return false;
247 247 } else {
248 248 // vim keyMap is not active. Leave notebook edit mode.
249 249 // Don't let CM handle the event, defer to global handling.
250 250 return true;
251 251 }
252 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
252 } else if (event.which === keycodes.down && event.type === 'keydown') {
253 253 // If we are not at the bottom, let CM handle the down arrow and
254 254 // prevent the global keydown handler from handling it.
255 255 if (!that.at_bottom()) {
256 256 event.stop();
257 257 return false;
258 258 } else {
259 259 return true;
260 260 }
261 } else if (event.keyCode === key.TAB && event.type === 'keydown' && event.shiftKey) {
261 } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
262 262 if (editor.somethingSelected()){
263 263 var anchor = editor.getCursor("anchor");
264 264 var head = editor.getCursor("head");
265 265 if( anchor.line != head.line){
266 266 return false;
267 267 }
268 268 }
269 269 IPython.tooltip.request(that);
270 270 event.stop();
271 271 return true;
272 } else if (event.keyCode === key.TAB && event.type == 'keydown') {
272 } else if (event.keyCode === keycodes.tab && event.type == 'keydown') {
273 273 // Tab completion.
274 274 IPython.tooltip.remove_and_cancel_tooltip();
275 275 if (editor.somethingSelected()) {
276 276 return false;
277 277 }
278 278 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
279 279 if (pre_cursor.trim() === "") {
280 280 // Don't autocomplete if the part of the line before the cursor
281 281 // is empty. In this case, let CodeMirror handle indentation.
282 282 return false;
283 283 } else {
284 284 event.stop();
285 285 this.completer.startCompletion();
286 286 return true;
287 287 }
288 288 } else {
289 289 // keypress/keyup also trigger on TAB press, and we don't want to
290 290 // use those to disable tab completion.
291 291 return false;
292 292 }
293 293 return false;
294 294 };
295 295
296 296 // Kernel related calls.
297 297
298 298 CodeCell.prototype.set_kernel = function (kernel) {
299 299 this.kernel = kernel;
300 300 };
301 301
302 302 /**
303 303 * Execute current code cell to the kernel
304 304 * @method execute
305 305 */
306 306 CodeCell.prototype.execute = function () {
307 307 this.output_area.clear_output();
308 308
309 309 // Clear widget area
310 310 this.widget_subarea.html('');
311 311 this.widget_subarea.height('');
312 312 this.widget_area.height('');
313 313 this.widget_area.hide();
314 314
315 315 this.set_input_prompt('*');
316 316 this.element.addClass("running");
317 317 if (this.last_msg_id) {
318 318 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
319 319 }
320 320 var callbacks = this.get_callbacks();
321 321
322 322 var old_msg_id = this.last_msg_id;
323 323 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true});
324 324 if (old_msg_id) {
325 325 delete CodeCell.msg_cells[old_msg_id];
326 326 }
327 327 CodeCell.msg_cells[this.last_msg_id] = this;
328 328 };
329 329
330 330 /**
331 331 * Construct the default callbacks for
332 332 * @method get_callbacks
333 333 */
334 334 CodeCell.prototype.get_callbacks = function () {
335 335 return {
336 336 shell : {
337 337 reply : $.proxy(this._handle_execute_reply, this),
338 338 payload : {
339 339 set_next_input : $.proxy(this._handle_set_next_input, this),
340 340 page : $.proxy(this._open_with_pager, this)
341 341 }
342 342 },
343 343 iopub : {
344 344 output : $.proxy(this.output_area.handle_output, this.output_area),
345 345 clear_output : $.proxy(this.output_area.handle_clear_output, this.output_area),
346 346 },
347 347 input : $.proxy(this._handle_input_request, this)
348 348 };
349 349 };
350 350
351 351 CodeCell.prototype._open_with_pager = function (payload) {
352 352 $([IPython.events]).trigger('open_with_text.Pager', payload);
353 353 };
354 354
355 355 /**
356 356 * @method _handle_execute_reply
357 357 * @private
358 358 */
359 359 CodeCell.prototype._handle_execute_reply = function (msg) {
360 360 this.set_input_prompt(msg.content.execution_count);
361 361 this.element.removeClass("running");
362 362 $([IPython.events]).trigger('set_dirty.Notebook', {value: true});
363 363 };
364 364
365 365 /**
366 366 * @method _handle_set_next_input
367 367 * @private
368 368 */
369 369 CodeCell.prototype._handle_set_next_input = function (payload) {
370 370 var data = {'cell': this, 'text': payload.text};
371 371 $([IPython.events]).trigger('set_next_input.Notebook', data);
372 372 };
373 373
374 374 /**
375 375 * @method _handle_input_request
376 376 * @private
377 377 */
378 378 CodeCell.prototype._handle_input_request = function (msg) {
379 379 this.output_area.append_raw_input(msg);
380 380 };
381 381
382 382
383 383 // Basic cell manipulation.
384 384
385 385 CodeCell.prototype.select = function () {
386 386 var cont = IPython.Cell.prototype.select.apply(this);
387 387 if (cont) {
388 388 this.code_mirror.refresh();
389 389 this.auto_highlight();
390 390 }
391 391 return cont;
392 392 };
393 393
394 394 CodeCell.prototype.render = function () {
395 395 var cont = IPython.Cell.prototype.render.apply(this);
396 396 // Always execute, even if we are already in the rendered state
397 397 return cont;
398 398 };
399 399
400 400 CodeCell.prototype.unrender = function () {
401 401 // CodeCell is always rendered
402 402 return false;
403 403 };
404 404
405 405 /**
406 406 * Determine whether or not the unfocus event should be aknowledged.
407 407 *
408 408 * @method should_cancel_blur
409 409 *
410 410 * @return results {bool} Whether or not to ignore the cell's blur event.
411 411 **/
412 412 CodeCell.prototype.should_cancel_blur = function () {
413 413 // Cancel this unfocus event if the base wants to cancel or the cell
414 414 // completer is open or the tooltip is open.
415 415 return IPython.Cell.prototype.should_cancel_blur.apply(this) ||
416 416 (this.completer && this.completer.is_visible()) ||
417 417 (IPython.tooltip && IPython.tooltip.is_visible());
418 418 };
419 419
420 420 CodeCell.prototype.select_all = function () {
421 421 var start = {line: 0, ch: 0};
422 422 var nlines = this.code_mirror.lineCount();
423 423 var last_line = this.code_mirror.getLine(nlines-1);
424 424 var end = {line: nlines-1, ch: last_line.length};
425 425 this.code_mirror.setSelection(start, end);
426 426 };
427 427
428 428
429 429 CodeCell.prototype.collapse_output = function () {
430 430 this.collapsed = true;
431 431 this.output_area.collapse();
432 432 };
433 433
434 434
435 435 CodeCell.prototype.expand_output = function () {
436 436 this.collapsed = false;
437 437 this.output_area.expand();
438 438 this.output_area.unscroll_area();
439 439 };
440 440
441 441 CodeCell.prototype.scroll_output = function () {
442 442 this.output_area.expand();
443 443 this.output_area.scroll_if_long();
444 444 };
445 445
446 446 CodeCell.prototype.toggle_output = function () {
447 447 this.collapsed = Boolean(1 - this.collapsed);
448 448 this.output_area.toggle_output();
449 449 };
450 450
451 451 CodeCell.prototype.toggle_output_scroll = function () {
452 452 this.output_area.toggle_scroll();
453 453 };
454 454
455 455
456 456 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
457 457 var ns;
458 458 if (prompt_value == undefined) {
459 459 ns = "&nbsp;";
460 460 } else {
461 461 ns = encodeURIComponent(prompt_value);
462 462 }
463 463 return 'In&nbsp;[' + ns + ']:';
464 464 };
465 465
466 466 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
467 467 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
468 468 for(var i=1; i < lines_number; i++) {
469 469 html.push(['...:']);
470 470 }
471 471 return html.join('<br/>');
472 472 };
473 473
474 474 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
475 475
476 476
477 477 CodeCell.prototype.set_input_prompt = function (number) {
478 478 var nline = 1;
479 479 if (this.code_mirror !== undefined) {
480 480 nline = this.code_mirror.lineCount();
481 481 }
482 482 this.input_prompt_number = number;
483 483 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
484 484 // This HTML call is okay because the user contents are escaped.
485 485 this.element.find('div.input_prompt').html(prompt_html);
486 486 };
487 487
488 488
489 489 CodeCell.prototype.clear_input = function () {
490 490 this.code_mirror.setValue('');
491 491 };
492 492
493 493
494 494 CodeCell.prototype.get_text = function () {
495 495 return this.code_mirror.getValue();
496 496 };
497 497
498 498
499 499 CodeCell.prototype.set_text = function (code) {
500 500 return this.code_mirror.setValue(code);
501 501 };
502 502
503 503
504 504 CodeCell.prototype.at_top = function () {
505 505 var cursor = this.code_mirror.getCursor();
506 506 if (cursor.line === 0 && cursor.ch === 0) {
507 507 return true;
508 508 } else {
509 509 return false;
510 510 }
511 511 };
512 512
513 513
514 514 CodeCell.prototype.at_bottom = function () {
515 515 var cursor = this.code_mirror.getCursor();
516 516 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
517 517 return true;
518 518 } else {
519 519 return false;
520 520 }
521 521 };
522 522
523 523
524 524 CodeCell.prototype.clear_output = function (wait) {
525 525 this.output_area.clear_output(wait);
526 526 this.set_input_prompt();
527 527 };
528 528
529 529
530 530 // JSON serialization
531 531
532 532 CodeCell.prototype.fromJSON = function (data) {
533 533 IPython.Cell.prototype.fromJSON.apply(this, arguments);
534 534 if (data.cell_type === 'code') {
535 535 if (data.input !== undefined) {
536 536 this.set_text(data.input);
537 537 // make this value the starting point, so that we can only undo
538 538 // to this state, instead of a blank cell
539 539 this.code_mirror.clearHistory();
540 540 this.auto_highlight();
541 541 }
542 542 if (data.prompt_number !== undefined) {
543 543 this.set_input_prompt(data.prompt_number);
544 544 } else {
545 545 this.set_input_prompt();
546 546 }
547 547 this.output_area.trusted = data.trusted || false;
548 548 this.output_area.fromJSON(data.outputs);
549 549 if (data.collapsed !== undefined) {
550 550 if (data.collapsed) {
551 551 this.collapse_output();
552 552 } else {
553 553 this.expand_output();
554 554 }
555 555 }
556 556 }
557 557 };
558 558
559 559
560 560 CodeCell.prototype.toJSON = function () {
561 561 var data = IPython.Cell.prototype.toJSON.apply(this);
562 562 data.input = this.get_text();
563 563 // is finite protect against undefined and '*' value
564 564 if (isFinite(this.input_prompt_number)) {
565 565 data.prompt_number = this.input_prompt_number;
566 566 }
567 567 var outputs = this.output_area.toJSON();
568 568 data.outputs = outputs;
569 569 data.language = 'python';
570 570 data.trusted = this.output_area.trusted;
571 571 data.collapsed = this.collapsed;
572 572 return data;
573 573 };
574 574
575 575
576 576 IPython.CodeCell = CodeCell;
577 577
578 578 return IPython;
579 579 }(IPython));
@@ -1,391 +1,384 b''
1 1 // function completer.
2 2 //
3 3 // completer should be a class that takes an cell instance
4 4 var IPython = (function (IPython) {
5 5 // that will prevent us from misspelling
6 6 "use strict";
7 7
8 8 // easier key mapping
9 var key = IPython.utils.keycodes;
9 var keycodes = IPython.keyboard.keycodes;
10 10
11 11 function prepend_n_prc(str, n) {
12 12 for( var i =0 ; i< n ; i++){
13 13 str = '%'+str ;
14 14 }
15 15 return str;
16 16 }
17 17
18 18 function _existing_completion(item, completion_array){
19 19 for( var c in completion_array ) {
20 20 if(completion_array[c].trim().substr(-item.length) == item)
21 21 { return true; }
22 22 }
23 23 return false;
24 24 }
25 25
26 26 // what is the common start of all completions
27 27 function shared_start(B, drop_prct) {
28 28 if (B.length == 1) {
29 29 return B[0];
30 30 }
31 31 var A = [];
32 32 var common;
33 33 var min_lead_prct = 10;
34 34 for (var i = 0; i < B.length; i++) {
35 35 var str = B[i].str;
36 36 var localmin = 0;
37 37 if(drop_prct === true){
38 38 while ( str.substr(0, 1) == '%') {
39 39 localmin = localmin+1;
40 40 str = str.substring(1);
41 41 }
42 42 }
43 43 min_lead_prct = Math.min(min_lead_prct, localmin);
44 44 A.push(str);
45 45 }
46 46
47 47 if (A.length > 1) {
48 48 var tem1, tem2, s;
49 49 A = A.slice(0).sort();
50 50 tem1 = A[0];
51 51 s = tem1.length;
52 52 tem2 = A.pop();
53 53 while (s && tem2.indexOf(tem1) == -1) {
54 54 tem1 = tem1.substring(0, --s);
55 55 }
56 56 if (tem1 === "" || tem2.indexOf(tem1) !== 0) {
57 57 return {
58 58 str:prepend_n_prc('', min_lead_prct),
59 59 type: "computed",
60 60 from: B[0].from,
61 61 to: B[0].to
62 62 };
63 63 }
64 64 return {
65 65 str: prepend_n_prc(tem1, min_lead_prct),
66 66 type: "computed",
67 67 from: B[0].from,
68 68 to: B[0].to
69 69 };
70 70 }
71 71 return null;
72 72 }
73 73
74 74
75 75 var Completer = function (cell) {
76 76 this._visible = false;
77 77 this.cell = cell;
78 78 this.editor = cell.code_mirror;
79 79 var that = this;
80 80 $([IPython.events]).on('status_busy.Kernel', function () {
81 81 that.skip_kernel_completion = true;
82 82 });
83 83 $([IPython.events]).on('status_idle.Kernel', function () {
84 84 that.skip_kernel_completion = false;
85 85 });
86 86 };
87 87
88 88 Completer.prototype.is_visible = function () {
89 89 // Return whether or not the completer is visible.
90 90 return this._visible;
91 91 };
92 92
93 93 Completer.prototype.startCompletion = function () {
94 94 // call for a 'first' completion, that will set the editor and do some
95 95 // special behaviour like autopicking if only one completion availlable
96 96 //
97 97 if (this.editor.somethingSelected()) return;
98 98 this.done = false;
99 99 // use to get focus back on opera
100 100 this.carry_on_completion(true);
101 101 };
102 102
103 103
104 104 // easy access for julia to monkeypatch
105 105 //
106 106 Completer.reinvoke_re = /[%0-9a-z._/\\:~-]/i;
107 107
108 108 Completer.prototype.reinvoke= function(pre_cursor, block, cursor){
109 109 return Completer.reinvoke_re.test(pre_cursor);
110 110 };
111 111
112 112 /**
113 113 *
114 114 * pass true as parameter if this is the first invocation of the completer
115 115 * this will prevent the completer to dissmiss itself if it is not on a
116 116 * word boundary like pressing tab after a space, and make it autopick the
117 117 * only choice if there is only one which prevent from popping the UI. as
118 118 * well as fast-forwarding the typing if all completion have a common
119 119 * shared start
120 120 **/
121 121 Completer.prototype.carry_on_completion = function (first_invocation) {
122 122 // Pass true as parameter if you want the completer to autopick when
123 123 // only one completion. This function is automatically reinvoked at
124 124 // each keystroke with first_invocation = false
125 125 var cur = this.editor.getCursor();
126 126 var line = this.editor.getLine(cur.line);
127 127 var pre_cursor = this.editor.getRange({
128 128 line: cur.line,
129 129 ch: cur.ch - 1
130 130 }, cur);
131 131
132 132 // we need to check that we are still on a word boundary
133 133 // because while typing the completer is still reinvoking itself
134 134 // so dismiss if we are on a "bad" caracter
135 135 if (!this.reinvoke(pre_cursor) && !first_invocation) {
136 136 this.close();
137 137 return;
138 138 }
139 139
140 140 this.autopick = false;
141 141 if (first_invocation) {
142 142 this.autopick = true;
143 143 }
144 144
145 145 // We want a single cursor position.
146 146 if (this.editor.somethingSelected()) {
147 147 return;
148 148 }
149 149
150 150 // one kernel completion came back, finish_completing will be called with the results
151 151 // we fork here and directly call finish completing if kernel is busy
152 152 if (this.skip_kernel_completion) {
153 153 this.finish_completing({
154 154 'matches': [],
155 155 matched_text: ""
156 156 });
157 157 } else {
158 158 this.cell.kernel.complete(line, cur.ch, $.proxy(this.finish_completing, this));
159 159 }
160 160 };
161 161
162 162 Completer.prototype.finish_completing = function (msg) {
163 163 // let's build a function that wrap all that stuff into what is needed
164 164 // for the new completer:
165 165 var content = msg.content;
166 166 var matched_text = content.matched_text;
167 167 var matches = content.matches;
168 168
169 169 var cur = this.editor.getCursor();
170 170 var results = CodeMirror.contextHint(this.editor);
171 171 var filtered_results = [];
172 172 //remove results from context completion
173 173 //that are already in kernel completion
174 174 for (var elm in results) {
175 175 if (!_existing_completion(results[elm].str, matches)) {
176 176 filtered_results.push(results[elm]);
177 177 }
178 178 }
179 179
180 180 // append the introspection result, in order, at at the beginning of
181 181 // the table and compute the replacement range from current cursor
182 182 // positon and matched_text length.
183 183 for (var i = matches.length - 1; i >= 0; --i) {
184 184 filtered_results.unshift({
185 185 str: matches[i],
186 186 type: "introspection",
187 187 from: {
188 188 line: cur.line,
189 189 ch: cur.ch - matched_text.length
190 190 },
191 191 to: {
192 192 line: cur.line,
193 193 ch: cur.ch
194 194 }
195 195 });
196 196 }
197 197
198 198 // one the 2 sources results have been merge, deal with it
199 199 this.raw_result = filtered_results;
200 200
201 201 // if empty result return
202 202 if (!this.raw_result || !this.raw_result.length) return;
203 203
204 204 // When there is only one completion, use it directly.
205 205 if (this.autopick && this.raw_result.length == 1) {
206 206 this.insert(this.raw_result[0]);
207 207 return;
208 208 }
209 209
210 210 if (this.raw_result.length == 1) {
211 211 // test if first and only completion totally matches
212 212 // what is typed, in this case dismiss
213 213 var str = this.raw_result[0].str;
214 214 var pre_cursor = this.editor.getRange({
215 215 line: cur.line,
216 216 ch: cur.ch - str.length
217 217 }, cur);
218 218 if (pre_cursor == str) {
219 219 this.close();
220 220 return;
221 221 }
222 222 }
223 223
224 224 this.complete = $('<div/>').addClass('completions');
225 225 this.complete.attr('id', 'complete');
226 226
227 227 // Currently webkit doesn't use the size attr correctly. See:
228 228 // https://code.google.com/p/chromium/issues/detail?id=4579
229 229 this.sel = $('<select style="width: auto"/>')
230 230 .attr('multiple', 'true')
231 231 .attr('size', Math.min(10, this.raw_result.length));
232 232 this.complete.append(this.sel);
233 233 this._visible = true;
234 234 $('body').append(this.complete);
235 235
236 236 // After everything is on the page, compute the postion.
237 237 // We put it above the code if it is too close to the bottom of the page.
238 238 cur.ch = cur.ch-matched_text.length;
239 239 var pos = this.editor.cursorCoords(cur);
240 240 var left = pos.left-3;
241 241 var top;
242 242 var cheight = this.complete.height();
243 243 var wheight = $(window).height();
244 244 if (pos.bottom+cheight+5 > wheight) {
245 245 top = pos.top-cheight-4;
246 246 } else {
247 247 top = pos.bottom+1;
248 248 }
249 249 this.complete.css('left', left + 'px');
250 250 this.complete.css('top', top + 'px');
251 251
252 252
253 253 //build the container
254 254 var that = this;
255 255 this.sel.dblclick(function () {
256 256 that.pick();
257 257 });
258 258 this.sel.blur(this.close);
259 259 this.sel.keydown(function (event) {
260 260 that.keydown(event);
261 261 });
262 262 this.sel.keypress(function (event) {
263 263 that.keypress(event);
264 264 });
265 265
266 266 this.build_gui_list(this.raw_result);
267 267
268 268 this.sel.focus();
269 269 IPython.keyboard_manager.disable();
270 270 // Opera sometimes ignores focusing a freshly created node
271 271 if (window.opera) setTimeout(function () {
272 272 if (!this.done) this.sel.focus();
273 273 }, 100);
274 274 return true;
275 275 };
276 276
277 277 Completer.prototype.insert = function (completion) {
278 278 this.editor.replaceRange(completion.str, completion.from, completion.to);
279 279 };
280 280
281 281 Completer.prototype.build_gui_list = function (completions) {
282 282 for (var i = 0; i < completions.length; ++i) {
283 283 var opt = $('<option/>').text(completions[i].str).addClass(completions[i].type);
284 284 this.sel.append(opt);
285 285 }
286 286 this.sel.children().first().attr('selected', 'true');
287 287 this.sel.scrollTop(0);
288 288 };
289 289
290 290 Completer.prototype.close = function () {
291 291 this._visible = false;
292 292 if (this.done) return;
293 293 this.done = true;
294 294 $('.completions').remove();
295 295 IPython.keyboard_manager.enable();
296 296 };
297 297
298 298 Completer.prototype.pick = function () {
299 299 this.insert(this.raw_result[this.sel[0].selectedIndex]);
300 300 this.close();
301 301 var that = this;
302 302 setTimeout(function () {
303 303 that.editor.focus();
304 304 }, 50);
305 305 };
306 306
307 307 Completer.prototype.keydown = function (event) {
308 308 var code = event.keyCode;
309 309 var that = this;
310 var special_key = false;
311
312 // detect special keys like SHIFT,PGUP,...
313 for( var _key in key ) {
314 if (code == key[_key] ) {
315 special_key = true;
316 }
317 }
318 310
319 311 // Enter
320 if (code == key.ENTER) {
312 if (code == keycodes.enter) {
321 313 CodeMirror.e_stop(event);
322 314 this.pick();
323 315 }
324 316 // Escape or backspace
325 else if (code == key.ESC) {
317 else if (code == keycodes.esc) {
326 318 CodeMirror.e_stop(event);
327 319 this.close();
328 320 this.editor.focus();
329 } else if (code == key.BACKSPACE) {
321
322 } else if (code == keycodes.backspace) {
330 323 this.close();
331 324 this.editor.focus();
332 } else if (code == key.TAB) {
325 } else if (code == keycodes.tab) {
333 326 //all the fastforwarding operation,
334 327 //Check that shared start is not null which can append with prefixed completion
335 328 // like %pylab , pylab have no shred start, and ff will result in py<tab><tab>
336 329 // to erase py
337 330 var sh = shared_start(this.raw_result, true);
338 331 if (sh) {
339 332 this.insert(sh);
340 333 }
341 334 this.close();
342 335 CodeMirror.e_stop(event);
343 336 this.editor.focus();
344 337 //reinvoke self
345 338 setTimeout(function () {
346 339 that.carry_on_completion();
347 340 }, 50);
348 } else if (code == key.UPARROW || code == key.DOWNARROW) {
341 } else if (code == keycodes.up || code == keycodes.down) {
349 342 // need to do that to be able to move the arrow
350 343 // when on the first or last line ofo a code cell
351 344 event.stopPropagation();
352 345 }
353 346 };
354 347
355 348 Completer.prototype.keypress = function (event) {
356 349 // FIXME: This is a band-aid.
357 350 // on keypress, trigger insertion of a single character.
358 351 // This simulates the old behavior of completion as you type,
359 352 // before events were disconnected and CodeMirror stopped
360 353 // receiving events while the completer is focused.
361 354
362 355 var that = this;
363 356 var code = event.keyCode;
364 357
365 358 // don't handle keypress if it's not a character (arrows on FF)
366 359 // or ENTER/TAB
367 360 if (event.charCode === 0 ||
368 code == key.ENTER ||
369 code == key.TAB
361 code == keycodes.enter ||
362 code == keycodes.tab
370 363 ) return;
371 364
372 365 var cur = this.editor.getCursor();
373 366 var completion = {
374 367 str: String.fromCharCode(event.which),
375 368 type: "introspection",
376 369 from: cur,
377 370 to: cur,
378 371 };
379 372 this.insert(completion);
380 373
381 374 this.close();
382 375 this.editor.focus();
383 376 setTimeout(function () {
384 377 that.carry_on_completion();
385 378 }, 50);
386 379 };
387 380
388 381 IPython.Completer = Completer;
389 382
390 383 return IPython;
391 384 }(IPython));
@@ -1,792 +1,598 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Keyboard management
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 // Setup global keycodes and inverse keycodes.
16
17 // See http://unixpapa.com/js/key.html for a complete description. The short of
18 // it is that there are different keycode sets. Firefox uses the "Mozilla keycodes"
19 // and Webkit/IE use the "IE keycodes". These keycode sets are mostly the same
20 // but have minor differences.
21
22 // These apply to Firefox, (Webkit and IE)
23 var _keycodes = {
24 'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73,
25 'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81, 'r': 82,
26 's': 83, 't': 84, 'u': 85, 'v': 86, 'w': 87, 'x': 88, 'y': 89, 'z': 90,
27 '1 !': 49, '2 @': 50, '3 #': 51, '4 $': 52, '5 %': 53, '6 ^': 54,
28 '7 &': 55, '8 *': 56, '9 (': 57, '0 )': 48,
29 '[ {': 219, '] }': 221, '` ~': 192, ', <': 188, '. >': 190, '/ ?': 191,
30 '\\ |': 220, '\' "': 222,
31 'numpad0': 96, 'numpad1': 97, 'numpad2': 98, 'numpad3': 99, 'numpad4': 100,
32 'numpad5': 101, 'numpad6': 102, 'numpad7': 103, 'numpad8': 104, 'numpad9': 105,
33 'multiply': 106, 'add': 107, 'subtract': 109, 'decimal': 110, 'divide': 111,
34 'f1': 112, 'f2': 113, 'f3': 114, 'f4': 115, 'f5': 116, 'f6': 117, 'f7': 118,
35 'f8': 119, 'f9': 120, 'f11': 122, 'f12': 123, 'f13': 124, 'f14': 125, 'f15': 126,
36 'backspace': 8, 'tab': 9, 'enter': 13, 'shift': 16, 'ctrl': 17, 'alt': 18,
37 'meta': 91, 'capslock': 20, 'esc': 27, 'space': 32, 'pageup': 33, 'pagedown': 34,
38 'end': 35, 'home': 36, 'left': 37, 'up': 38, 'right': 39, 'down': 40,
39 'insert': 45, 'delete': 46, 'numlock': 144,
40 };
41
42 // These apply to Firefox and Opera
43 var _mozilla_keycodes = {
44 '; :': 59, '= +': 61, '- _': 173, 'meta': 224
45 };
46
47 // This apply to Webkit and IE
48 var _ie_keycodes = {
49 '; :': 186, '= +': 187, '- _': 189,
50 };
51
52 15 var browser = IPython.utils.browser[0];
53 16 var platform = IPython.utils.platform;
54
55 if (browser === 'Firefox' || browser === 'Opera') {
56 $.extend(_keycodes, _mozilla_keycodes);
57 } else if (browser === 'Safari' || browser === 'Chrome' || browser === 'MSIE') {
58 $.extend(_keycodes, _ie_keycodes);
59 }
60
61 var keycodes = {};
62 var inv_keycodes = {};
63 for (var name in _keycodes) {
64 var names = name.split(' ');
65 if (names.length === 1) {
66 var n = names[0];
67 keycodes[n] = _keycodes[n];
68 inv_keycodes[_keycodes[n]] = n;
69 } else {
70 var primary = names[0];
71 var secondary = names[1];
72 keycodes[primary] = _keycodes[name];
73 keycodes[secondary] = _keycodes[name];
74 inv_keycodes[_keycodes[name]] = primary;
75 }
76 }
77
78 17
79 18 // Default keyboard shortcuts
80 19
81 20 var default_common_shortcuts = {
82 21 'shift' : {
83 22 help : '',
84 23 help_index : '',
85 24 handler : function (event) {
86 25 // ignore shift keydown
87 26 return true;
88 27 }
89 28 },
90 29 'shift+enter' : {
91 30 help : 'run cell, select below',
92 31 help_index : 'ba',
93 32 handler : function (event) {
94 33 IPython.notebook.execute_cell_and_select_below();
95 34 return false;
96 35 }
97 36 },
98 37 'ctrl+enter' : {
99 38 help : 'run cell',
100 39 help_index : 'bb',
101 40 handler : function (event) {
102 41 IPython.notebook.execute_cell();
103 42 return false;
104 43 }
105 44 },
106 45 'alt+enter' : {
107 46 help : 'run cell, insert below',
108 47 help_index : 'bc',
109 48 handler : function (event) {
110 49 IPython.notebook.execute_cell_and_insert_below();
111 50 return false;
112 51 }
113 52 }
114 53 };
115 54
116 55 if (platform === 'MacOS') {
117 56 default_common_shortcuts['cmd+s'] =
118 57 {
119 58 help : 'save notebook',
120 59 help_index : 'fb',
121 60 handler : function (event) {
122 61 IPython.notebook.save_checkpoint();
123 62 event.preventDefault();
124 63 return false;
125 64 }
126 65 };
127 66 } else {
128 67 default_common_shortcuts['ctrl+s'] =
129 68 {
130 69 help : 'save notebook',
131 70 help_index : 'fb',
132 71 handler : function (event) {
133 72 IPython.notebook.save_checkpoint();
134 73 event.preventDefault();
135 74 return false;
136 75 }
137 76 };
138 77 }
139 78
140 79 // Edit mode defaults
141 80
142 81 var default_edit_shortcuts = {
143 82 'esc' : {
144 83 help : 'command mode',
145 84 help_index : 'aa',
146 85 handler : function (event) {
147 86 IPython.notebook.command_mode();
148 87 IPython.notebook.focus_cell();
149 88 return false;
150 89 }
151 90 },
152 91 'ctrl+m' : {
153 92 help : 'command mode',
154 93 help_index : 'ab',
155 94 handler : function (event) {
156 95 IPython.notebook.command_mode();
157 96 IPython.notebook.focus_cell();
158 97 return false;
159 98 }
160 99 },
161 100 'up' : {
162 101 help : '',
163 102 help_index : '',
164 103 handler : function (event) {
165 104 var index = IPython.notebook.get_selected_index();
166 105 if (index !== null && index !== 0) {
167 106 var cell = IPython.notebook.get_cell(index);
168 107 if (cell && cell.at_top()) {
169 108 event.preventDefault();
170 109 IPython.notebook.command_mode();
171 110 IPython.notebook.select_prev();
172 111 IPython.notebook.edit_mode();
173 112 return false;
174 113 }
175 114 }
176 115 }
177 116 },
178 117 'down' : {
179 118 help : '',
180 119 help_index : '',
181 120 handler : function (event) {
182 121 var index = IPython.notebook.get_selected_index();
183 122 if (index !== null && index !== (IPython.notebook.ncells()-1)) {
184 123 var cell = IPython.notebook.get_cell(index);
185 124 if (cell && cell.at_bottom()) {
186 125 event.preventDefault();
187 126 IPython.notebook.command_mode();
188 127 IPython.notebook.select_next();
189 128 IPython.notebook.edit_mode();
190 129 return false;
191 130 }
192 131 }
193 132 }
194 133 },
195 134 'alt+-' : {
196 135 help : 'split cell',
197 136 help_index : 'ea',
198 137 handler : function (event) {
199 138 IPython.notebook.split_cell();
200 139 return false;
201 140 }
202 141 },
203 142 'alt+subtract' : {
204 143 help : '',
205 144 help_index : 'eb',
206 145 handler : function (event) {
207 146 IPython.notebook.split_cell();
208 147 return false;
209 148 }
210 149 },
211 150 'tab' : {
212 151 help : 'indent or complete',
213 152 help_index : 'ec',
214 153 },
215 154 'shift+tab' : {
216 155 help : 'tooltip',
217 156 help_index : 'ed',
218 157 },
219 158 };
220 159
221 160 if (platform === 'MacOS') {
222 161 default_edit_shortcuts['cmd+/'] =
223 162 {
224 163 help : 'toggle comment',
225 164 help_index : 'ee'
226 165 };
227 166 default_edit_shortcuts['cmd+]'] =
228 167 {
229 168 help : 'indent',
230 169 help_index : 'ef'
231 170 };
232 171 default_edit_shortcuts['cmd+['] =
233 172 {
234 173 help : 'dedent',
235 174 help_index : 'eg'
236 175 };
237 176 } else {
238 177 default_edit_shortcuts['ctrl+/'] =
239 178 {
240 179 help : 'toggle comment',
241 180 help_index : 'ee'
242 181 };
243 182 default_edit_shortcuts['ctrl+]'] =
244 183 {
245 184 help : 'indent',
246 185 help_index : 'ef'
247 186 };
248 187 default_edit_shortcuts['ctrl+['] =
249 188 {
250 189 help : 'dedent',
251 190 help_index : 'eg'
252 191 };
253 192 }
254 193
255 194 // Command mode defaults
256 195
257 196 var default_command_shortcuts = {
258 197 'enter' : {
259 198 help : 'edit mode',
260 199 help_index : 'aa',
261 200 handler : function (event) {
262 201 IPython.notebook.edit_mode();
263 202 return false;
264 203 }
265 204 },
266 205 'up' : {
267 206 help : 'select previous cell',
268 207 help_index : 'da',
269 208 handler : function (event) {
270 209 var index = IPython.notebook.get_selected_index();
271 210 if (index !== 0 && index !== null) {
272 211 IPython.notebook.select_prev();
273 212 IPython.notebook.focus_cell();
274 213 }
275 214 return false;
276 215 }
277 216 },
278 217 'down' : {
279 218 help : 'select next cell',
280 219 help_index : 'db',
281 220 handler : function (event) {
282 221 var index = IPython.notebook.get_selected_index();
283 222 if (index !== (IPython.notebook.ncells()-1) && index !== null) {
284 223 IPython.notebook.select_next();
285 224 IPython.notebook.focus_cell();
286 225 }
287 226 return false;
288 227 }
289 228 },
290 229 'k' : {
291 230 help : 'select previous cell',
292 231 help_index : 'dc',
293 232 handler : function (event) {
294 233 var index = IPython.notebook.get_selected_index();
295 234 if (index !== 0 && index !== null) {
296 235 IPython.notebook.select_prev();
297 236 IPython.notebook.focus_cell();
298 237 }
299 238 return false;
300 239 }
301 240 },
302 241 'j' : {
303 242 help : 'select next cell',
304 243 help_index : 'dd',
305 244 handler : function (event) {
306 245 var index = IPython.notebook.get_selected_index();
307 246 if (index !== (IPython.notebook.ncells()-1) && index !== null) {
308 247 IPython.notebook.select_next();
309 248 IPython.notebook.focus_cell();
310 249 }
311 250 return false;
312 251 }
313 252 },
314 253 'x' : {
315 254 help : 'cut cell',
316 255 help_index : 'ee',
317 256 handler : function (event) {
318 257 IPython.notebook.cut_cell();
319 258 return false;
320 259 }
321 260 },
322 261 'c' : {
323 262 help : 'copy cell',
324 263 help_index : 'ef',
325 264 handler : function (event) {
326 265 IPython.notebook.copy_cell();
327 266 return false;
328 267 }
329 268 },
330 269 'shift+v' : {
331 270 help : 'paste cell above',
332 271 help_index : 'eg',
333 272 handler : function (event) {
334 273 IPython.notebook.paste_cell_above();
335 274 return false;
336 275 }
337 276 },
338 277 'v' : {
339 278 help : 'paste cell below',
340 279 help_index : 'eh',
341 280 handler : function (event) {
342 281 IPython.notebook.paste_cell_below();
343 282 return false;
344 283 }
345 284 },
346 285 'd' : {
347 286 help : 'delete cell (press twice)',
348 287 help_index : 'ej',
349 288 count: 2,
350 289 handler : function (event) {
351 290 IPython.notebook.delete_cell();
352 291 return false;
353 292 }
354 293 },
355 294 'a' : {
356 295 help : 'insert cell above',
357 296 help_index : 'ec',
358 297 handler : function (event) {
359 298 IPython.notebook.insert_cell_above('code');
360 299 IPython.notebook.select_prev();
361 300 IPython.notebook.focus_cell();
362 301 return false;
363 302 }
364 303 },
365 304 'b' : {
366 305 help : 'insert cell below',
367 306 help_index : 'ed',
368 307 handler : function (event) {
369 308 IPython.notebook.insert_cell_below('code');
370 309 IPython.notebook.select_next();
371 310 IPython.notebook.focus_cell();
372 311 return false;
373 312 }
374 313 },
375 314 'y' : {
376 315 help : 'to code',
377 316 help_index : 'ca',
378 317 handler : function (event) {
379 318 IPython.notebook.to_code();
380 319 return false;
381 320 }
382 321 },
383 322 'm' : {
384 323 help : 'to markdown',
385 324 help_index : 'cb',
386 325 handler : function (event) {
387 326 IPython.notebook.to_markdown();
388 327 return false;
389 328 }
390 329 },
391 330 'r' : {
392 331 help : 'to raw',
393 332 help_index : 'cc',
394 333 handler : function (event) {
395 334 IPython.notebook.to_raw();
396 335 return false;
397 336 }
398 337 },
399 338 '1' : {
400 339 help : 'to heading 1',
401 340 help_index : 'cd',
402 341 handler : function (event) {
403 342 IPython.notebook.to_heading(undefined, 1);
404 343 return false;
405 344 }
406 345 },
407 346 '2' : {
408 347 help : 'to heading 2',
409 348 help_index : 'ce',
410 349 handler : function (event) {
411 350 IPython.notebook.to_heading(undefined, 2);
412 351 return false;
413 352 }
414 353 },
415 354 '3' : {
416 355 help : 'to heading 3',
417 356 help_index : 'cf',
418 357 handler : function (event) {
419 358 IPython.notebook.to_heading(undefined, 3);
420 359 return false;
421 360 }
422 361 },
423 362 '4' : {
424 363 help : 'to heading 4',
425 364 help_index : 'cg',
426 365 handler : function (event) {
427 366 IPython.notebook.to_heading(undefined, 4);
428 367 return false;
429 368 }
430 369 },
431 370 '5' : {
432 371 help : 'to heading 5',
433 372 help_index : 'ch',
434 373 handler : function (event) {
435 374 IPython.notebook.to_heading(undefined, 5);
436 375 return false;
437 376 }
438 377 },
439 378 '6' : {
440 379 help : 'to heading 6',
441 380 help_index : 'ci',
442 381 handler : function (event) {
443 382 IPython.notebook.to_heading(undefined, 6);
444 383 return false;
445 384 }
446 385 },
447 386 'o' : {
448 387 help : 'toggle output',
449 388 help_index : 'gb',
450 389 handler : function (event) {
451 390 IPython.notebook.toggle_output();
452 391 return false;
453 392 }
454 393 },
455 394 'shift+o' : {
456 395 help : 'toggle output scrolling',
457 396 help_index : 'gc',
458 397 handler : function (event) {
459 398 IPython.notebook.toggle_output_scroll();
460 399 return false;
461 400 }
462 401 },
463 402 's' : {
464 403 help : 'save notebook',
465 404 help_index : 'fa',
466 405 handler : function (event) {
467 406 IPython.notebook.save_checkpoint();
468 407 return false;
469 408 }
470 409 },
471 410 'ctrl+j' : {
472 411 help : 'move cell down',
473 412 help_index : 'eb',
474 413 handler : function (event) {
475 414 IPython.notebook.move_cell_down();
476 415 return false;
477 416 }
478 417 },
479 418 'ctrl+k' : {
480 419 help : 'move cell up',
481 420 help_index : 'ea',
482 421 handler : function (event) {
483 422 IPython.notebook.move_cell_up();
484 423 return false;
485 424 }
486 425 },
487 426 'l' : {
488 427 help : 'toggle line numbers',
489 428 help_index : 'ga',
490 429 handler : function (event) {
491 430 IPython.notebook.cell_toggle_line_numbers();
492 431 return false;
493 432 }
494 433 },
495 434 'i' : {
496 435 help : 'interrupt kernel (press twice)',
497 436 help_index : 'ha',
498 437 count: 2,
499 438 handler : function (event) {
500 439 IPython.notebook.kernel.interrupt();
501 440 return false;
502 441 }
503 442 },
504 443 '0' : {
505 444 help : 'restart kernel (press twice)',
506 445 help_index : 'hb',
507 446 count: 2,
508 447 handler : function (event) {
509 448 IPython.notebook.restart_kernel();
510 449 return false;
511 450 }
512 451 },
513 452 'h' : {
514 453 help : 'keyboard shortcuts',
515 454 help_index : 'ge',
516 455 handler : function (event) {
517 456 IPython.quick_help.show_keyboard_shortcuts();
518 457 return false;
519 458 }
520 459 },
521 460 'z' : {
522 461 help : 'undo last delete',
523 462 help_index : 'ei',
524 463 handler : function (event) {
525 464 IPython.notebook.undelete_cell();
526 465 return false;
527 466 }
528 467 },
529 468 'shift+m' : {
530 469 help : 'merge cell below',
531 470 help_index : 'ek',
532 471 handler : function (event) {
533 472 IPython.notebook.merge_cell_below();
534 473 return false;
535 474 }
536 475 },
537 476 'q' : {
538 477 help : 'close pager',
539 478 help_index : 'gd',
540 479 handler : function (event) {
541 480 IPython.pager.collapse();
542 481 return false;
543 482 }
544 483 },
545 484 };
546 485
547 486
548 // Shortcut manager class
549
550 var ShortcutManager = function (delay) {
551 this._shortcuts = {};
552 this._counts = {};
553 this._timers = {};
554 this.delay = delay || 800; // delay in milliseconds
555 };
556
557 ShortcutManager.prototype.help = function () {
558 var help = [];
559 for (var shortcut in this._shortcuts) {
560 var help_string = this._shortcuts[shortcut].help;
561 var help_index = this._shortcuts[shortcut].help_index;
562 if (help_string) {
563 if (platform === 'MacOS') {
564 shortcut = shortcut.replace('meta', 'cmd');
565 }
566 help.push({
567 shortcut: shortcut,
568 help: help_string,
569 help_index: help_index}
570 );
571 }
572 }
573 help.sort(function (a, b) {
574 if (a.help_index > b.help_index)
575 return 1;
576 if (a.help_index < b.help_index)
577 return -1;
578 return 0;
579 });
580 return help;
581 };
582
583 ShortcutManager.prototype.normalize_key = function (key) {
584 return inv_keycodes[keycodes[key]];
585 };
586
587 ShortcutManager.prototype.normalize_shortcut = function (shortcut) {
588 // Sort a sequence of + separated modifiers into the order alt+ctrl+meta+shift
589 shortcut = shortcut.replace('cmd', 'meta').toLowerCase();
590 var values = shortcut.split("+");
591 if (values.length === 1) {
592 return this.normalize_key(values[0]);
593 } else {
594 var modifiers = values.slice(0,-1);
595 var key = this.normalize_key(values[values.length-1]);
596 modifiers.sort();
597 return modifiers.join('+') + '+' + key;
598 }
599 };
600
601 ShortcutManager.prototype.event_to_shortcut = function (event) {
602 // Convert a jQuery keyboard event to a strong based keyboard shortcut
603 var shortcut = '';
604 var key = inv_keycodes[event.which];
605 if (event.altKey && key !== 'alt') {shortcut += 'alt+';}
606 if (event.ctrlKey && key !== 'ctrl') {shortcut += 'ctrl+';}
607 if (event.metaKey && key !== 'meta') {shortcut += 'meta+';}
608 if (event.shiftKey && key !== 'shift') {shortcut += 'shift+';}
609 shortcut += key;
610 return shortcut;
611 };
612
613 ShortcutManager.prototype.clear_shortcuts = function () {
614 this._shortcuts = {};
615 };
616
617 ShortcutManager.prototype.add_shortcut = function (shortcut, data) {
618 if (typeof(data) === 'function') {
619 data = {help: '', help_index: '', handler: data};
620 }
621 data.help_index = data.help_index || '';
622 data.help = data.help || '';
623 data.count = data.count || 1;
624 if (data.help_index === '') {
625 data.help_index = 'zz';
626 }
627 shortcut = this.normalize_shortcut(shortcut);
628 this._counts[shortcut] = 0;
629 this._shortcuts[shortcut] = data;
630 };
631
632 ShortcutManager.prototype.add_shortcuts = function (data) {
633 for (var shortcut in data) {
634 this.add_shortcut(shortcut, data[shortcut]);
635 }
636 };
637
638 ShortcutManager.prototype.remove_shortcut = function (shortcut) {
639 shortcut = this.normalize_shortcut(shortcut);
640 delete this._counts[shortcut];
641 delete this._shortcuts[shortcut];
642 };
643
644 ShortcutManager.prototype.count_handler = function (shortcut, event, data) {
645 var that = this;
646 var c = this._counts;
647 var t = this._timers;
648 var timer = null;
649 if (c[shortcut] === data.count-1) {
650 c[shortcut] = 0;
651 timer = t[shortcut];
652 if (timer) {clearTimeout(timer); delete t[shortcut];}
653 return data.handler(event);
654 } else {
655 c[shortcut] = c[shortcut] + 1;
656 timer = setTimeout(function () {
657 c[shortcut] = 0;
658 }, that.delay);
659 t[shortcut] = timer;
660 }
661 return false;
662 };
663
664 ShortcutManager.prototype.call_handler = function (event) {
665 var shortcut = this.event_to_shortcut(event);
666 var data = this._shortcuts[shortcut];
667 if (data) {
668 var handler = data.handler;
669 if (handler) {
670 if (data.count === 1) {
671 return handler(event);
672 } else if (data.count > 1) {
673 return this.count_handler(shortcut, event, data);
674 }
675 }
676 }
677 return true;
678 };
487 // Main keyboard manager for the notebook
679 488
489 var ShortcutManager = IPython.keyboard.ShortcutManager;
490 var keycodes = IPython.keyboard.keycodes;
680 491
681 // Main keyboard manager for the notebook
682
683 492 var KeyboardManager = function () {
684 493 this.mode = 'command';
685 494 this.enabled = true;
686 495 this.bind_events();
687 496 this.command_shortcuts = new ShortcutManager();
688 497 this.command_shortcuts.add_shortcuts(default_common_shortcuts);
689 498 this.command_shortcuts.add_shortcuts(default_command_shortcuts);
690 499 this.edit_shortcuts = new ShortcutManager();
691 500 this.edit_shortcuts.add_shortcuts(default_common_shortcuts);
692 501 this.edit_shortcuts.add_shortcuts(default_edit_shortcuts);
693 502 };
694 503
695 504 KeyboardManager.prototype.bind_events = function () {
696 505 var that = this;
697 506 $(document).keydown(function (event) {
698 507 return that.handle_keydown(event);
699 508 });
700 509 };
701 510
702 511 KeyboardManager.prototype.handle_keydown = function (event) {
703 512 var notebook = IPython.notebook;
704 513
705 514 if (event.which === keycodes.esc) {
706 515 // Intercept escape at highest level to avoid closing
707 516 // websocket connection with firefox
708 517 event.preventDefault();
709 518 }
710 519
711 520 if (!this.enabled) {
712 521 if (event.which === keycodes.esc) {
713 522 // ESC
714 523 notebook.command_mode();
715 524 return false;
716 525 }
717 526 return true;
718 527 }
719 528
720 529 if (this.mode === 'edit') {
721 530 return this.edit_shortcuts.call_handler(event);
722 531 } else if (this.mode === 'command') {
723 532 return this.command_shortcuts.call_handler(event);
724 533 }
725 534 return true;
726 535 };
727 536
728 537 KeyboardManager.prototype.edit_mode = function () {
729 538 this.last_mode = this.mode;
730 539 this.mode = 'edit';
731 540 };
732 541
733 542 KeyboardManager.prototype.command_mode = function () {
734 543 this.last_mode = this.mode;
735 544 this.mode = 'command';
736 545 };
737 546
738 547 KeyboardManager.prototype.enable = function () {
739 548 this.enabled = true;
740 549 };
741 550
742 551 KeyboardManager.prototype.disable = function () {
743 552 this.enabled = false;
744 553 };
745 554
746 555 KeyboardManager.prototype.register_events = function (e) {
747 556 var that = this;
748 557 var handle_focus = function () {
749 558 that.disable();
750 559 };
751 560 var handle_blur = function () {
752 561 that.enable();
753 562 };
754 563 e.on('focusin', handle_focus);
755 564 e.on('focusout', handle_blur);
756 565 // TODO: Very strange. The focusout event does not seem fire for the
757 566 // bootstrap textboxes on FF25&26... This works around that by
758 567 // registering focus and blur events recursively on all inputs within
759 568 // registered element.
760 569 e.find('input').blur(handle_blur);
761 570 e.on('DOMNodeInserted', function (event) {
762 571 var target = $(event.target);
763 572 if (target.is('input')) {
764 573 target.blur(handle_blur);
765 574 } else {
766 575 target.find('input').blur(handle_blur);
767 576 }
768 577 });
769 578 // There are times (raw_input) where we remove the element from the DOM before
770 579 // focusout is called. In this case we bind to the remove event of jQueryUI,
771 580 // which gets triggered upon removal, iff it is focused at the time.
772 581 // is_focused must be used to check for the case where an element within
773 582 // the element being removed is focused.
774 583 e.on('remove', function () {
775 584 if (IPython.utils.is_focused(e[0])) {
776 585 that.enable();
777 586 }
778 587 });
779 588 };
780 589
781 590
782 IPython.keycodes = keycodes;
783 IPython.inv_keycodes = inv_keycodes;
784 591 IPython.default_common_shortcuts = default_common_shortcuts;
785 592 IPython.default_edit_shortcuts = default_edit_shortcuts;
786 593 IPython.default_command_shortcuts = default_command_shortcuts;
787 IPython.ShortcutManager = ShortcutManager;
788 594 IPython.KeyboardManager = KeyboardManager;
789 595
790 596 return IPython;
791 597
792 598 }(IPython));
@@ -1,2332 +1,2332 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Notebook
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 var utils = IPython.utils;
16 16
17 17 /**
18 18 * A notebook contains and manages cells.
19 19 *
20 20 * @class Notebook
21 21 * @constructor
22 22 * @param {String} selector A jQuery selector for the notebook's DOM element
23 23 * @param {Object} [options] A config object
24 24 */
25 25 var Notebook = function (selector, options) {
26 26 this.options = options = options || {};
27 27 this.base_url = options.base_url;
28 28 this.notebook_path = options.notebook_path;
29 29 this.notebook_name = options.notebook_name;
30 30 this.element = $(selector);
31 31 this.element.scroll();
32 32 this.element.data("notebook", this);
33 33 this.next_prompt_number = 1;
34 34 this.session = null;
35 35 this.kernel = null;
36 36 this.clipboard = null;
37 37 this.undelete_backup = null;
38 38 this.undelete_index = null;
39 39 this.undelete_below = false;
40 40 this.paste_enabled = false;
41 41 // It is important to start out in command mode to match the intial mode
42 42 // of the KeyboardManager.
43 43 this.mode = 'command';
44 44 this.set_dirty(false);
45 45 this.metadata = {};
46 46 this._checkpoint_after_save = false;
47 47 this.last_checkpoint = null;
48 48 this.checkpoints = [];
49 49 this.autosave_interval = 0;
50 50 this.autosave_timer = null;
51 51 // autosave *at most* every two minutes
52 52 this.minimum_autosave_interval = 120000;
53 53 // single worksheet for now
54 54 this.worksheet_metadata = {};
55 55 this.notebook_name_blacklist_re = /[\/\\:]/;
56 56 this.nbformat = 3; // Increment this when changing the nbformat
57 57 this.nbformat_minor = 0; // Increment this when changing the nbformat
58 58 this.style();
59 59 this.create_elements();
60 60 this.bind_events();
61 61 };
62 62
63 63 /**
64 64 * Tweak the notebook's CSS style.
65 65 *
66 66 * @method style
67 67 */
68 68 Notebook.prototype.style = function () {
69 69 $('div#notebook').addClass('border-box-sizing');
70 70 };
71 71
72 72 /**
73 73 * Create an HTML and CSS representation of the notebook.
74 74 *
75 75 * @method create_elements
76 76 */
77 77 Notebook.prototype.create_elements = function () {
78 78 var that = this;
79 79 this.element.attr('tabindex','-1');
80 80 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
81 81 // We add this end_space div to the end of the notebook div to:
82 82 // i) provide a margin between the last cell and the end of the notebook
83 83 // ii) to prevent the div from scrolling up when the last cell is being
84 84 // edited, but is too low on the page, which browsers will do automatically.
85 85 var end_space = $('<div/>').addClass('end_space');
86 86 end_space.dblclick(function (e) {
87 87 var ncells = that.ncells();
88 88 that.insert_cell_below('code',ncells-1);
89 89 });
90 90 this.element.append(this.container);
91 91 this.container.append(end_space);
92 92 };
93 93
94 94 /**
95 95 * Bind JavaScript events: key presses and custom IPython events.
96 96 *
97 97 * @method bind_events
98 98 */
99 99 Notebook.prototype.bind_events = function () {
100 100 var that = this;
101 101
102 102 $([IPython.events]).on('set_next_input.Notebook', function (event, data) {
103 103 var index = that.find_cell_index(data.cell);
104 104 var new_cell = that.insert_cell_below('code',index);
105 105 new_cell.set_text(data.text);
106 106 that.dirty = true;
107 107 });
108 108
109 109 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
110 110 that.dirty = data.value;
111 111 });
112 112
113 113 $([IPython.events]).on('select.Cell', function (event, data) {
114 114 var index = that.find_cell_index(data.cell);
115 115 that.select(index);
116 116 });
117 117
118 118 $([IPython.events]).on('edit_mode.Cell', function (event, data) {
119 119 that.handle_edit_mode(that.find_cell_index(data.cell));
120 120 });
121 121
122 122 $([IPython.events]).on('command_mode.Cell', function (event, data) {
123 123 that.command_mode();
124 124 });
125 125
126 126 $([IPython.events]).on('status_autorestarting.Kernel', function () {
127 127 IPython.dialog.modal({
128 128 title: "Kernel Restarting",
129 129 body: "The kernel appears to have died. It will restart automatically.",
130 130 buttons: {
131 131 OK : {
132 132 class : "btn-primary"
133 133 }
134 134 }
135 135 });
136 136 });
137 137
138 138 var collapse_time = function (time) {
139 139 var app_height = $('#ipython-main-app').height(); // content height
140 140 var splitter_height = $('div#pager_splitter').outerHeight(true);
141 141 var new_height = app_height - splitter_height;
142 142 that.element.animate({height : new_height + 'px'}, time);
143 143 };
144 144
145 145 this.element.bind('collapse_pager', function (event, extrap) {
146 146 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
147 147 collapse_time(time);
148 148 });
149 149
150 150 var expand_time = function (time) {
151 151 var app_height = $('#ipython-main-app').height(); // content height
152 152 var splitter_height = $('div#pager_splitter').outerHeight(true);
153 153 var pager_height = $('div#pager').outerHeight(true);
154 154 var new_height = app_height - pager_height - splitter_height;
155 155 that.element.animate({height : new_height + 'px'}, time);
156 156 };
157 157
158 158 this.element.bind('expand_pager', function (event, extrap) {
159 159 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
160 160 expand_time(time);
161 161 });
162 162
163 163 // Firefox 22 broke $(window).on("beforeunload")
164 164 // I'm not sure why or how.
165 165 window.onbeforeunload = function (e) {
166 166 // TODO: Make killing the kernel configurable.
167 167 var kill_kernel = false;
168 168 if (kill_kernel) {
169 169 that.session.kill_kernel();
170 170 }
171 171 // if we are autosaving, trigger an autosave on nav-away.
172 172 // still warn, because if we don't the autosave may fail.
173 173 if (that.dirty) {
174 174 if ( that.autosave_interval ) {
175 175 // schedule autosave in a timeout
176 176 // this gives you a chance to forcefully discard changes
177 177 // by reloading the page if you *really* want to.
178 178 // the timer doesn't start until you *dismiss* the dialog.
179 179 setTimeout(function () {
180 180 if (that.dirty) {
181 181 that.save_notebook();
182 182 }
183 183 }, 1000);
184 184 return "Autosave in progress, latest changes may be lost.";
185 185 } else {
186 186 return "Unsaved changes will be lost.";
187 187 }
188 188 }
189 189 // Null is the *only* return value that will make the browser not
190 190 // pop up the "don't leave" dialog.
191 191 return null;
192 192 };
193 193 };
194 194
195 195 /**
196 196 * Set the dirty flag, and trigger the set_dirty.Notebook event
197 197 *
198 198 * @method set_dirty
199 199 */
200 200 Notebook.prototype.set_dirty = function (value) {
201 201 if (value === undefined) {
202 202 value = true;
203 203 }
204 204 if (this.dirty == value) {
205 205 return;
206 206 }
207 207 $([IPython.events]).trigger('set_dirty.Notebook', {value: value});
208 208 };
209 209
210 210 /**
211 211 * Scroll the top of the page to a given cell.
212 212 *
213 213 * @method scroll_to_cell
214 214 * @param {Number} cell_number An index of the cell to view
215 215 * @param {Number} time Animation time in milliseconds
216 216 * @return {Number} Pixel offset from the top of the container
217 217 */
218 218 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
219 219 var cells = this.get_cells();
220 220 time = time || 0;
221 221 cell_number = Math.min(cells.length-1,cell_number);
222 222 cell_number = Math.max(0 ,cell_number);
223 223 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
224 224 this.element.animate({scrollTop:scroll_value}, time);
225 225 return scroll_value;
226 226 };
227 227
228 228 /**
229 229 * Scroll to the bottom of the page.
230 230 *
231 231 * @method scroll_to_bottom
232 232 */
233 233 Notebook.prototype.scroll_to_bottom = function () {
234 234 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
235 235 };
236 236
237 237 /**
238 238 * Scroll to the top of the page.
239 239 *
240 240 * @method scroll_to_top
241 241 */
242 242 Notebook.prototype.scroll_to_top = function () {
243 243 this.element.animate({scrollTop:0}, 0);
244 244 };
245 245
246 246 // Edit Notebook metadata
247 247
248 248 Notebook.prototype.edit_metadata = function () {
249 249 var that = this;
250 250 IPython.dialog.edit_metadata(this.metadata, function (md) {
251 251 that.metadata = md;
252 252 }, 'Notebook');
253 253 };
254 254
255 255 // Cell indexing, retrieval, etc.
256 256
257 257 /**
258 258 * Get all cell elements in the notebook.
259 259 *
260 260 * @method get_cell_elements
261 261 * @return {jQuery} A selector of all cell elements
262 262 */
263 263 Notebook.prototype.get_cell_elements = function () {
264 264 return this.container.children("div.cell");
265 265 };
266 266
267 267 /**
268 268 * Get a particular cell element.
269 269 *
270 270 * @method get_cell_element
271 271 * @param {Number} index An index of a cell to select
272 272 * @return {jQuery} A selector of the given cell.
273 273 */
274 274 Notebook.prototype.get_cell_element = function (index) {
275 275 var result = null;
276 276 var e = this.get_cell_elements().eq(index);
277 277 if (e.length !== 0) {
278 278 result = e;
279 279 }
280 280 return result;
281 281 };
282 282
283 283 /**
284 284 * Try to get a particular cell by msg_id.
285 285 *
286 286 * @method get_msg_cell
287 287 * @param {String} msg_id A message UUID
288 288 * @return {Cell} Cell or null if no cell was found.
289 289 */
290 290 Notebook.prototype.get_msg_cell = function (msg_id) {
291 291 return IPython.CodeCell.msg_cells[msg_id] || null;
292 292 };
293 293
294 294 /**
295 295 * Count the cells in this notebook.
296 296 *
297 297 * @method ncells
298 298 * @return {Number} The number of cells in this notebook
299 299 */
300 300 Notebook.prototype.ncells = function () {
301 301 return this.get_cell_elements().length;
302 302 };
303 303
304 304 /**
305 305 * Get all Cell objects in this notebook.
306 306 *
307 307 * @method get_cells
308 308 * @return {Array} This notebook's Cell objects
309 309 */
310 310 // TODO: we are often calling cells as cells()[i], which we should optimize
311 311 // to cells(i) or a new method.
312 312 Notebook.prototype.get_cells = function () {
313 313 return this.get_cell_elements().toArray().map(function (e) {
314 314 return $(e).data("cell");
315 315 });
316 316 };
317 317
318 318 /**
319 319 * Get a Cell object from this notebook.
320 320 *
321 321 * @method get_cell
322 322 * @param {Number} index An index of a cell to retrieve
323 323 * @return {Cell} A particular cell
324 324 */
325 325 Notebook.prototype.get_cell = function (index) {
326 326 var result = null;
327 327 var ce = this.get_cell_element(index);
328 328 if (ce !== null) {
329 329 result = ce.data('cell');
330 330 }
331 331 return result;
332 332 };
333 333
334 334 /**
335 335 * Get the cell below a given cell.
336 336 *
337 337 * @method get_next_cell
338 338 * @param {Cell} cell The provided cell
339 339 * @return {Cell} The next cell
340 340 */
341 341 Notebook.prototype.get_next_cell = function (cell) {
342 342 var result = null;
343 343 var index = this.find_cell_index(cell);
344 344 if (this.is_valid_cell_index(index+1)) {
345 345 result = this.get_cell(index+1);
346 346 }
347 347 return result;
348 348 };
349 349
350 350 /**
351 351 * Get the cell above a given cell.
352 352 *
353 353 * @method get_prev_cell
354 354 * @param {Cell} cell The provided cell
355 355 * @return {Cell} The previous cell
356 356 */
357 357 Notebook.prototype.get_prev_cell = function (cell) {
358 358 // TODO: off-by-one
359 359 // nb.get_prev_cell(nb.get_cell(1)) is null
360 360 var result = null;
361 361 var index = this.find_cell_index(cell);
362 362 if (index !== null && index > 1) {
363 363 result = this.get_cell(index-1);
364 364 }
365 365 return result;
366 366 };
367 367
368 368 /**
369 369 * Get the numeric index of a given cell.
370 370 *
371 371 * @method find_cell_index
372 372 * @param {Cell} cell The provided cell
373 373 * @return {Number} The cell's numeric index
374 374 */
375 375 Notebook.prototype.find_cell_index = function (cell) {
376 376 var result = null;
377 377 this.get_cell_elements().filter(function (index) {
378 378 if ($(this).data("cell") === cell) {
379 379 result = index;
380 380 }
381 381 });
382 382 return result;
383 383 };
384 384
385 385 /**
386 386 * Get a given index , or the selected index if none is provided.
387 387 *
388 388 * @method index_or_selected
389 389 * @param {Number} index A cell's index
390 390 * @return {Number} The given index, or selected index if none is provided.
391 391 */
392 392 Notebook.prototype.index_or_selected = function (index) {
393 393 var i;
394 394 if (index === undefined || index === null) {
395 395 i = this.get_selected_index();
396 396 if (i === null) {
397 397 i = 0;
398 398 }
399 399 } else {
400 400 i = index;
401 401 }
402 402 return i;
403 403 };
404 404
405 405 /**
406 406 * Get the currently selected cell.
407 407 * @method get_selected_cell
408 408 * @return {Cell} The selected cell
409 409 */
410 410 Notebook.prototype.get_selected_cell = function () {
411 411 var index = this.get_selected_index();
412 412 return this.get_cell(index);
413 413 };
414 414
415 415 /**
416 416 * Check whether a cell index is valid.
417 417 *
418 418 * @method is_valid_cell_index
419 419 * @param {Number} index A cell index
420 420 * @return True if the index is valid, false otherwise
421 421 */
422 422 Notebook.prototype.is_valid_cell_index = function (index) {
423 423 if (index !== null && index >= 0 && index < this.ncells()) {
424 424 return true;
425 425 } else {
426 426 return false;
427 427 }
428 428 };
429 429
430 430 /**
431 431 * Get the index of the currently selected cell.
432 432
433 433 * @method get_selected_index
434 434 * @return {Number} The selected cell's numeric index
435 435 */
436 436 Notebook.prototype.get_selected_index = function () {
437 437 var result = null;
438 438 this.get_cell_elements().filter(function (index) {
439 439 if ($(this).data("cell").selected === true) {
440 440 result = index;
441 441 }
442 442 });
443 443 return result;
444 444 };
445 445
446 446
447 447 // Cell selection.
448 448
449 449 /**
450 450 * Programmatically select a cell.
451 451 *
452 452 * @method select
453 453 * @param {Number} index A cell's index
454 454 * @return {Notebook} This notebook
455 455 */
456 456 Notebook.prototype.select = function (index) {
457 457 if (this.is_valid_cell_index(index)) {
458 458 var sindex = this.get_selected_index();
459 459 if (sindex !== null && index !== sindex) {
460 460 this.get_cell(sindex).unselect();
461 461 }
462 462 var cell = this.get_cell(index);
463 463 cell.select();
464 464 if (cell.cell_type === 'heading') {
465 465 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
466 466 {'cell_type':cell.cell_type,level:cell.level}
467 467 );
468 468 } else {
469 469 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
470 470 {'cell_type':cell.cell_type}
471 471 );
472 472 }
473 473 }
474 474 return this;
475 475 };
476 476
477 477 /**
478 478 * Programmatically select the next cell.
479 479 *
480 480 * @method select_next
481 481 * @return {Notebook} This notebook
482 482 */
483 483 Notebook.prototype.select_next = function () {
484 484 var index = this.get_selected_index();
485 485 this.select(index+1);
486 486 return this;
487 487 };
488 488
489 489 /**
490 490 * Programmatically select the previous cell.
491 491 *
492 492 * @method select_prev
493 493 * @return {Notebook} This notebook
494 494 */
495 495 Notebook.prototype.select_prev = function () {
496 496 var index = this.get_selected_index();
497 497 this.select(index-1);
498 498 return this;
499 499 };
500 500
501 501
502 502 // Edit/Command mode
503 503
504 504 /**
505 505 * Gets the index of the cell that is in edit mode.
506 506 *
507 507 * @method get_edit_index
508 508 *
509 509 * @return index {int}
510 510 **/
511 511 Notebook.prototype.get_edit_index = function () {
512 512 var result = null;
513 513 this.get_cell_elements().filter(function (index) {
514 514 if ($(this).data("cell").mode === 'edit') {
515 515 result = index;
516 516 }
517 517 });
518 518 return result;
519 519 };
520 520
521 521 /**
522 522 * Make the notebook enter command mode.
523 523 *
524 524 * @method command_mode
525 525 **/
526 526 Notebook.prototype.command_mode = function () {
527 527 // Make sure there isn't an edit mode cell lingering around.
528 528 var cell = this.get_cell(this.get_edit_index());
529 529 if (cell) {
530 530 cell.command_mode();
531 531 }
532 532
533 533 // Notify the keyboard manager if this is a change of mode for the
534 534 // notebook as a whole.
535 535 if (this.mode !== 'command') {
536 536 this.mode = 'command';
537 537 $([IPython.events]).trigger('command_mode.Notebook');
538 538 IPython.keyboard_manager.command_mode();
539 539 }
540 540 };
541 541
542 542 /**
543 543 * Handle when a cell fires it's edit_mode event.
544 544 *
545 545 * @method handle_edit_mode
546 546 * @param [index] {int} Cell index to select. If no index is provided,
547 547 * the current selected cell is used.
548 548 **/
549 549 Notebook.prototype.handle_edit_mode = function (index) {
550 550 // Make sure the cell exists.
551 551 var cell = this.get_cell(index);
552 552 if (cell === null) { return; }
553 553
554 554 // Set the cell to edit mode and notify the keyboard manager if this
555 555 // is a change of mode for the notebook as a whole.
556 556 if (this.mode !== 'edit') {
557 557 cell.edit_mode();
558 558 this.mode = 'edit';
559 559 $([IPython.events]).trigger('edit_mode.Notebook');
560 560 IPython.keyboard_manager.edit_mode();
561 561 }
562 562 };
563 563
564 564 /**
565 565 * Make a cell enter edit mode.
566 566 *
567 567 * @method edit_mode
568 568 * @param [index] {int} Cell index to select. If no index is provided,
569 569 * the current selected cell is used.
570 570 **/
571 571 Notebook.prototype.edit_mode = function (index) {
572 572 if (index===undefined) {
573 573 index = this.get_selected_index();
574 574 }
575 575 // Make sure the cell exists.
576 576 var cell = this.get_cell(index);
577 577 if (cell === null) { return; }
578 578 if (cell.mode != 'edit') {
579 579 cell.unrender();
580 580 cell.focus_editor();
581 581 }
582 582 };
583 583
584 584 /**
585 585 * Focus the currently selected cell.
586 586 *
587 587 * @method focus_cell
588 588 **/
589 589 Notebook.prototype.focus_cell = function () {
590 590 var cell = this.get_selected_cell();
591 591 if (cell === null) {return;} // No cell is selected
592 592 cell.focus_cell();
593 593 };
594 594
595 595 // Cell movement
596 596
597 597 /**
598 598 * Move given (or selected) cell up and select it.
599 599 *
600 600 * @method move_cell_up
601 601 * @param [index] {integer} cell index
602 602 * @return {Notebook} This notebook
603 603 **/
604 604 Notebook.prototype.move_cell_up = function (index) {
605 605 var i = this.index_or_selected(index);
606 606 if (this.is_valid_cell_index(i) && i > 0) {
607 607 var pivot = this.get_cell_element(i-1);
608 608 var tomove = this.get_cell_element(i);
609 609 if (pivot !== null && tomove !== null) {
610 610 tomove.detach();
611 611 pivot.before(tomove);
612 612 this.select(i-1);
613 613 var cell = this.get_selected_cell();
614 614 cell.focus_cell();
615 615 }
616 616 this.set_dirty(true);
617 617 }
618 618 return this;
619 619 };
620 620
621 621
622 622 /**
623 623 * Move given (or selected) cell down and select it
624 624 *
625 625 * @method move_cell_down
626 626 * @param [index] {integer} cell index
627 627 * @return {Notebook} This notebook
628 628 **/
629 629 Notebook.prototype.move_cell_down = function (index) {
630 630 var i = this.index_or_selected(index);
631 631 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
632 632 var pivot = this.get_cell_element(i+1);
633 633 var tomove = this.get_cell_element(i);
634 634 if (pivot !== null && tomove !== null) {
635 635 tomove.detach();
636 636 pivot.after(tomove);
637 637 this.select(i+1);
638 638 var cell = this.get_selected_cell();
639 639 cell.focus_cell();
640 640 }
641 641 }
642 642 this.set_dirty();
643 643 return this;
644 644 };
645 645
646 646
647 647 // Insertion, deletion.
648 648
649 649 /**
650 650 * Delete a cell from the notebook.
651 651 *
652 652 * @method delete_cell
653 653 * @param [index] A cell's numeric index
654 654 * @return {Notebook} This notebook
655 655 */
656 656 Notebook.prototype.delete_cell = function (index) {
657 657 var i = this.index_or_selected(index);
658 658 var cell = this.get_selected_cell();
659 659 this.undelete_backup = cell.toJSON();
660 660 $('#undelete_cell').removeClass('disabled');
661 661 if (this.is_valid_cell_index(i)) {
662 662 var old_ncells = this.ncells();
663 663 var ce = this.get_cell_element(i);
664 664 ce.remove();
665 665 if (i === 0) {
666 666 // Always make sure we have at least one cell.
667 667 if (old_ncells === 1) {
668 668 this.insert_cell_below('code');
669 669 }
670 670 this.select(0);
671 671 this.undelete_index = 0;
672 672 this.undelete_below = false;
673 673 } else if (i === old_ncells-1 && i !== 0) {
674 674 this.select(i-1);
675 675 this.undelete_index = i - 1;
676 676 this.undelete_below = true;
677 677 } else {
678 678 this.select(i);
679 679 this.undelete_index = i;
680 680 this.undelete_below = false;
681 681 }
682 682 $([IPython.events]).trigger('delete.Cell', {'cell': cell, 'index': i});
683 683 this.set_dirty(true);
684 684 }
685 685 return this;
686 686 };
687 687
688 688 /**
689 689 * Restore the most recently deleted cell.
690 690 *
691 691 * @method undelete
692 692 */
693 693 Notebook.prototype.undelete_cell = function() {
694 694 if (this.undelete_backup !== null && this.undelete_index !== null) {
695 695 var current_index = this.get_selected_index();
696 696 if (this.undelete_index < current_index) {
697 697 current_index = current_index + 1;
698 698 }
699 699 if (this.undelete_index >= this.ncells()) {
700 700 this.select(this.ncells() - 1);
701 701 }
702 702 else {
703 703 this.select(this.undelete_index);
704 704 }
705 705 var cell_data = this.undelete_backup;
706 706 var new_cell = null;
707 707 if (this.undelete_below) {
708 708 new_cell = this.insert_cell_below(cell_data.cell_type);
709 709 } else {
710 710 new_cell = this.insert_cell_above(cell_data.cell_type);
711 711 }
712 712 new_cell.fromJSON(cell_data);
713 713 if (this.undelete_below) {
714 714 this.select(current_index+1);
715 715 } else {
716 716 this.select(current_index);
717 717 }
718 718 this.undelete_backup = null;
719 719 this.undelete_index = null;
720 720 }
721 721 $('#undelete_cell').addClass('disabled');
722 722 };
723 723
724 724 /**
725 725 * Insert a cell so that after insertion the cell is at given index.
726 726 *
727 727 * Similar to insert_above, but index parameter is mandatory
728 728 *
729 729 * Index will be brought back into the accissible range [0,n]
730 730 *
731 731 * @method insert_cell_at_index
732 732 * @param type {string} in ['code','markdown','heading']
733 733 * @param [index] {int} a valid index where to inser cell
734 734 *
735 735 * @return cell {cell|null} created cell or null
736 736 **/
737 737 Notebook.prototype.insert_cell_at_index = function(type, index){
738 738
739 739 var ncells = this.ncells();
740 740 index = Math.min(index,ncells);
741 741 index = Math.max(index,0);
742 742 var cell = null;
743 743
744 744 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
745 745 if (type === 'code') {
746 746 cell = new IPython.CodeCell(this.kernel);
747 747 cell.set_input_prompt();
748 748 } else if (type === 'markdown') {
749 749 cell = new IPython.MarkdownCell();
750 750 } else if (type === 'raw') {
751 751 cell = new IPython.RawCell();
752 752 } else if (type === 'heading') {
753 753 cell = new IPython.HeadingCell();
754 754 }
755 755
756 756 if(this._insert_element_at_index(cell.element,index)) {
757 757 cell.render();
758 758 $([IPython.events]).trigger('create.Cell', {'cell': cell, 'index': index});
759 759 cell.refresh();
760 760 // We used to select the cell after we refresh it, but there
761 761 // are now cases were this method is called where select is
762 762 // not appropriate. The selection logic should be handled by the
763 763 // caller of the the top level insert_cell methods.
764 764 this.set_dirty(true);
765 765 }
766 766 }
767 767 return cell;
768 768
769 769 };
770 770
771 771 /**
772 772 * Insert an element at given cell index.
773 773 *
774 774 * @method _insert_element_at_index
775 775 * @param element {dom element} a cell element
776 776 * @param [index] {int} a valid index where to inser cell
777 777 * @private
778 778 *
779 779 * return true if everything whent fine.
780 780 **/
781 781 Notebook.prototype._insert_element_at_index = function(element, index){
782 782 if (element === undefined){
783 783 return false;
784 784 }
785 785
786 786 var ncells = this.ncells();
787 787
788 788 if (ncells === 0) {
789 789 // special case append if empty
790 790 this.element.find('div.end_space').before(element);
791 791 } else if ( ncells === index ) {
792 792 // special case append it the end, but not empty
793 793 this.get_cell_element(index-1).after(element);
794 794 } else if (this.is_valid_cell_index(index)) {
795 795 // otherwise always somewhere to append to
796 796 this.get_cell_element(index).before(element);
797 797 } else {
798 798 return false;
799 799 }
800 800
801 801 if (this.undelete_index !== null && index <= this.undelete_index) {
802 802 this.undelete_index = this.undelete_index + 1;
803 803 this.set_dirty(true);
804 804 }
805 805 return true;
806 806 };
807 807
808 808 /**
809 809 * Insert a cell of given type above given index, or at top
810 810 * of notebook if index smaller than 0.
811 811 *
812 812 * default index value is the one of currently selected cell
813 813 *
814 814 * @method insert_cell_above
815 815 * @param type {string} cell type
816 816 * @param [index] {integer}
817 817 *
818 818 * @return handle to created cell or null
819 819 **/
820 820 Notebook.prototype.insert_cell_above = function (type, index) {
821 821 index = this.index_or_selected(index);
822 822 return this.insert_cell_at_index(type, index);
823 823 };
824 824
825 825 /**
826 826 * Insert a cell of given type below given index, or at bottom
827 827 * of notebook if index greater thatn number of cell
828 828 *
829 829 * default index value is the one of currently selected cell
830 830 *
831 831 * @method insert_cell_below
832 832 * @param type {string} cell type
833 833 * @param [index] {integer}
834 834 *
835 835 * @return handle to created cell or null
836 836 *
837 837 **/
838 838 Notebook.prototype.insert_cell_below = function (type, index) {
839 839 index = this.index_or_selected(index);
840 840 return this.insert_cell_at_index(type, index+1);
841 841 };
842 842
843 843
844 844 /**
845 845 * Insert cell at end of notebook
846 846 *
847 847 * @method insert_cell_at_bottom
848 848 * @param {String} type cell type
849 849 *
850 850 * @return the added cell; or null
851 851 **/
852 852 Notebook.prototype.insert_cell_at_bottom = function (type){
853 853 var len = this.ncells();
854 854 return this.insert_cell_below(type,len-1);
855 855 };
856 856
857 857 /**
858 858 * Turn a cell into a code cell.
859 859 *
860 860 * @method to_code
861 861 * @param {Number} [index] A cell's index
862 862 */
863 863 Notebook.prototype.to_code = function (index) {
864 864 var i = this.index_or_selected(index);
865 865 if (this.is_valid_cell_index(i)) {
866 866 var source_element = this.get_cell_element(i);
867 867 var source_cell = source_element.data("cell");
868 868 if (!(source_cell instanceof IPython.CodeCell)) {
869 869 var target_cell = this.insert_cell_below('code',i);
870 870 var text = source_cell.get_text();
871 871 if (text === source_cell.placeholder) {
872 872 text = '';
873 873 }
874 874 target_cell.set_text(text);
875 875 // make this value the starting point, so that we can only undo
876 876 // to this state, instead of a blank cell
877 877 target_cell.code_mirror.clearHistory();
878 878 source_element.remove();
879 879 this.select(i);
880 880 this.set_dirty(true);
881 881 }
882 882 }
883 883 };
884 884
885 885 /**
886 886 * Turn a cell into a Markdown cell.
887 887 *
888 888 * @method to_markdown
889 889 * @param {Number} [index] A cell's index
890 890 */
891 891 Notebook.prototype.to_markdown = function (index) {
892 892 var i = this.index_or_selected(index);
893 893 if (this.is_valid_cell_index(i)) {
894 894 var source_element = this.get_cell_element(i);
895 895 var source_cell = source_element.data("cell");
896 896 if (!(source_cell instanceof IPython.MarkdownCell)) {
897 897 var target_cell = this.insert_cell_below('markdown',i);
898 898 var text = source_cell.get_text();
899 899 if (text === source_cell.placeholder) {
900 900 text = '';
901 901 }
902 902 // We must show the editor before setting its contents
903 903 target_cell.unrender();
904 904 target_cell.set_text(text);
905 905 // make this value the starting point, so that we can only undo
906 906 // to this state, instead of a blank cell
907 907 target_cell.code_mirror.clearHistory();
908 908 source_element.remove();
909 909 this.select(i);
910 910 if ((source_cell instanceof IPython.TextCell) && source_cell.rendered) {
911 911 target_cell.render();
912 912 }
913 913 this.set_dirty(true);
914 914 }
915 915 }
916 916 };
917 917
918 918 /**
919 919 * Turn a cell into a raw text cell.
920 920 *
921 921 * @method to_raw
922 922 * @param {Number} [index] A cell's index
923 923 */
924 924 Notebook.prototype.to_raw = function (index) {
925 925 var i = this.index_or_selected(index);
926 926 if (this.is_valid_cell_index(i)) {
927 927 var source_element = this.get_cell_element(i);
928 928 var source_cell = source_element.data("cell");
929 929 var target_cell = null;
930 930 if (!(source_cell instanceof IPython.RawCell)) {
931 931 target_cell = this.insert_cell_below('raw',i);
932 932 var text = source_cell.get_text();
933 933 if (text === source_cell.placeholder) {
934 934 text = '';
935 935 }
936 936 // We must show the editor before setting its contents
937 937 target_cell.unrender();
938 938 target_cell.set_text(text);
939 939 // make this value the starting point, so that we can only undo
940 940 // to this state, instead of a blank cell
941 941 target_cell.code_mirror.clearHistory();
942 942 source_element.remove();
943 943 this.select(i);
944 944 this.set_dirty(true);
945 945 }
946 946 }
947 947 };
948 948
949 949 /**
950 950 * Turn a cell into a heading cell.
951 951 *
952 952 * @method to_heading
953 953 * @param {Number} [index] A cell's index
954 954 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
955 955 */
956 956 Notebook.prototype.to_heading = function (index, level) {
957 957 level = level || 1;
958 958 var i = this.index_or_selected(index);
959 959 if (this.is_valid_cell_index(i)) {
960 960 var source_element = this.get_cell_element(i);
961 961 var source_cell = source_element.data("cell");
962 962 var target_cell = null;
963 963 if (source_cell instanceof IPython.HeadingCell) {
964 964 source_cell.set_level(level);
965 965 } else {
966 966 target_cell = this.insert_cell_below('heading',i);
967 967 var text = source_cell.get_text();
968 968 if (text === source_cell.placeholder) {
969 969 text = '';
970 970 }
971 971 // We must show the editor before setting its contents
972 972 target_cell.set_level(level);
973 973 target_cell.unrender();
974 974 target_cell.set_text(text);
975 975 // make this value the starting point, so that we can only undo
976 976 // to this state, instead of a blank cell
977 977 target_cell.code_mirror.clearHistory();
978 978 source_element.remove();
979 979 this.select(i);
980 980 if ((source_cell instanceof IPython.TextCell) && source_cell.rendered) {
981 981 target_cell.render();
982 982 }
983 983 }
984 984 this.set_dirty(true);
985 985 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
986 986 {'cell_type':'heading',level:level}
987 987 );
988 988 }
989 989 };
990 990
991 991
992 992 // Cut/Copy/Paste
993 993
994 994 /**
995 995 * Enable UI elements for pasting cells.
996 996 *
997 997 * @method enable_paste
998 998 */
999 999 Notebook.prototype.enable_paste = function () {
1000 1000 var that = this;
1001 1001 if (!this.paste_enabled) {
1002 1002 $('#paste_cell_replace').removeClass('disabled')
1003 1003 .on('click', function () {that.paste_cell_replace();});
1004 1004 $('#paste_cell_above').removeClass('disabled')
1005 1005 .on('click', function () {that.paste_cell_above();});
1006 1006 $('#paste_cell_below').removeClass('disabled')
1007 1007 .on('click', function () {that.paste_cell_below();});
1008 1008 this.paste_enabled = true;
1009 1009 }
1010 1010 };
1011 1011
1012 1012 /**
1013 1013 * Disable UI elements for pasting cells.
1014 1014 *
1015 1015 * @method disable_paste
1016 1016 */
1017 1017 Notebook.prototype.disable_paste = function () {
1018 1018 if (this.paste_enabled) {
1019 1019 $('#paste_cell_replace').addClass('disabled').off('click');
1020 1020 $('#paste_cell_above').addClass('disabled').off('click');
1021 1021 $('#paste_cell_below').addClass('disabled').off('click');
1022 1022 this.paste_enabled = false;
1023 1023 }
1024 1024 };
1025 1025
1026 1026 /**
1027 1027 * Cut a cell.
1028 1028 *
1029 1029 * @method cut_cell
1030 1030 */
1031 1031 Notebook.prototype.cut_cell = function () {
1032 1032 this.copy_cell();
1033 1033 this.delete_cell();
1034 1034 };
1035 1035
1036 1036 /**
1037 1037 * Copy a cell.
1038 1038 *
1039 1039 * @method copy_cell
1040 1040 */
1041 1041 Notebook.prototype.copy_cell = function () {
1042 1042 var cell = this.get_selected_cell();
1043 1043 this.clipboard = cell.toJSON();
1044 1044 this.enable_paste();
1045 1045 };
1046 1046
1047 1047 /**
1048 1048 * Replace the selected cell with a cell in the clipboard.
1049 1049 *
1050 1050 * @method paste_cell_replace
1051 1051 */
1052 1052 Notebook.prototype.paste_cell_replace = function () {
1053 1053 if (this.clipboard !== null && this.paste_enabled) {
1054 1054 var cell_data = this.clipboard;
1055 1055 var new_cell = this.insert_cell_above(cell_data.cell_type);
1056 1056 new_cell.fromJSON(cell_data);
1057 1057 var old_cell = this.get_next_cell(new_cell);
1058 1058 this.delete_cell(this.find_cell_index(old_cell));
1059 1059 this.select(this.find_cell_index(new_cell));
1060 1060 }
1061 1061 };
1062 1062
1063 1063 /**
1064 1064 * Paste a cell from the clipboard above the selected cell.
1065 1065 *
1066 1066 * @method paste_cell_above
1067 1067 */
1068 1068 Notebook.prototype.paste_cell_above = function () {
1069 1069 if (this.clipboard !== null && this.paste_enabled) {
1070 1070 var cell_data = this.clipboard;
1071 1071 var new_cell = this.insert_cell_above(cell_data.cell_type);
1072 1072 new_cell.fromJSON(cell_data);
1073 1073 new_cell.focus_cell();
1074 1074 }
1075 1075 };
1076 1076
1077 1077 /**
1078 1078 * Paste a cell from the clipboard below the selected cell.
1079 1079 *
1080 1080 * @method paste_cell_below
1081 1081 */
1082 1082 Notebook.prototype.paste_cell_below = function () {
1083 1083 if (this.clipboard !== null && this.paste_enabled) {
1084 1084 var cell_data = this.clipboard;
1085 1085 var new_cell = this.insert_cell_below(cell_data.cell_type);
1086 1086 new_cell.fromJSON(cell_data);
1087 1087 new_cell.focus_cell();
1088 1088 }
1089 1089 };
1090 1090
1091 1091 // Split/merge
1092 1092
1093 1093 /**
1094 1094 * Split the selected cell into two, at the cursor.
1095 1095 *
1096 1096 * @method split_cell
1097 1097 */
1098 1098 Notebook.prototype.split_cell = function () {
1099 1099 var mdc = IPython.MarkdownCell;
1100 1100 var rc = IPython.RawCell;
1101 1101 var cell = this.get_selected_cell();
1102 1102 if (cell.is_splittable()) {
1103 1103 var texta = cell.get_pre_cursor();
1104 1104 var textb = cell.get_post_cursor();
1105 1105 if (cell instanceof IPython.CodeCell) {
1106 1106 // In this case the operations keep the notebook in its existing mode
1107 1107 // so we don't need to do any post-op mode changes.
1108 1108 cell.set_text(textb);
1109 1109 var new_cell = this.insert_cell_above('code');
1110 1110 new_cell.set_text(texta);
1111 1111 } else if ((cell instanceof mdc && !cell.rendered) || (cell instanceof rc)) {
1112 1112 // We know cell is !rendered so we can use set_text.
1113 1113 cell.set_text(textb);
1114 1114 var new_cell = this.insert_cell_above(cell.cell_type);
1115 1115 // Unrender the new cell so we can call set_text.
1116 1116 new_cell.unrender();
1117 1117 new_cell.set_text(texta);
1118 1118 }
1119 1119 }
1120 1120 };
1121 1121
1122 1122 /**
1123 1123 * Combine the selected cell into the cell above it.
1124 1124 *
1125 1125 * @method merge_cell_above
1126 1126 */
1127 1127 Notebook.prototype.merge_cell_above = function () {
1128 1128 var mdc = IPython.MarkdownCell;
1129 1129 var rc = IPython.RawCell;
1130 1130 var index = this.get_selected_index();
1131 1131 var cell = this.get_cell(index);
1132 1132 var render = cell.rendered;
1133 1133 if (!cell.is_mergeable()) {
1134 1134 return;
1135 1135 }
1136 1136 if (index > 0) {
1137 1137 var upper_cell = this.get_cell(index-1);
1138 1138 if (!upper_cell.is_mergeable()) {
1139 1139 return;
1140 1140 }
1141 1141 var upper_text = upper_cell.get_text();
1142 1142 var text = cell.get_text();
1143 1143 if (cell instanceof IPython.CodeCell) {
1144 1144 cell.set_text(upper_text+'\n'+text);
1145 1145 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1146 1146 cell.unrender(); // Must unrender before we set_text.
1147 1147 cell.set_text(upper_text+'\n\n'+text);
1148 1148 if (render) {
1149 1149 // The rendered state of the final cell should match
1150 1150 // that of the original selected cell;
1151 1151 cell.render();
1152 1152 }
1153 1153 }
1154 1154 this.delete_cell(index-1);
1155 1155 this.select(this.find_cell_index(cell));
1156 1156 }
1157 1157 };
1158 1158
1159 1159 /**
1160 1160 * Combine the selected cell into the cell below it.
1161 1161 *
1162 1162 * @method merge_cell_below
1163 1163 */
1164 1164 Notebook.prototype.merge_cell_below = function () {
1165 1165 var mdc = IPython.MarkdownCell;
1166 1166 var rc = IPython.RawCell;
1167 1167 var index = this.get_selected_index();
1168 1168 var cell = this.get_cell(index);
1169 1169 var render = cell.rendered;
1170 1170 if (!cell.is_mergeable()) {
1171 1171 return;
1172 1172 }
1173 1173 if (index < this.ncells()-1) {
1174 1174 var lower_cell = this.get_cell(index+1);
1175 1175 if (!lower_cell.is_mergeable()) {
1176 1176 return;
1177 1177 }
1178 1178 var lower_text = lower_cell.get_text();
1179 1179 var text = cell.get_text();
1180 1180 if (cell instanceof IPython.CodeCell) {
1181 1181 cell.set_text(text+'\n'+lower_text);
1182 1182 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1183 1183 cell.unrender(); // Must unrender before we set_text.
1184 1184 cell.set_text(text+'\n\n'+lower_text);
1185 1185 if (render) {
1186 1186 // The rendered state of the final cell should match
1187 1187 // that of the original selected cell;
1188 1188 cell.render();
1189 1189 }
1190 1190 }
1191 1191 this.delete_cell(index+1);
1192 1192 this.select(this.find_cell_index(cell));
1193 1193 }
1194 1194 };
1195 1195
1196 1196
1197 1197 // Cell collapsing and output clearing
1198 1198
1199 1199 /**
1200 1200 * Hide a cell's output.
1201 1201 *
1202 1202 * @method collapse_output
1203 1203 * @param {Number} index A cell's numeric index
1204 1204 */
1205 1205 Notebook.prototype.collapse_output = function (index) {
1206 1206 var i = this.index_or_selected(index);
1207 1207 var cell = this.get_cell(i);
1208 1208 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1209 1209 cell.collapse_output();
1210 1210 this.set_dirty(true);
1211 1211 }
1212 1212 };
1213 1213
1214 1214 /**
1215 1215 * Hide each code cell's output area.
1216 1216 *
1217 1217 * @method collapse_all_output
1218 1218 */
1219 1219 Notebook.prototype.collapse_all_output = function () {
1220 1220 $.map(this.get_cells(), function (cell, i) {
1221 1221 if (cell instanceof IPython.CodeCell) {
1222 1222 cell.collapse_output();
1223 1223 }
1224 1224 });
1225 1225 // this should not be set if the `collapse` key is removed from nbformat
1226 1226 this.set_dirty(true);
1227 1227 };
1228 1228
1229 1229 /**
1230 1230 * Show a cell's output.
1231 1231 *
1232 1232 * @method expand_output
1233 1233 * @param {Number} index A cell's numeric index
1234 1234 */
1235 1235 Notebook.prototype.expand_output = function (index) {
1236 1236 var i = this.index_or_selected(index);
1237 1237 var cell = this.get_cell(i);
1238 1238 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1239 1239 cell.expand_output();
1240 1240 this.set_dirty(true);
1241 1241 }
1242 1242 };
1243 1243
1244 1244 /**
1245 1245 * Expand each code cell's output area, and remove scrollbars.
1246 1246 *
1247 1247 * @method expand_all_output
1248 1248 */
1249 1249 Notebook.prototype.expand_all_output = function () {
1250 1250 $.map(this.get_cells(), function (cell, i) {
1251 1251 if (cell instanceof IPython.CodeCell) {
1252 1252 cell.expand_output();
1253 1253 }
1254 1254 });
1255 1255 // this should not be set if the `collapse` key is removed from nbformat
1256 1256 this.set_dirty(true);
1257 1257 };
1258 1258
1259 1259 /**
1260 1260 * Clear the selected CodeCell's output area.
1261 1261 *
1262 1262 * @method clear_output
1263 1263 * @param {Number} index A cell's numeric index
1264 1264 */
1265 1265 Notebook.prototype.clear_output = function (index) {
1266 1266 var i = this.index_or_selected(index);
1267 1267 var cell = this.get_cell(i);
1268 1268 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1269 1269 cell.clear_output();
1270 1270 this.set_dirty(true);
1271 1271 }
1272 1272 };
1273 1273
1274 1274 /**
1275 1275 * Clear each code cell's output area.
1276 1276 *
1277 1277 * @method clear_all_output
1278 1278 */
1279 1279 Notebook.prototype.clear_all_output = function () {
1280 1280 $.map(this.get_cells(), function (cell, i) {
1281 1281 if (cell instanceof IPython.CodeCell) {
1282 1282 cell.clear_output();
1283 1283 }
1284 1284 });
1285 1285 this.set_dirty(true);
1286 1286 };
1287 1287
1288 1288 /**
1289 1289 * Scroll the selected CodeCell's output area.
1290 1290 *
1291 1291 * @method scroll_output
1292 1292 * @param {Number} index A cell's numeric index
1293 1293 */
1294 1294 Notebook.prototype.scroll_output = function (index) {
1295 1295 var i = this.index_or_selected(index);
1296 1296 var cell = this.get_cell(i);
1297 1297 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1298 1298 cell.scroll_output();
1299 1299 this.set_dirty(true);
1300 1300 }
1301 1301 };
1302 1302
1303 1303 /**
1304 1304 * Expand each code cell's output area, and add a scrollbar for long output.
1305 1305 *
1306 1306 * @method scroll_all_output
1307 1307 */
1308 1308 Notebook.prototype.scroll_all_output = function () {
1309 1309 $.map(this.get_cells(), function (cell, i) {
1310 1310 if (cell instanceof IPython.CodeCell) {
1311 1311 cell.scroll_output();
1312 1312 }
1313 1313 });
1314 1314 // this should not be set if the `collapse` key is removed from nbformat
1315 1315 this.set_dirty(true);
1316 1316 };
1317 1317
1318 1318 /** Toggle whether a cell's output is collapsed or expanded.
1319 1319 *
1320 1320 * @method toggle_output
1321 1321 * @param {Number} index A cell's numeric index
1322 1322 */
1323 1323 Notebook.prototype.toggle_output = function (index) {
1324 1324 var i = this.index_or_selected(index);
1325 1325 var cell = this.get_cell(i);
1326 1326 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1327 1327 cell.toggle_output();
1328 1328 this.set_dirty(true);
1329 1329 }
1330 1330 };
1331 1331
1332 1332 /**
1333 1333 * Hide/show the output of all cells.
1334 1334 *
1335 1335 * @method toggle_all_output
1336 1336 */
1337 1337 Notebook.prototype.toggle_all_output = function () {
1338 1338 $.map(this.get_cells(), function (cell, i) {
1339 1339 if (cell instanceof IPython.CodeCell) {
1340 1340 cell.toggle_output();
1341 1341 }
1342 1342 });
1343 1343 // this should not be set if the `collapse` key is removed from nbformat
1344 1344 this.set_dirty(true);
1345 1345 };
1346 1346
1347 1347 /**
1348 1348 * Toggle a scrollbar for long cell outputs.
1349 1349 *
1350 1350 * @method toggle_output_scroll
1351 1351 * @param {Number} index A cell's numeric index
1352 1352 */
1353 1353 Notebook.prototype.toggle_output_scroll = function (index) {
1354 1354 var i = this.index_or_selected(index);
1355 1355 var cell = this.get_cell(i);
1356 1356 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1357 1357 cell.toggle_output_scroll();
1358 1358 this.set_dirty(true);
1359 1359 }
1360 1360 };
1361 1361
1362 1362 /**
1363 1363 * Toggle the scrolling of long output on all cells.
1364 1364 *
1365 1365 * @method toggle_all_output_scrolling
1366 1366 */
1367 1367 Notebook.prototype.toggle_all_output_scroll = function () {
1368 1368 $.map(this.get_cells(), function (cell, i) {
1369 1369 if (cell instanceof IPython.CodeCell) {
1370 1370 cell.toggle_output_scroll();
1371 1371 }
1372 1372 });
1373 1373 // this should not be set if the `collapse` key is removed from nbformat
1374 1374 this.set_dirty(true);
1375 1375 };
1376 1376
1377 1377 // Other cell functions: line numbers, ...
1378 1378
1379 1379 /**
1380 1380 * Toggle line numbers in the selected cell's input area.
1381 1381 *
1382 1382 * @method cell_toggle_line_numbers
1383 1383 */
1384 1384 Notebook.prototype.cell_toggle_line_numbers = function() {
1385 1385 this.get_selected_cell().toggle_line_numbers();
1386 1386 };
1387 1387
1388 1388 // Session related things
1389 1389
1390 1390 /**
1391 1391 * Start a new session and set it on each code cell.
1392 1392 *
1393 1393 * @method start_session
1394 1394 */
1395 1395 Notebook.prototype.start_session = function () {
1396 1396 this.session = new IPython.Session(this, this.options);
1397 1397 this.session.start($.proxy(this._session_started, this));
1398 1398 };
1399 1399
1400 1400
1401 1401 /**
1402 1402 * Once a session is started, link the code cells to the kernel and pass the
1403 1403 * comm manager to the widget manager
1404 1404 *
1405 1405 */
1406 1406 Notebook.prototype._session_started = function(){
1407 1407 this.kernel = this.session.kernel;
1408 1408 var ncells = this.ncells();
1409 1409 for (var i=0; i<ncells; i++) {
1410 1410 var cell = this.get_cell(i);
1411 1411 if (cell instanceof IPython.CodeCell) {
1412 1412 cell.set_kernel(this.session.kernel);
1413 1413 }
1414 1414 }
1415 1415 };
1416 1416
1417 1417 /**
1418 1418 * Prompt the user to restart the IPython kernel.
1419 1419 *
1420 1420 * @method restart_kernel
1421 1421 */
1422 1422 Notebook.prototype.restart_kernel = function () {
1423 1423 var that = this;
1424 1424 IPython.dialog.modal({
1425 1425 title : "Restart kernel or continue running?",
1426 1426 body : $("<p/>").text(
1427 1427 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1428 1428 ),
1429 1429 buttons : {
1430 1430 "Continue running" : {},
1431 1431 "Restart" : {
1432 1432 "class" : "btn-danger",
1433 1433 "click" : function() {
1434 1434 that.session.restart_kernel();
1435 1435 }
1436 1436 }
1437 1437 }
1438 1438 });
1439 1439 };
1440 1440
1441 1441 /**
1442 1442 * Execute or render cell outputs and go into command mode.
1443 1443 *
1444 1444 * @method execute_cell
1445 1445 */
1446 1446 Notebook.prototype.execute_cell = function () {
1447 1447 // mode = shift, ctrl, alt
1448 1448 var cell = this.get_selected_cell();
1449 1449 var cell_index = this.find_cell_index(cell);
1450 1450
1451 1451 cell.execute();
1452 1452 cell.focus_cell();
1453 1453 this.command_mode();
1454 1454 this.set_dirty(true);
1455 1455 };
1456 1456
1457 1457 /**
1458 1458 * Execute or render cell outputs and insert a new cell below.
1459 1459 *
1460 1460 * @method execute_cell_and_insert_below
1461 1461 */
1462 1462 Notebook.prototype.execute_cell_and_insert_below = function () {
1463 1463 var cell = this.get_selected_cell();
1464 1464 var cell_index = this.find_cell_index(cell);
1465 1465
1466 1466 cell.execute();
1467 1467
1468 1468 // If we are at the end always insert a new cell and return
1469 1469 if (cell_index === (this.ncells()-1)) {
1470 1470 this.insert_cell_below('code');
1471 1471 this.edit_mode(cell_index+1);
1472 1472 this.scroll_to_bottom();
1473 1473 this.set_dirty(true);
1474 1474 return;
1475 1475 }
1476 1476
1477 1477 this.insert_cell_below('code');
1478 1478 this.edit_mode(cell_index+1);
1479 1479 this.set_dirty(true);
1480 1480 };
1481 1481
1482 1482 /**
1483 1483 * Execute or render cell outputs and select the next cell.
1484 1484 *
1485 1485 * @method execute_cell_and_select_below
1486 1486 */
1487 1487 Notebook.prototype.execute_cell_and_select_below = function () {
1488 1488
1489 1489 var cell = this.get_selected_cell();
1490 1490 var cell_index = this.find_cell_index(cell);
1491 1491
1492 1492 cell.execute();
1493 1493
1494 1494 // If we are at the end always insert a new cell and return
1495 1495 if (cell_index === (this.ncells()-1)) {
1496 1496 this.insert_cell_below('code');
1497 1497 this.edit_mode(cell_index+1);
1498 1498 this.scroll_to_bottom();
1499 1499 this.set_dirty(true);
1500 1500 return;
1501 1501 }
1502 1502
1503 1503 this.select(cell_index+1);
1504 1504 this.get_cell(cell_index+1).focus_cell();
1505 1505 this.command_mode();
1506 1506 this.set_dirty(true);
1507 1507 };
1508 1508
1509 1509 /**
1510 1510 * Execute all cells below the selected cell.
1511 1511 *
1512 1512 * @method execute_cells_below
1513 1513 */
1514 1514 Notebook.prototype.execute_cells_below = function () {
1515 1515 this.execute_cell_range(this.get_selected_index(), this.ncells());
1516 1516 this.scroll_to_bottom();
1517 1517 };
1518 1518
1519 1519 /**
1520 1520 * Execute all cells above the selected cell.
1521 1521 *
1522 1522 * @method execute_cells_above
1523 1523 */
1524 1524 Notebook.prototype.execute_cells_above = function () {
1525 1525 this.execute_cell_range(0, this.get_selected_index());
1526 1526 };
1527 1527
1528 1528 /**
1529 1529 * Execute all cells.
1530 1530 *
1531 1531 * @method execute_all_cells
1532 1532 */
1533 1533 Notebook.prototype.execute_all_cells = function () {
1534 1534 this.execute_cell_range(0, this.ncells());
1535 1535 this.scroll_to_bottom();
1536 1536 };
1537 1537
1538 1538 /**
1539 1539 * Execute a contiguous range of cells.
1540 1540 *
1541 1541 * @method execute_cell_range
1542 1542 * @param {Number} start Index of the first cell to execute (inclusive)
1543 1543 * @param {Number} end Index of the last cell to execute (exclusive)
1544 1544 */
1545 1545 Notebook.prototype.execute_cell_range = function (start, end) {
1546 1546 for (var i=start; i<end; i++) {
1547 1547 this.select(i);
1548 1548 this.execute_cell();
1549 1549 }
1550 1550 };
1551 1551
1552 1552 // Persistance and loading
1553 1553
1554 1554 /**
1555 1555 * Getter method for this notebook's name.
1556 1556 *
1557 1557 * @method get_notebook_name
1558 1558 * @return {String} This notebook's name (excluding file extension)
1559 1559 */
1560 1560 Notebook.prototype.get_notebook_name = function () {
1561 1561 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1562 1562 return nbname;
1563 1563 };
1564 1564
1565 1565 /**
1566 1566 * Setter method for this notebook's name.
1567 1567 *
1568 1568 * @method set_notebook_name
1569 1569 * @param {String} name A new name for this notebook
1570 1570 */
1571 1571 Notebook.prototype.set_notebook_name = function (name) {
1572 1572 this.notebook_name = name;
1573 1573 };
1574 1574
1575 1575 /**
1576 1576 * Check that a notebook's name is valid.
1577 1577 *
1578 1578 * @method test_notebook_name
1579 1579 * @param {String} nbname A name for this notebook
1580 1580 * @return {Boolean} True if the name is valid, false if invalid
1581 1581 */
1582 1582 Notebook.prototype.test_notebook_name = function (nbname) {
1583 1583 nbname = nbname || '';
1584 1584 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1585 1585 return true;
1586 1586 } else {
1587 1587 return false;
1588 1588 }
1589 1589 };
1590 1590
1591 1591 /**
1592 1592 * Load a notebook from JSON (.ipynb).
1593 1593 *
1594 1594 * This currently handles one worksheet: others are deleted.
1595 1595 *
1596 1596 * @method fromJSON
1597 1597 * @param {Object} data JSON representation of a notebook
1598 1598 */
1599 1599 Notebook.prototype.fromJSON = function (data) {
1600 1600 var content = data.content;
1601 1601 var ncells = this.ncells();
1602 1602 var i;
1603 1603 for (i=0; i<ncells; i++) {
1604 1604 // Always delete cell 0 as they get renumbered as they are deleted.
1605 1605 this.delete_cell(0);
1606 1606 }
1607 1607 // Save the metadata and name.
1608 1608 this.metadata = content.metadata;
1609 1609 this.notebook_name = data.name;
1610 1610 // Only handle 1 worksheet for now.
1611 1611 var worksheet = content.worksheets[0];
1612 1612 if (worksheet !== undefined) {
1613 1613 if (worksheet.metadata) {
1614 1614 this.worksheet_metadata = worksheet.metadata;
1615 1615 }
1616 1616 var new_cells = worksheet.cells;
1617 1617 ncells = new_cells.length;
1618 1618 var cell_data = null;
1619 1619 var new_cell = null;
1620 1620 for (i=0; i<ncells; i++) {
1621 1621 cell_data = new_cells[i];
1622 1622 // VERSIONHACK: plaintext -> raw
1623 1623 // handle never-released plaintext name for raw cells
1624 1624 if (cell_data.cell_type === 'plaintext'){
1625 1625 cell_data.cell_type = 'raw';
1626 1626 }
1627 1627
1628 1628 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1629 1629 new_cell.fromJSON(cell_data);
1630 1630 }
1631 1631 }
1632 1632 if (content.worksheets.length > 1) {
1633 1633 IPython.dialog.modal({
1634 1634 title : "Multiple worksheets",
1635 1635 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1636 1636 "but this version of IPython can only handle the first. " +
1637 1637 "If you save this notebook, worksheets after the first will be lost.",
1638 1638 buttons : {
1639 1639 OK : {
1640 1640 class : "btn-danger"
1641 1641 }
1642 1642 }
1643 1643 });
1644 1644 }
1645 1645 };
1646 1646
1647 1647 /**
1648 1648 * Dump this notebook into a JSON-friendly object.
1649 1649 *
1650 1650 * @method toJSON
1651 1651 * @return {Object} A JSON-friendly representation of this notebook.
1652 1652 */
1653 1653 Notebook.prototype.toJSON = function () {
1654 1654 var cells = this.get_cells();
1655 1655 var ncells = cells.length;
1656 1656 var cell_array = new Array(ncells);
1657 1657 for (var i=0; i<ncells; i++) {
1658 1658 cell_array[i] = cells[i].toJSON();
1659 1659 }
1660 1660 var data = {
1661 1661 // Only handle 1 worksheet for now.
1662 1662 worksheets : [{
1663 1663 cells: cell_array,
1664 1664 metadata: this.worksheet_metadata
1665 1665 }],
1666 1666 metadata : this.metadata
1667 1667 };
1668 1668 return data;
1669 1669 };
1670 1670
1671 1671 /**
1672 1672 * Start an autosave timer, for periodically saving the notebook.
1673 1673 *
1674 1674 * @method set_autosave_interval
1675 1675 * @param {Integer} interval the autosave interval in milliseconds
1676 1676 */
1677 1677 Notebook.prototype.set_autosave_interval = function (interval) {
1678 1678 var that = this;
1679 1679 // clear previous interval, so we don't get simultaneous timers
1680 1680 if (this.autosave_timer) {
1681 1681 clearInterval(this.autosave_timer);
1682 1682 }
1683 1683
1684 1684 this.autosave_interval = this.minimum_autosave_interval = interval;
1685 1685 if (interval) {
1686 1686 this.autosave_timer = setInterval(function() {
1687 1687 if (that.dirty) {
1688 1688 that.save_notebook();
1689 1689 }
1690 1690 }, interval);
1691 1691 $([IPython.events]).trigger("autosave_enabled.Notebook", interval);
1692 1692 } else {
1693 1693 this.autosave_timer = null;
1694 1694 $([IPython.events]).trigger("autosave_disabled.Notebook");
1695 1695 }
1696 1696 };
1697 1697
1698 1698 /**
1699 1699 * Save this notebook on the server.
1700 1700 *
1701 1701 * @method save_notebook
1702 1702 */
1703 1703 Notebook.prototype.save_notebook = function (extra_settings) {
1704 1704 // Create a JSON model to be sent to the server.
1705 1705 var model = {};
1706 1706 model.name = this.notebook_name;
1707 1707 model.path = this.notebook_path;
1708 1708 model.content = this.toJSON();
1709 1709 model.content.nbformat = this.nbformat;
1710 1710 model.content.nbformat_minor = this.nbformat_minor;
1711 1711 // time the ajax call for autosave tuning purposes.
1712 1712 var start = new Date().getTime();
1713 1713 // We do the call with settings so we can set cache to false.
1714 1714 var settings = {
1715 1715 processData : false,
1716 1716 cache : false,
1717 1717 type : "PUT",
1718 1718 data : JSON.stringify(model),
1719 1719 headers : {'Content-Type': 'application/json'},
1720 1720 success : $.proxy(this.save_notebook_success, this, start),
1721 1721 error : $.proxy(this.save_notebook_error, this)
1722 1722 };
1723 1723 if (extra_settings) {
1724 1724 for (var key in extra_settings) {
1725 1725 settings[key] = extra_settings[key];
1726 1726 }
1727 1727 }
1728 1728 $([IPython.events]).trigger('notebook_saving.Notebook');
1729 1729 var url = utils.url_join_encode(
1730 1730 this.base_url,
1731 1731 'api/notebooks',
1732 1732 this.notebook_path,
1733 1733 this.notebook_name
1734 1734 );
1735 1735 $.ajax(url, settings);
1736 1736 };
1737 1737
1738 1738 /**
1739 1739 * Success callback for saving a notebook.
1740 1740 *
1741 1741 * @method save_notebook_success
1742 1742 * @param {Integer} start the time when the save request started
1743 1743 * @param {Object} data JSON representation of a notebook
1744 1744 * @param {String} status Description of response status
1745 1745 * @param {jqXHR} xhr jQuery Ajax object
1746 1746 */
1747 1747 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1748 1748 this.set_dirty(false);
1749 1749 $([IPython.events]).trigger('notebook_saved.Notebook');
1750 1750 this._update_autosave_interval(start);
1751 1751 if (this._checkpoint_after_save) {
1752 1752 this.create_checkpoint();
1753 1753 this._checkpoint_after_save = false;
1754 1754 }
1755 1755 };
1756 1756
1757 1757 /**
1758 1758 * update the autosave interval based on how long the last save took
1759 1759 *
1760 1760 * @method _update_autosave_interval
1761 1761 * @param {Integer} timestamp when the save request started
1762 1762 */
1763 1763 Notebook.prototype._update_autosave_interval = function (start) {
1764 1764 var duration = (new Date().getTime() - start);
1765 1765 if (this.autosave_interval) {
1766 1766 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1767 1767 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1768 1768 // round to 10 seconds, otherwise we will be setting a new interval too often
1769 1769 interval = 10000 * Math.round(interval / 10000);
1770 1770 // set new interval, if it's changed
1771 1771 if (interval != this.autosave_interval) {
1772 1772 this.set_autosave_interval(interval);
1773 1773 }
1774 1774 }
1775 1775 };
1776 1776
1777 1777 /**
1778 1778 * Failure callback for saving a notebook.
1779 1779 *
1780 1780 * @method save_notebook_error
1781 1781 * @param {jqXHR} xhr jQuery Ajax object
1782 1782 * @param {String} status Description of response status
1783 1783 * @param {String} error HTTP error message
1784 1784 */
1785 1785 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1786 1786 $([IPython.events]).trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1787 1787 };
1788 1788
1789 1789 Notebook.prototype.new_notebook = function(){
1790 1790 var path = this.notebook_path;
1791 1791 var base_url = this.base_url;
1792 1792 var settings = {
1793 1793 processData : false,
1794 1794 cache : false,
1795 1795 type : "POST",
1796 1796 dataType : "json",
1797 1797 async : false,
1798 1798 success : function (data, status, xhr){
1799 1799 var notebook_name = data.name;
1800 1800 window.open(
1801 1801 utils.url_join_encode(
1802 1802 base_url,
1803 1803 'notebooks',
1804 1804 path,
1805 1805 notebook_name
1806 1806 ),
1807 1807 '_blank'
1808 1808 );
1809 1809 }
1810 1810 };
1811 1811 var url = utils.url_join_encode(
1812 1812 base_url,
1813 1813 'api/notebooks',
1814 1814 path
1815 1815 );
1816 1816 $.ajax(url,settings);
1817 1817 };
1818 1818
1819 1819
1820 1820 Notebook.prototype.copy_notebook = function(){
1821 1821 var path = this.notebook_path;
1822 1822 var base_url = this.base_url;
1823 1823 var settings = {
1824 1824 processData : false,
1825 1825 cache : false,
1826 1826 type : "POST",
1827 1827 dataType : "json",
1828 1828 data : JSON.stringify({copy_from : this.notebook_name}),
1829 1829 async : false,
1830 1830 success : function (data, status, xhr) {
1831 1831 window.open(utils.url_join_encode(
1832 1832 base_url,
1833 1833 'notebooks',
1834 1834 data.path,
1835 1835 data.name
1836 1836 ), '_blank');
1837 1837 }
1838 1838 };
1839 1839 var url = utils.url_join_encode(
1840 1840 base_url,
1841 1841 'api/notebooks',
1842 1842 path
1843 1843 );
1844 1844 $.ajax(url,settings);
1845 1845 };
1846 1846
1847 1847 Notebook.prototype.rename = function (nbname) {
1848 1848 var that = this;
1849 1849 if (!nbname.match(/\.ipynb$/)) {
1850 1850 nbname = nbname + ".ipynb";
1851 1851 }
1852 1852 var data = {name: nbname};
1853 1853 var settings = {
1854 1854 processData : false,
1855 1855 cache : false,
1856 1856 type : "PATCH",
1857 1857 data : JSON.stringify(data),
1858 1858 dataType: "json",
1859 1859 headers : {'Content-Type': 'application/json'},
1860 1860 success : $.proxy(that.rename_success, this),
1861 1861 error : $.proxy(that.rename_error, this)
1862 1862 };
1863 1863 $([IPython.events]).trigger('rename_notebook.Notebook', data);
1864 1864 var url = utils.url_join_encode(
1865 1865 this.base_url,
1866 1866 'api/notebooks',
1867 1867 this.notebook_path,
1868 1868 this.notebook_name
1869 1869 );
1870 1870 $.ajax(url, settings);
1871 1871 };
1872 1872
1873 1873 Notebook.prototype.delete = function () {
1874 1874 var that = this;
1875 1875 var settings = {
1876 1876 processData : false,
1877 1877 cache : false,
1878 1878 type : "DELETE",
1879 1879 dataType: "json",
1880 1880 };
1881 1881 var url = utils.url_join_encode(
1882 1882 this.base_url,
1883 1883 'api/notebooks',
1884 1884 this.notebook_path,
1885 1885 this.notebook_name
1886 1886 );
1887 1887 $.ajax(url, settings);
1888 1888 };
1889 1889
1890 1890
1891 1891 Notebook.prototype.rename_success = function (json, status, xhr) {
1892 1892 var name = this.notebook_name = json.name;
1893 1893 var path = json.path;
1894 1894 this.session.rename_notebook(name, path);
1895 1895 $([IPython.events]).trigger('notebook_renamed.Notebook', json);
1896 1896 };
1897 1897
1898 1898 Notebook.prototype.rename_error = function (xhr, status, error) {
1899 1899 var that = this;
1900 1900 var dialog = $('<div/>').append(
1901 1901 $("<p/>").addClass("rename-message")
1902 1902 .text('This notebook name already exists.')
1903 1903 );
1904 1904 $([IPython.events]).trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
1905 1905 IPython.dialog.modal({
1906 1906 title: "Notebook Rename Error!",
1907 1907 body: dialog,
1908 1908 buttons : {
1909 1909 "Cancel": {},
1910 1910 "OK": {
1911 1911 class: "btn-primary",
1912 1912 click: function () {
1913 1913 IPython.save_widget.rename_notebook();
1914 1914 }}
1915 1915 },
1916 1916 open : function (event, ui) {
1917 1917 var that = $(this);
1918 1918 // Upon ENTER, click the OK button.
1919 1919 that.find('input[type="text"]').keydown(function (event, ui) {
1920 if (event.which === utils.keycodes.ENTER) {
1920 if (event.which === IPython.keyboard.keycodes.enter) {
1921 1921 that.find('.btn-primary').first().click();
1922 1922 }
1923 1923 });
1924 1924 that.find('input[type="text"]').focus();
1925 1925 }
1926 1926 });
1927 1927 };
1928 1928
1929 1929 /**
1930 1930 * Request a notebook's data from the server.
1931 1931 *
1932 1932 * @method load_notebook
1933 1933 * @param {String} notebook_name and path A notebook to load
1934 1934 */
1935 1935 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
1936 1936 var that = this;
1937 1937 this.notebook_name = notebook_name;
1938 1938 this.notebook_path = notebook_path;
1939 1939 // We do the call with settings so we can set cache to false.
1940 1940 var settings = {
1941 1941 processData : false,
1942 1942 cache : false,
1943 1943 type : "GET",
1944 1944 dataType : "json",
1945 1945 success : $.proxy(this.load_notebook_success,this),
1946 1946 error : $.proxy(this.load_notebook_error,this),
1947 1947 };
1948 1948 $([IPython.events]).trigger('notebook_loading.Notebook');
1949 1949 var url = utils.url_join_encode(
1950 1950 this.base_url,
1951 1951 'api/notebooks',
1952 1952 this.notebook_path,
1953 1953 this.notebook_name
1954 1954 );
1955 1955 $.ajax(url, settings);
1956 1956 };
1957 1957
1958 1958 /**
1959 1959 * Success callback for loading a notebook from the server.
1960 1960 *
1961 1961 * Load notebook data from the JSON response.
1962 1962 *
1963 1963 * @method load_notebook_success
1964 1964 * @param {Object} data JSON representation of a notebook
1965 1965 * @param {String} status Description of response status
1966 1966 * @param {jqXHR} xhr jQuery Ajax object
1967 1967 */
1968 1968 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
1969 1969 this.fromJSON(data);
1970 1970 if (this.ncells() === 0) {
1971 1971 this.insert_cell_below('code');
1972 1972 this.edit_mode(0);
1973 1973 } else {
1974 1974 this.select(0);
1975 1975 this.command_mode();
1976 1976 }
1977 1977 this.set_dirty(false);
1978 1978 this.scroll_to_top();
1979 1979 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
1980 1980 var msg = "This notebook has been converted from an older " +
1981 1981 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
1982 1982 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
1983 1983 "newer notebook format will be used and older versions of IPython " +
1984 1984 "may not be able to read it. To keep the older version, close the " +
1985 1985 "notebook without saving it.";
1986 1986 IPython.dialog.modal({
1987 1987 title : "Notebook converted",
1988 1988 body : msg,
1989 1989 buttons : {
1990 1990 OK : {
1991 1991 class : "btn-primary"
1992 1992 }
1993 1993 }
1994 1994 });
1995 1995 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
1996 1996 var that = this;
1997 1997 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
1998 1998 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
1999 1999 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2000 2000 this_vs + ". You can still work with this notebook, but some features " +
2001 2001 "introduced in later notebook versions may not be available.";
2002 2002
2003 2003 IPython.dialog.modal({
2004 2004 title : "Newer Notebook",
2005 2005 body : msg,
2006 2006 buttons : {
2007 2007 OK : {
2008 2008 class : "btn-danger"
2009 2009 }
2010 2010 }
2011 2011 });
2012 2012
2013 2013 }
2014 2014
2015 2015 // Create the session after the notebook is completely loaded to prevent
2016 2016 // code execution upon loading, which is a security risk.
2017 2017 if (this.session === null) {
2018 2018 this.start_session();
2019 2019 }
2020 2020 // load our checkpoint list
2021 2021 this.list_checkpoints();
2022 2022
2023 2023 // load toolbar state
2024 2024 if (this.metadata.celltoolbar) {
2025 2025 IPython.CellToolbar.global_show();
2026 2026 IPython.CellToolbar.activate_preset(this.metadata.celltoolbar);
2027 2027 }
2028 2028
2029 2029 $([IPython.events]).trigger('notebook_loaded.Notebook');
2030 2030 };
2031 2031
2032 2032 /**
2033 2033 * Failure callback for loading a notebook from the server.
2034 2034 *
2035 2035 * @method load_notebook_error
2036 2036 * @param {jqXHR} xhr jQuery Ajax object
2037 2037 * @param {String} status Description of response status
2038 2038 * @param {String} error HTTP error message
2039 2039 */
2040 2040 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2041 2041 $([IPython.events]).trigger('notebook_load_failed.Notebook', [xhr, status, error]);
2042 2042 var msg;
2043 2043 if (xhr.status === 400) {
2044 2044 msg = error;
2045 2045 } else if (xhr.status === 500) {
2046 2046 msg = "An unknown error occurred while loading this notebook. " +
2047 2047 "This version can load notebook formats " +
2048 2048 "v" + this.nbformat + " or earlier.";
2049 2049 }
2050 2050 IPython.dialog.modal({
2051 2051 title: "Error loading notebook",
2052 2052 body : msg,
2053 2053 buttons : {
2054 2054 "OK": {}
2055 2055 }
2056 2056 });
2057 2057 };
2058 2058
2059 2059 /********************* checkpoint-related *********************/
2060 2060
2061 2061 /**
2062 2062 * Save the notebook then immediately create a checkpoint.
2063 2063 *
2064 2064 * @method save_checkpoint
2065 2065 */
2066 2066 Notebook.prototype.save_checkpoint = function () {
2067 2067 this._checkpoint_after_save = true;
2068 2068 this.save_notebook();
2069 2069 };
2070 2070
2071 2071 /**
2072 2072 * Add a checkpoint for this notebook.
2073 2073 * for use as a callback from checkpoint creation.
2074 2074 *
2075 2075 * @method add_checkpoint
2076 2076 */
2077 2077 Notebook.prototype.add_checkpoint = function (checkpoint) {
2078 2078 var found = false;
2079 2079 for (var i = 0; i < this.checkpoints.length; i++) {
2080 2080 var existing = this.checkpoints[i];
2081 2081 if (existing.id == checkpoint.id) {
2082 2082 found = true;
2083 2083 this.checkpoints[i] = checkpoint;
2084 2084 break;
2085 2085 }
2086 2086 }
2087 2087 if (!found) {
2088 2088 this.checkpoints.push(checkpoint);
2089 2089 }
2090 2090 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2091 2091 };
2092 2092
2093 2093 /**
2094 2094 * List checkpoints for this notebook.
2095 2095 *
2096 2096 * @method list_checkpoints
2097 2097 */
2098 2098 Notebook.prototype.list_checkpoints = function () {
2099 2099 var url = utils.url_join_encode(
2100 2100 this.base_url,
2101 2101 'api/notebooks',
2102 2102 this.notebook_path,
2103 2103 this.notebook_name,
2104 2104 'checkpoints'
2105 2105 );
2106 2106 $.get(url).done(
2107 2107 $.proxy(this.list_checkpoints_success, this)
2108 2108 ).fail(
2109 2109 $.proxy(this.list_checkpoints_error, this)
2110 2110 );
2111 2111 };
2112 2112
2113 2113 /**
2114 2114 * Success callback for listing checkpoints.
2115 2115 *
2116 2116 * @method list_checkpoint_success
2117 2117 * @param {Object} data JSON representation of a checkpoint
2118 2118 * @param {String} status Description of response status
2119 2119 * @param {jqXHR} xhr jQuery Ajax object
2120 2120 */
2121 2121 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2122 2122 data = $.parseJSON(data);
2123 2123 this.checkpoints = data;
2124 2124 if (data.length) {
2125 2125 this.last_checkpoint = data[data.length - 1];
2126 2126 } else {
2127 2127 this.last_checkpoint = null;
2128 2128 }
2129 2129 $([IPython.events]).trigger('checkpoints_listed.Notebook', [data]);
2130 2130 };
2131 2131
2132 2132 /**
2133 2133 * Failure callback for listing a checkpoint.
2134 2134 *
2135 2135 * @method list_checkpoint_error
2136 2136 * @param {jqXHR} xhr jQuery Ajax object
2137 2137 * @param {String} status Description of response status
2138 2138 * @param {String} error_msg HTTP error message
2139 2139 */
2140 2140 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2141 2141 $([IPython.events]).trigger('list_checkpoints_failed.Notebook');
2142 2142 };
2143 2143
2144 2144 /**
2145 2145 * Create a checkpoint of this notebook on the server from the most recent save.
2146 2146 *
2147 2147 * @method create_checkpoint
2148 2148 */
2149 2149 Notebook.prototype.create_checkpoint = function () {
2150 2150 var url = utils.url_join_encode(
2151 2151 this.base_url,
2152 2152 'api/notebooks',
2153 2153 this.notebook_path,
2154 2154 this.notebook_name,
2155 2155 'checkpoints'
2156 2156 );
2157 2157 $.post(url).done(
2158 2158 $.proxy(this.create_checkpoint_success, this)
2159 2159 ).fail(
2160 2160 $.proxy(this.create_checkpoint_error, this)
2161 2161 );
2162 2162 };
2163 2163
2164 2164 /**
2165 2165 * Success callback for creating a checkpoint.
2166 2166 *
2167 2167 * @method create_checkpoint_success
2168 2168 * @param {Object} data JSON representation of a checkpoint
2169 2169 * @param {String} status Description of response status
2170 2170 * @param {jqXHR} xhr jQuery Ajax object
2171 2171 */
2172 2172 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2173 2173 data = $.parseJSON(data);
2174 2174 this.add_checkpoint(data);
2175 2175 $([IPython.events]).trigger('checkpoint_created.Notebook', data);
2176 2176 };
2177 2177
2178 2178 /**
2179 2179 * Failure callback for creating a checkpoint.
2180 2180 *
2181 2181 * @method create_checkpoint_error
2182 2182 * @param {jqXHR} xhr jQuery Ajax object
2183 2183 * @param {String} status Description of response status
2184 2184 * @param {String} error_msg HTTP error message
2185 2185 */
2186 2186 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2187 2187 $([IPython.events]).trigger('checkpoint_failed.Notebook');
2188 2188 };
2189 2189
2190 2190 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2191 2191 var that = this;
2192 2192 checkpoint = checkpoint || this.last_checkpoint;
2193 2193 if ( ! checkpoint ) {
2194 2194 console.log("restore dialog, but no checkpoint to restore to!");
2195 2195 return;
2196 2196 }
2197 2197 var body = $('<div/>').append(
2198 2198 $('<p/>').addClass("p-space").text(
2199 2199 "Are you sure you want to revert the notebook to " +
2200 2200 "the latest checkpoint?"
2201 2201 ).append(
2202 2202 $("<strong/>").text(
2203 2203 " This cannot be undone."
2204 2204 )
2205 2205 )
2206 2206 ).append(
2207 2207 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2208 2208 ).append(
2209 2209 $('<p/>').addClass("p-space").text(
2210 2210 Date(checkpoint.last_modified)
2211 2211 ).css("text-align", "center")
2212 2212 );
2213 2213
2214 2214 IPython.dialog.modal({
2215 2215 title : "Revert notebook to checkpoint",
2216 2216 body : body,
2217 2217 buttons : {
2218 2218 Revert : {
2219 2219 class : "btn-danger",
2220 2220 click : function () {
2221 2221 that.restore_checkpoint(checkpoint.id);
2222 2222 }
2223 2223 },
2224 2224 Cancel : {}
2225 2225 }
2226 2226 });
2227 2227 };
2228 2228
2229 2229 /**
2230 2230 * Restore the notebook to a checkpoint state.
2231 2231 *
2232 2232 * @method restore_checkpoint
2233 2233 * @param {String} checkpoint ID
2234 2234 */
2235 2235 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2236 2236 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2237 2237 var url = utils.url_join_encode(
2238 2238 this.base_url,
2239 2239 'api/notebooks',
2240 2240 this.notebook_path,
2241 2241 this.notebook_name,
2242 2242 'checkpoints',
2243 2243 checkpoint
2244 2244 );
2245 2245 $.post(url).done(
2246 2246 $.proxy(this.restore_checkpoint_success, this)
2247 2247 ).fail(
2248 2248 $.proxy(this.restore_checkpoint_error, this)
2249 2249 );
2250 2250 };
2251 2251
2252 2252 /**
2253 2253 * Success callback for restoring a notebook to a checkpoint.
2254 2254 *
2255 2255 * @method restore_checkpoint_success
2256 2256 * @param {Object} data (ignored, should be empty)
2257 2257 * @param {String} status Description of response status
2258 2258 * @param {jqXHR} xhr jQuery Ajax object
2259 2259 */
2260 2260 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2261 2261 $([IPython.events]).trigger('checkpoint_restored.Notebook');
2262 2262 this.load_notebook(this.notebook_name, this.notebook_path);
2263 2263 };
2264 2264
2265 2265 /**
2266 2266 * Failure callback for restoring a notebook to a checkpoint.
2267 2267 *
2268 2268 * @method restore_checkpoint_error
2269 2269 * @param {jqXHR} xhr jQuery Ajax object
2270 2270 * @param {String} status Description of response status
2271 2271 * @param {String} error_msg HTTP error message
2272 2272 */
2273 2273 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2274 2274 $([IPython.events]).trigger('checkpoint_restore_failed.Notebook');
2275 2275 };
2276 2276
2277 2277 /**
2278 2278 * Delete a notebook checkpoint.
2279 2279 *
2280 2280 * @method delete_checkpoint
2281 2281 * @param {String} checkpoint ID
2282 2282 */
2283 2283 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2284 2284 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2285 2285 var url = utils.url_join_encode(
2286 2286 this.base_url,
2287 2287 'api/notebooks',
2288 2288 this.notebook_path,
2289 2289 this.notebook_name,
2290 2290 'checkpoints',
2291 2291 checkpoint
2292 2292 );
2293 2293 $.ajax(url, {
2294 2294 type: 'DELETE',
2295 2295 success: $.proxy(this.delete_checkpoint_success, this),
2296 2296 error: $.proxy(this.delete_notebook_error,this)
2297 2297 });
2298 2298 };
2299 2299
2300 2300 /**
2301 2301 * Success callback for deleting a notebook checkpoint
2302 2302 *
2303 2303 * @method delete_checkpoint_success
2304 2304 * @param {Object} data (ignored, should be empty)
2305 2305 * @param {String} status Description of response status
2306 2306 * @param {jqXHR} xhr jQuery Ajax object
2307 2307 */
2308 2308 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2309 2309 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2310 2310 this.load_notebook(this.notebook_name, this.notebook_path);
2311 2311 };
2312 2312
2313 2313 /**
2314 2314 * Failure callback for deleting a notebook checkpoint.
2315 2315 *
2316 2316 * @method delete_checkpoint_error
2317 2317 * @param {jqXHR} xhr jQuery Ajax object
2318 2318 * @param {String} status Description of response status
2319 2319 * @param {String} error_msg HTTP error message
2320 2320 */
2321 2321 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2322 2322 $([IPython.events]).trigger('checkpoint_delete_failed.Notebook');
2323 2323 };
2324 2324
2325 2325
2326 2326 IPython.Notebook = Notebook;
2327 2327
2328 2328
2329 2329 return IPython;
2330 2330
2331 2331 }(IPython));
2332 2332
@@ -1,865 +1,865 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // OutputArea
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule OutputArea
16 16 */
17 17 var IPython = (function (IPython) {
18 18 "use strict";
19 19
20 20 var utils = IPython.utils;
21 21
22 22 /**
23 23 * @class OutputArea
24 24 *
25 25 * @constructor
26 26 */
27 27
28 28 var OutputArea = function (selector, prompt_area) {
29 29 this.selector = selector;
30 30 this.wrapper = $(selector);
31 31 this.outputs = [];
32 32 this.collapsed = false;
33 33 this.scrolled = false;
34 34 this.trusted = true;
35 35 this.clear_queued = null;
36 36 if (prompt_area === undefined) {
37 37 this.prompt_area = true;
38 38 } else {
39 39 this.prompt_area = prompt_area;
40 40 }
41 41 this.create_elements();
42 42 this.style();
43 43 this.bind_events();
44 44 };
45 45
46 46
47 47 /**
48 48 * Class prototypes
49 49 **/
50 50
51 51 OutputArea.prototype.create_elements = function () {
52 52 this.element = $("<div/>");
53 53 this.collapse_button = $("<div/>");
54 54 this.prompt_overlay = $("<div/>");
55 55 this.wrapper.append(this.prompt_overlay);
56 56 this.wrapper.append(this.element);
57 57 this.wrapper.append(this.collapse_button);
58 58 };
59 59
60 60
61 61 OutputArea.prototype.style = function () {
62 62 this.collapse_button.hide();
63 63 this.prompt_overlay.hide();
64 64
65 65 this.wrapper.addClass('output_wrapper');
66 66 this.element.addClass('output');
67 67
68 68 this.collapse_button.addClass("btn output_collapsed");
69 69 this.collapse_button.attr('title', 'click to expand output');
70 70 this.collapse_button.text('. . .');
71 71
72 72 this.prompt_overlay.addClass('out_prompt_overlay prompt');
73 73 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
74 74
75 75 this.collapse();
76 76 };
77 77
78 78 /**
79 79 * Should the OutputArea scroll?
80 80 * Returns whether the height (in lines) exceeds a threshold.
81 81 *
82 82 * @private
83 83 * @method _should_scroll
84 84 * @param [lines=100]{Integer}
85 85 * @return {Bool}
86 86 *
87 87 */
88 88 OutputArea.prototype._should_scroll = function (lines) {
89 89 if (lines <=0 ){ return }
90 90 if (!lines) {
91 91 lines = 100;
92 92 }
93 93 // line-height from http://stackoverflow.com/questions/1185151
94 94 var fontSize = this.element.css('font-size');
95 95 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
96 96
97 97 return (this.element.height() > lines * lineHeight);
98 98 };
99 99
100 100
101 101 OutputArea.prototype.bind_events = function () {
102 102 var that = this;
103 103 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
104 104 this.prompt_overlay.click(function () { that.toggle_scroll(); });
105 105
106 106 this.element.resize(function () {
107 107 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
108 108 if ( IPython.utils.browser[0] === "Firefox" ) {
109 109 return;
110 110 }
111 111 // maybe scroll output,
112 112 // if it's grown large enough and hasn't already been scrolled.
113 113 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
114 114 that.scroll_area();
115 115 }
116 116 });
117 117 this.collapse_button.click(function () {
118 118 that.expand();
119 119 });
120 120 };
121 121
122 122
123 123 OutputArea.prototype.collapse = function () {
124 124 if (!this.collapsed) {
125 125 this.element.hide();
126 126 this.prompt_overlay.hide();
127 127 if (this.element.html()){
128 128 this.collapse_button.show();
129 129 }
130 130 this.collapsed = true;
131 131 }
132 132 };
133 133
134 134
135 135 OutputArea.prototype.expand = function () {
136 136 if (this.collapsed) {
137 137 this.collapse_button.hide();
138 138 this.element.show();
139 139 this.prompt_overlay.show();
140 140 this.collapsed = false;
141 141 }
142 142 };
143 143
144 144
145 145 OutputArea.prototype.toggle_output = function () {
146 146 if (this.collapsed) {
147 147 this.expand();
148 148 } else {
149 149 this.collapse();
150 150 }
151 151 };
152 152
153 153
154 154 OutputArea.prototype.scroll_area = function () {
155 155 this.element.addClass('output_scroll');
156 156 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
157 157 this.scrolled = true;
158 158 };
159 159
160 160
161 161 OutputArea.prototype.unscroll_area = function () {
162 162 this.element.removeClass('output_scroll');
163 163 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
164 164 this.scrolled = false;
165 165 };
166 166
167 167 /**
168 168 *
169 169 * Scroll OutputArea if height supperior than a threshold (in lines).
170 170 *
171 171 * Threshold is a maximum number of lines. If unspecified, defaults to
172 172 * OutputArea.minimum_scroll_threshold.
173 173 *
174 174 * Negative threshold will prevent the OutputArea from ever scrolling.
175 175 *
176 176 * @method scroll_if_long
177 177 *
178 178 * @param [lines=20]{Number} Default to 20 if not set,
179 179 * behavior undefined for value of `0`.
180 180 *
181 181 **/
182 182 OutputArea.prototype.scroll_if_long = function (lines) {
183 183 var n = lines | OutputArea.minimum_scroll_threshold;
184 184 if(n <= 0){
185 185 return
186 186 }
187 187
188 188 if (this._should_scroll(n)) {
189 189 // only allow scrolling long-enough output
190 190 this.scroll_area();
191 191 }
192 192 };
193 193
194 194
195 195 OutputArea.prototype.toggle_scroll = function () {
196 196 if (this.scrolled) {
197 197 this.unscroll_area();
198 198 } else {
199 199 // only allow scrolling long-enough output
200 200 this.scroll_if_long();
201 201 }
202 202 };
203 203
204 204
205 205 // typeset with MathJax if MathJax is available
206 206 OutputArea.prototype.typeset = function () {
207 207 if (window.MathJax){
208 208 MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
209 209 }
210 210 };
211 211
212 212
213 213 OutputArea.prototype.handle_output = function (msg) {
214 214 var json = {};
215 215 var msg_type = json.output_type = msg.header.msg_type;
216 216 var content = msg.content;
217 217 if (msg_type === "stream") {
218 218 json.text = content.data;
219 219 json.stream = content.name;
220 220 } else if (msg_type === "display_data") {
221 221 json = content.data;
222 222 json.output_type = msg_type;
223 223 json.metadata = content.metadata;
224 224 } else if (msg_type === "pyout") {
225 225 json = content.data;
226 226 json.output_type = msg_type;
227 227 json.metadata = content.metadata;
228 228 json.prompt_number = content.execution_count;
229 229 } else if (msg_type === "pyerr") {
230 230 json.ename = content.ename;
231 231 json.evalue = content.evalue;
232 232 json.traceback = content.traceback;
233 233 }
234 234 this.append_output(json);
235 235 };
236 236
237 237
238 238 OutputArea.prototype.rename_keys = function (data, key_map) {
239 239 var remapped = {};
240 240 for (var key in data) {
241 241 var new_key = key_map[key] || key;
242 242 remapped[new_key] = data[key];
243 243 }
244 244 return remapped;
245 245 };
246 246
247 247
248 248 OutputArea.output_types = [
249 249 'application/javascript',
250 250 'text/html',
251 251 'text/latex',
252 252 'image/svg+xml',
253 253 'image/png',
254 254 'image/jpeg',
255 255 'application/pdf',
256 256 'text/plain'
257 257 ];
258 258
259 259 OutputArea.prototype.validate_output = function (json) {
260 260 // scrub invalid outputs
261 261 // TODO: right now everything is a string, but JSON really shouldn't be.
262 262 // nbformat 4 will fix that.
263 263 $.map(OutputArea.output_types, function(key){
264 264 if (json[key] !== undefined && typeof json[key] !== 'string') {
265 265 console.log("Invalid type for " + key, json[key]);
266 266 delete json[key];
267 267 }
268 268 });
269 269 return json;
270 270 };
271 271
272 272 OutputArea.prototype.append_output = function (json) {
273 273 this.expand();
274 274 // Clear the output if clear is queued.
275 275 var needs_height_reset = false;
276 276 if (this.clear_queued) {
277 277 this.clear_output(false);
278 278 needs_height_reset = true;
279 279 }
280 280
281 281 // validate output data types
282 282 json = this.validate_output(json);
283 283
284 284 if (json.output_type === 'pyout') {
285 285 this.append_pyout(json);
286 286 } else if (json.output_type === 'pyerr') {
287 287 this.append_pyerr(json);
288 288 } else if (json.output_type === 'display_data') {
289 289 this.append_display_data(json);
290 290 } else if (json.output_type === 'stream') {
291 291 this.append_stream(json);
292 292 }
293 293
294 294 this.outputs.push(json);
295 295
296 296 // Only reset the height to automatic if the height is currently
297 297 // fixed (done by wait=True flag on clear_output).
298 298 if (needs_height_reset) {
299 299 this.element.height('');
300 300 }
301 301
302 302 var that = this;
303 303 setTimeout(function(){that.element.trigger('resize');}, 100);
304 304 };
305 305
306 306
307 307 OutputArea.prototype.create_output_area = function () {
308 308 var oa = $("<div/>").addClass("output_area");
309 309 if (this.prompt_area) {
310 310 oa.append($('<div/>').addClass('prompt'));
311 311 }
312 312 return oa;
313 313 };
314 314
315 315
316 316 function _get_metadata_key(metadata, key, mime) {
317 317 var mime_md = metadata[mime];
318 318 // mime-specific higher priority
319 319 if (mime_md && mime_md[key] !== undefined) {
320 320 return mime_md[key];
321 321 }
322 322 // fallback on global
323 323 return metadata[key];
324 324 }
325 325
326 326 OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
327 327 var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
328 328 if (_get_metadata_key(md, 'isolated', mime)) {
329 329 // Create an iframe to isolate the subarea from the rest of the
330 330 // document
331 331 var iframe = $('<iframe/>').addClass('box-flex1');
332 332 iframe.css({'height':1, 'width':'100%', 'display':'block'});
333 333 iframe.attr('frameborder', 0);
334 334 iframe.attr('scrolling', 'auto');
335 335
336 336 // Once the iframe is loaded, the subarea is dynamically inserted
337 337 iframe.on('load', function() {
338 338 // Workaround needed by Firefox, to properly render svg inside
339 339 // iframes, see http://stackoverflow.com/questions/10177190/
340 340 // svg-dynamically-added-to-iframe-does-not-render-correctly
341 341 this.contentDocument.open();
342 342
343 343 // Insert the subarea into the iframe
344 344 // We must directly write the html. When using Jquery's append
345 345 // method, javascript is evaluated in the parent document and
346 346 // not in the iframe document. At this point, subarea doesn't
347 347 // contain any user content.
348 348 this.contentDocument.write(subarea.html());
349 349
350 350 this.contentDocument.close();
351 351
352 352 var body = this.contentDocument.body;
353 353 // Adjust the iframe height automatically
354 354 iframe.height(body.scrollHeight + 'px');
355 355 });
356 356
357 357 // Elements should be appended to the inner subarea and not to the
358 358 // iframe
359 359 iframe.append = function(that) {
360 360 subarea.append(that);
361 361 };
362 362
363 363 return iframe;
364 364 } else {
365 365 return subarea;
366 366 }
367 367 }
368 368
369 369
370 370 OutputArea.prototype._append_javascript_error = function (err, element) {
371 371 // display a message when a javascript error occurs in display output
372 372 var msg = "Javascript error adding output!"
373 373 if ( element === undefined ) return;
374 374 element
375 375 .append($('<div/>').text(msg).addClass('js-error'))
376 376 .append($('<div/>').text(err.toString()).addClass('js-error'))
377 377 .append($('<div/>').text('See your browser Javascript console for more details.').addClass('js-error'));
378 378 };
379 379
380 380 OutputArea.prototype._safe_append = function (toinsert) {
381 381 // safely append an item to the document
382 382 // this is an object created by user code,
383 383 // and may have errors, which should not be raised
384 384 // under any circumstances.
385 385 try {
386 386 this.element.append(toinsert);
387 387 } catch(err) {
388 388 console.log(err);
389 389 // Create an actual output_area and output_subarea, which creates
390 390 // the prompt area and the proper indentation.
391 391 var toinsert = this.create_output_area();
392 392 var subarea = $('<div/>').addClass('output_subarea');
393 393 toinsert.append(subarea);
394 394 this._append_javascript_error(err, subarea);
395 395 this.element.append(toinsert);
396 396 }
397 397 };
398 398
399 399
400 400 OutputArea.prototype.append_pyout = function (json) {
401 401 var n = json.prompt_number || ' ';
402 402 var toinsert = this.create_output_area();
403 403 if (this.prompt_area) {
404 404 toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
405 405 }
406 406 this.append_mime_type(json, toinsert);
407 407 this._safe_append(toinsert);
408 408 // If we just output latex, typeset it.
409 409 if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
410 410 this.typeset();
411 411 }
412 412 };
413 413
414 414
415 415 OutputArea.prototype.append_pyerr = function (json) {
416 416 var tb = json.traceback;
417 417 if (tb !== undefined && tb.length > 0) {
418 418 var s = '';
419 419 var len = tb.length;
420 420 for (var i=0; i<len; i++) {
421 421 s = s + tb[i] + '\n';
422 422 }
423 423 s = s + '\n';
424 424 var toinsert = this.create_output_area();
425 425 this.append_text(s, {}, toinsert);
426 426 this._safe_append(toinsert);
427 427 }
428 428 };
429 429
430 430
431 431 OutputArea.prototype.append_stream = function (json) {
432 432 // temporary fix: if stream undefined (json file written prior to this patch),
433 433 // default to most likely stdout:
434 434 if (json.stream == undefined){
435 435 json.stream = 'stdout';
436 436 }
437 437 var text = json.text;
438 438 var subclass = "output_"+json.stream;
439 439 if (this.outputs.length > 0){
440 440 // have at least one output to consider
441 441 var last = this.outputs[this.outputs.length-1];
442 442 if (last.output_type == 'stream' && json.stream == last.stream){
443 443 // latest output was in the same stream,
444 444 // so append directly into its pre tag
445 445 // escape ANSI & HTML specials:
446 446 var pre = this.element.find('div.'+subclass).last().find('pre');
447 447 var html = utils.fixCarriageReturn(
448 448 pre.html() + utils.fixConsole(text));
449 449 // The only user content injected with this HTML call is
450 450 // escaped by the fixConsole() method.
451 451 pre.html(html);
452 452 return;
453 453 }
454 454 }
455 455
456 456 if (!text.replace("\r", "")) {
457 457 // text is nothing (empty string, \r, etc.)
458 458 // so don't append any elements, which might add undesirable space
459 459 return;
460 460 }
461 461
462 462 // If we got here, attach a new div
463 463 var toinsert = this.create_output_area();
464 464 this.append_text(text, {}, toinsert, "output_stream "+subclass);
465 465 this._safe_append(toinsert);
466 466 };
467 467
468 468
469 469 OutputArea.prototype.append_display_data = function (json) {
470 470 var toinsert = this.create_output_area();
471 471 if (this.append_mime_type(json, toinsert)) {
472 472 this._safe_append(toinsert);
473 473 // If we just output latex, typeset it.
474 474 if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
475 475 this.typeset();
476 476 }
477 477 }
478 478 };
479 479
480 480
481 481 OutputArea.safe_outputs = {
482 482 'text/plain' : true,
483 483 'image/png' : true,
484 484 'image/jpeg' : true
485 485 };
486 486
487 487 OutputArea.prototype.append_mime_type = function (json, element) {
488 488 for (var type_i in OutputArea.display_order) {
489 489 var type = OutputArea.display_order[type_i];
490 490 var append = OutputArea.append_map[type];
491 491 if ((json[type] !== undefined) && append) {
492 492 if (!this.trusted && !OutputArea.safe_outputs[type]) {
493 493 // not trusted show warning and do not display
494 494 var content = {
495 495 text : "Untrusted " + type + " output ignored.",
496 496 stream : "stderr"
497 497 }
498 498 this.append_stream(content);
499 499 continue;
500 500 }
501 501 var md = json.metadata || {};
502 502 var toinsert = append.apply(this, [json[type], md, element]);
503 503 $([IPython.events]).trigger('output_appended.OutputArea', [type, json[type], md, toinsert]);
504 504 return true;
505 505 }
506 506 }
507 507 return false;
508 508 };
509 509
510 510
511 511 OutputArea.prototype.append_html = function (html, md, element) {
512 512 var type = 'text/html';
513 513 var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
514 514 IPython.keyboard_manager.register_events(toinsert);
515 515 toinsert.append(html);
516 516 element.append(toinsert);
517 517 return toinsert;
518 518 };
519 519
520 520
521 521 OutputArea.prototype.append_javascript = function (js, md, element) {
522 522 // We just eval the JS code, element appears in the local scope.
523 523 var type = 'application/javascript';
524 524 var toinsert = this.create_output_subarea(md, "output_javascript", type);
525 525 IPython.keyboard_manager.register_events(toinsert);
526 526 element.append(toinsert);
527 527 // FIXME TODO : remove `container element for 3.0`
528 528 //backward compat, js should be eval'ed in a context where `container` is defined.
529 529 var container = element;
530 530 container.show = function(){console.log('Warning "container.show()" is deprecated.')};
531 531 // end backward compat
532 532 try {
533 533 eval(js);
534 534 } catch(err) {
535 535 console.log(err);
536 536 this._append_javascript_error(err, toinsert);
537 537 }
538 538 return toinsert;
539 539 };
540 540
541 541
542 542 OutputArea.prototype.append_text = function (data, md, element, extra_class) {
543 543 var type = 'text/plain';
544 544 var toinsert = this.create_output_subarea(md, "output_text", type);
545 545 // escape ANSI & HTML specials in plaintext:
546 546 data = utils.fixConsole(data);
547 547 data = utils.fixCarriageReturn(data);
548 548 data = utils.autoLinkUrls(data);
549 549 if (extra_class){
550 550 toinsert.addClass(extra_class);
551 551 }
552 552 // The only user content injected with this HTML call is
553 553 // escaped by the fixConsole() method.
554 554 toinsert.append($("<pre/>").html(data));
555 555 element.append(toinsert);
556 556 return toinsert;
557 557 };
558 558
559 559
560 560 OutputArea.prototype.append_svg = function (svg, md, element) {
561 561 var type = 'image/svg+xml';
562 562 var toinsert = this.create_output_subarea(md, "output_svg", type);
563 563 toinsert.append(svg);
564 564 element.append(toinsert);
565 565 return toinsert;
566 566 };
567 567
568 568
569 569 OutputArea.prototype._dblclick_to_reset_size = function (img) {
570 570 // wrap image after it's loaded on the page,
571 571 // otherwise the measured initial size will be incorrect
572 572 img.on("load", function (){
573 573 var h0 = img.height();
574 574 var w0 = img.width();
575 575 if (!(h0 && w0)) {
576 576 // zero size, don't make it resizable
577 577 return;
578 578 }
579 579 img.resizable({
580 580 aspectRatio: true,
581 581 autoHide: true
582 582 });
583 583 img.dblclick(function () {
584 584 // resize wrapper & image together for some reason:
585 585 img.parent().height(h0);
586 586 img.height(h0);
587 587 img.parent().width(w0);
588 588 img.width(w0);
589 589 });
590 590 });
591 591 };
592 592
593 593 var set_width_height = function (img, md, mime) {
594 594 // set width and height of an img element from metadata
595 595 var height = _get_metadata_key(md, 'height', mime);
596 596 if (height !== undefined) img.attr('height', height);
597 597 var width = _get_metadata_key(md, 'width', mime);
598 598 if (width !== undefined) img.attr('width', width);
599 599 };
600 600
601 601 OutputArea.prototype.append_png = function (png, md, element) {
602 602 var type = 'image/png';
603 603 var toinsert = this.create_output_subarea(md, "output_png", type);
604 604 var img = $("<img/>").attr('src','data:image/png;base64,'+png);
605 605 set_width_height(img, md, 'image/png');
606 606 this._dblclick_to_reset_size(img);
607 607 toinsert.append(img);
608 608 element.append(toinsert);
609 609 return toinsert;
610 610 };
611 611
612 612
613 613 OutputArea.prototype.append_jpeg = function (jpeg, md, element) {
614 614 var type = 'image/jpeg';
615 615 var toinsert = this.create_output_subarea(md, "output_jpeg", type);
616 616 var img = $("<img/>").attr('src','data:image/jpeg;base64,'+jpeg);
617 617 set_width_height(img, md, 'image/jpeg');
618 618 this._dblclick_to_reset_size(img);
619 619 toinsert.append(img);
620 620 element.append(toinsert);
621 621 return toinsert;
622 622 };
623 623
624 624
625 625 OutputArea.prototype.append_pdf = function (pdf, md, element) {
626 626 var type = 'application/pdf';
627 627 var toinsert = this.create_output_subarea(md, "output_pdf", type);
628 628 var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
629 629 a.attr('target', '_blank');
630 630 a.text('View PDF')
631 631 toinsert.append(a);
632 632 element.append(toinsert);
633 633 return toinsert;
634 634 }
635 635
636 636 OutputArea.prototype.append_latex = function (latex, md, element) {
637 637 // This method cannot do the typesetting because the latex first has to
638 638 // be on the page.
639 639 var type = 'text/latex';
640 640 var toinsert = this.create_output_subarea(md, "output_latex", type);
641 641 toinsert.append(latex);
642 642 element.append(toinsert);
643 643 return toinsert;
644 644 };
645 645
646 646
647 647 OutputArea.prototype.append_raw_input = function (msg) {
648 648 var that = this;
649 649 this.expand();
650 650 var content = msg.content;
651 651 var area = this.create_output_area();
652 652
653 653 // disable any other raw_inputs, if they are left around
654 654 $("div.output_subarea.raw_input").remove();
655 655
656 656 area.append(
657 657 $("<div/>")
658 658 .addClass("box-flex1 output_subarea raw_input")
659 659 .append(
660 660 $("<span/>")
661 661 .addClass("input_prompt")
662 662 .text(content.prompt)
663 663 )
664 664 .append(
665 665 $("<input/>")
666 666 .addClass("raw_input")
667 667 .attr('type', 'text')
668 668 .attr("size", 47)
669 669 .keydown(function (event, ui) {
670 670 // make sure we submit on enter,
671 671 // and don't re-execute the *cell* on shift-enter
672 if (event.which === utils.keycodes.ENTER) {
672 if (event.which === IPython.keyboard.keycodes.enter) {
673 673 that._submit_raw_input();
674 674 return false;
675 675 }
676 676 })
677 677 )
678 678 );
679 679
680 680 this.element.append(area);
681 681 var raw_input = area.find('input.raw_input');
682 682 // Register events that enable/disable the keyboard manager while raw
683 683 // input is focused.
684 684 IPython.keyboard_manager.register_events(raw_input);
685 685 // Note, the following line used to read raw_input.focus().focus().
686 686 // This seemed to be needed otherwise only the cell would be focused.
687 687 // But with the modal UI, this seems to work fine with one call to focus().
688 688 raw_input.focus();
689 689 }
690 690
691 691 OutputArea.prototype._submit_raw_input = function (evt) {
692 692 var container = this.element.find("div.raw_input");
693 693 var theprompt = container.find("span.input_prompt");
694 694 var theinput = container.find("input.raw_input");
695 695 var value = theinput.val();
696 696 var content = {
697 697 output_type : 'stream',
698 698 name : 'stdout',
699 699 text : theprompt.text() + value + '\n'
700 700 }
701 701 // remove form container
702 702 container.parent().remove();
703 703 // replace with plaintext version in stdout
704 704 this.append_output(content, false);
705 705 $([IPython.events]).trigger('send_input_reply.Kernel', value);
706 706 }
707 707
708 708
709 709 OutputArea.prototype.handle_clear_output = function (msg) {
710 710 // msg spec v4 had stdout, stderr, display keys
711 711 // v4.1 replaced these with just wait
712 712 // The default behavior is the same (stdout=stderr=display=True, wait=False),
713 713 // so v4 messages will still be properly handled,
714 714 // except for the rarely used clearing less than all output.
715 715 this.clear_output(msg.content.wait || false);
716 716 };
717 717
718 718
719 719 OutputArea.prototype.clear_output = function(wait) {
720 720 if (wait) {
721 721
722 722 // If a clear is queued, clear before adding another to the queue.
723 723 if (this.clear_queued) {
724 724 this.clear_output(false);
725 725 };
726 726
727 727 this.clear_queued = true;
728 728 } else {
729 729
730 730 // Fix the output div's height if the clear_output is waiting for
731 731 // new output (it is being used in an animation).
732 732 if (this.clear_queued) {
733 733 var height = this.element.height();
734 734 this.element.height(height);
735 735 this.clear_queued = false;
736 736 }
737 737
738 738 // clear all, no need for logic
739 739 this.element.html("");
740 740 this.outputs = [];
741 741 this.trusted = true;
742 742 this.unscroll_area();
743 743 return;
744 744 };
745 745 };
746 746
747 747
748 748 // JSON serialization
749 749
750 750 OutputArea.prototype.fromJSON = function (outputs) {
751 751 var len = outputs.length;
752 752 var data;
753 753
754 754 for (var i=0; i<len; i++) {
755 755 data = outputs[i];
756 756 var msg_type = data.output_type;
757 757 if (msg_type === "display_data" || msg_type === "pyout") {
758 758 // convert short keys to mime keys
759 759 // TODO: remove mapping of short keys when we update to nbformat 4
760 760 data = this.rename_keys(data, OutputArea.mime_map_r);
761 761 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map_r);
762 762 }
763 763
764 764 this.append_output(data);
765 765 }
766 766 };
767 767
768 768
769 769 OutputArea.prototype.toJSON = function () {
770 770 var outputs = [];
771 771 var len = this.outputs.length;
772 772 var data;
773 773 for (var i=0; i<len; i++) {
774 774 data = this.outputs[i];
775 775 var msg_type = data.output_type;
776 776 if (msg_type === "display_data" || msg_type === "pyout") {
777 777 // convert mime keys to short keys
778 778 data = this.rename_keys(data, OutputArea.mime_map);
779 779 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map);
780 780 }
781 781 outputs[i] = data;
782 782 }
783 783 return outputs;
784 784 };
785 785
786 786 /**
787 787 * Class properties
788 788 **/
789 789
790 790 /**
791 791 * Threshold to trigger autoscroll when the OutputArea is resized,
792 792 * typically when new outputs are added.
793 793 *
794 794 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
795 795 * unless it is < 0, in which case autoscroll will never be triggered
796 796 *
797 797 * @property auto_scroll_threshold
798 798 * @type Number
799 799 * @default 100
800 800 *
801 801 **/
802 802 OutputArea.auto_scroll_threshold = 100;
803 803
804 804 /**
805 805 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
806 806 * shorter than this are never scrolled.
807 807 *
808 808 * @property minimum_scroll_threshold
809 809 * @type Number
810 810 * @default 20
811 811 *
812 812 **/
813 813 OutputArea.minimum_scroll_threshold = 20;
814 814
815 815
816 816
817 817 OutputArea.mime_map = {
818 818 "text/plain" : "text",
819 819 "text/html" : "html",
820 820 "image/svg+xml" : "svg",
821 821 "image/png" : "png",
822 822 "image/jpeg" : "jpeg",
823 823 "text/latex" : "latex",
824 824 "application/json" : "json",
825 825 "application/javascript" : "javascript",
826 826 };
827 827
828 828 OutputArea.mime_map_r = {
829 829 "text" : "text/plain",
830 830 "html" : "text/html",
831 831 "svg" : "image/svg+xml",
832 832 "png" : "image/png",
833 833 "jpeg" : "image/jpeg",
834 834 "latex" : "text/latex",
835 835 "json" : "application/json",
836 836 "javascript" : "application/javascript",
837 837 };
838 838
839 839 OutputArea.display_order = [
840 840 'application/javascript',
841 841 'text/html',
842 842 'text/latex',
843 843 'image/svg+xml',
844 844 'image/png',
845 845 'image/jpeg',
846 846 'application/pdf',
847 847 'text/plain'
848 848 ];
849 849
850 850 OutputArea.append_map = {
851 851 "text/plain" : OutputArea.prototype.append_text,
852 852 "text/html" : OutputArea.prototype.append_html,
853 853 "image/svg+xml" : OutputArea.prototype.append_svg,
854 854 "image/png" : OutputArea.prototype.append_png,
855 855 "image/jpeg" : OutputArea.prototype.append_jpeg,
856 856 "text/latex" : OutputArea.prototype.append_latex,
857 857 "application/javascript" : OutputArea.prototype.append_javascript,
858 858 "application/pdf" : OutputArea.prototype.append_pdf
859 859 };
860 860
861 861 IPython.OutputArea = OutputArea;
862 862
863 863 return IPython;
864 864
865 865 }(IPython));
@@ -1,175 +1,175 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // SaveWidget
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 var utils = IPython.utils;
16 16
17 17 var SaveWidget = function (selector) {
18 18 this.selector = selector;
19 19 if (this.selector !== undefined) {
20 20 this.element = $(selector);
21 21 this.style();
22 22 this.bind_events();
23 23 }
24 24 };
25 25
26 26
27 27 SaveWidget.prototype.style = function () {
28 28 };
29 29
30 30
31 31 SaveWidget.prototype.bind_events = function () {
32 32 var that = this;
33 33 this.element.find('span#notebook_name').click(function () {
34 34 that.rename_notebook();
35 35 });
36 36 this.element.find('span#notebook_name').hover(function () {
37 37 $(this).addClass("ui-state-hover");
38 38 }, function () {
39 39 $(this).removeClass("ui-state-hover");
40 40 });
41 41 $([IPython.events]).on('notebook_loaded.Notebook', function () {
42 42 that.update_notebook_name();
43 43 that.update_document_title();
44 44 });
45 45 $([IPython.events]).on('notebook_saved.Notebook', function () {
46 46 that.update_notebook_name();
47 47 that.update_document_title();
48 48 });
49 49 $([IPython.events]).on('notebook_renamed.Notebook', function () {
50 50 that.update_notebook_name();
51 51 that.update_document_title();
52 52 that.update_address_bar();
53 53 });
54 54 $([IPython.events]).on('notebook_save_failed.Notebook', function () {
55 55 that.set_save_status('Autosave Failed!');
56 56 });
57 57 $([IPython.events]).on('checkpoints_listed.Notebook', function (event, data) {
58 58 that.set_last_checkpoint(data[0]);
59 59 });
60 60
61 61 $([IPython.events]).on('checkpoint_created.Notebook', function (event, data) {
62 62 that.set_last_checkpoint(data);
63 63 });
64 64 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
65 65 that.set_autosaved(data.value);
66 66 });
67 67 };
68 68
69 69
70 70 SaveWidget.prototype.rename_notebook = function () {
71 71 var that = this;
72 72 var dialog = $('<div/>').append(
73 73 $("<p/>").addClass("rename-message")
74 74 .text('Enter a new notebook name:')
75 75 ).append(
76 76 $("<br/>")
77 77 ).append(
78 78 $('<input/>').attr('type','text').attr('size','25')
79 79 .val(IPython.notebook.get_notebook_name())
80 80 );
81 81 IPython.dialog.modal({
82 82 title: "Rename Notebook",
83 83 body: dialog,
84 84 buttons : {
85 85 "Cancel": {},
86 86 "OK": {
87 87 class: "btn-primary",
88 88 click: function () {
89 89 var new_name = $(this).find('input').val();
90 90 if (!IPython.notebook.test_notebook_name(new_name)) {
91 91 $(this).find('.rename-message').text(
92 92 "Invalid notebook name. Notebook names must "+
93 93 "have 1 or more characters and can contain any characters " +
94 94 "except :/\\. Please enter a new notebook name:"
95 95 );
96 96 return false;
97 97 } else {
98 98 IPython.notebook.rename(new_name);
99 99 }
100 100 }}
101 101 },
102 102 open : function (event, ui) {
103 103 var that = $(this);
104 104 // Upon ENTER, click the OK button.
105 105 that.find('input[type="text"]').keydown(function (event, ui) {
106 if (event.which === utils.keycodes.ENTER) {
106 if (event.which === IPython.keyboard.keycodes.enter) {
107 107 that.find('.btn-primary').first().click();
108 108 return false;
109 109 }
110 110 });
111 111 that.find('input[type="text"]').focus().select();
112 112 }
113 113 });
114 114 }
115 115
116 116
117 117 SaveWidget.prototype.update_notebook_name = function () {
118 118 var nbname = IPython.notebook.get_notebook_name();
119 119 this.element.find('span#notebook_name').text(nbname);
120 120 };
121 121
122 122
123 123 SaveWidget.prototype.update_document_title = function () {
124 124 var nbname = IPython.notebook.get_notebook_name();
125 125 document.title = nbname;
126 126 };
127 127
128 128 SaveWidget.prototype.update_address_bar = function(){
129 129 var base_url = IPython.notebook.base_url;
130 130 var nbname = IPython.notebook.notebook_name;
131 131 var path = IPython.notebook.notebook_path;
132 132 var state = {path : path, name: nbname};
133 133 window.history.replaceState(state, "", utils.url_join_encode(
134 134 base_url,
135 135 "notebooks",
136 136 path,
137 137 nbname)
138 138 );
139 139 };
140 140
141 141
142 142 SaveWidget.prototype.set_save_status = function (msg) {
143 143 this.element.find('span#autosave_status').text(msg);
144 144 }
145 145
146 146 SaveWidget.prototype.set_checkpoint_status = function (msg) {
147 147 this.element.find('span#checkpoint_status').text(msg);
148 148 }
149 149
150 150 SaveWidget.prototype.set_last_checkpoint = function (checkpoint) {
151 151 if (!checkpoint) {
152 152 this.set_checkpoint_status("");
153 153 return;
154 154 }
155 155 var d = new Date(checkpoint.last_modified);
156 156 this.set_checkpoint_status(
157 157 "Last Checkpoint: " + d.format('mmm dd HH:MM')
158 158 );
159 159 }
160 160
161 161 SaveWidget.prototype.set_autosaved = function (dirty) {
162 162 if (dirty) {
163 163 this.set_save_status("(unsaved changes)");
164 164 } else {
165 165 this.set_save_status("(autosaved)");
166 166 }
167 167 };
168 168
169 169
170 170 IPython.SaveWidget = SaveWidget;
171 171
172 172 return IPython;
173 173
174 174 }(IPython));
175 175
@@ -1,561 +1,561 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2012 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // TextCell
10 10 //============================================================================
11 11
12 12
13 13
14 14 /**
15 15 A module that allow to create different type of Text Cell
16 16 @module IPython
17 17 @namespace IPython
18 18 */
19 19 var IPython = (function (IPython) {
20 20 "use strict";
21 21
22 22 // TextCell base class
23 var key = IPython.utils.keycodes;
23 var keycodes = IPython.keyboard.keycodes;
24 24
25 25 /**
26 26 * Construct a new TextCell, codemirror mode is by default 'htmlmixed', and cell type is 'text'
27 27 * cell start as not redered.
28 28 *
29 29 * @class TextCell
30 30 * @constructor TextCell
31 31 * @extend IPython.Cell
32 32 * @param {object|undefined} [options]
33 33 * @param [options.cm_config] {object} config to pass to CodeMirror, will extend/overwrite default config
34 34 * @param [options.placeholder] {string} default string to use when souce in empty for rendering (only use in some TextCell subclass)
35 35 */
36 36 var TextCell = function (options) {
37 37 // in all TextCell/Cell subclasses
38 38 // do not assign most of members here, just pass it down
39 39 // in the options dict potentially overwriting what you wish.
40 40 // they will be assigned in the base class.
41 41
42 42 // we cannot put this as a class key as it has handle to "this".
43 43 var cm_overwrite_options = {
44 44 onKeyEvent: $.proxy(this.handle_keyevent,this)
45 45 };
46 46
47 47 options = this.mergeopt(TextCell,options,{cm_config:cm_overwrite_options});
48 48
49 49 this.cell_type = this.cell_type || 'text';
50 50
51 51 IPython.Cell.apply(this, [options]);
52 52
53 53 this.rendered = false;
54 54 };
55 55
56 56 TextCell.prototype = new IPython.Cell();
57 57
58 58 TextCell.options_default = {
59 59 cm_config : {
60 60 extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
61 61 mode: 'htmlmixed',
62 62 lineWrapping : true,
63 63 }
64 64 };
65 65
66 66
67 67 /**
68 68 * Create the DOM element of the TextCell
69 69 * @method create_element
70 70 * @private
71 71 */
72 72 TextCell.prototype.create_element = function () {
73 73 IPython.Cell.prototype.create_element.apply(this, arguments);
74 74
75 75 var cell = $("<div>").addClass('cell text_cell border-box-sizing');
76 76 cell.attr('tabindex','2');
77 77
78 78 var prompt = $('<div/>').addClass('prompt input_prompt');
79 79 cell.append(prompt);
80 80 var inner_cell = $('<div/>').addClass('inner_cell');
81 81 this.celltoolbar = new IPython.CellToolbar(this);
82 82 inner_cell.append(this.celltoolbar.element);
83 83 var input_area = $('<div/>').addClass('input_area');
84 84 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
85 85 // The tabindex=-1 makes this div focusable.
86 86 var render_area = $('<div/>').addClass('text_cell_render border-box-sizing').
87 87 addClass('rendered_html').attr('tabindex','-1');
88 88 inner_cell.append(input_area).append(render_area);
89 89 cell.append(inner_cell);
90 90 this.element = cell;
91 91 };
92 92
93 93
94 94 /**
95 95 * Bind the DOM evet to cell actions
96 96 * Need to be called after TextCell.create_element
97 97 * @private
98 98 * @method bind_event
99 99 */
100 100 TextCell.prototype.bind_events = function () {
101 101 IPython.Cell.prototype.bind_events.apply(this);
102 102 var that = this;
103 103
104 104 this.element.dblclick(function () {
105 105 if (that.selected === false) {
106 106 $([IPython.events]).trigger('select.Cell', {'cell':that});
107 107 }
108 108 var cont = that.unrender();
109 109 if (cont) {
110 110 that.focus_editor();
111 111 }
112 112 });
113 113 };
114 114
115 115 TextCell.prototype.handle_keyevent = function (editor, event) {
116 116
117 117 // console.log('CM', this.mode, event.which, event.type)
118 118
119 119 if (this.mode === 'command') {
120 120 return true;
121 121 } else if (this.mode === 'edit') {
122 122 return this.handle_codemirror_keyevent(editor, event);
123 123 }
124 124 };
125 125
126 126 /**
127 127 * This method gets called in CodeMirror's onKeyDown/onKeyPress
128 128 * handlers and is used to provide custom key handling.
129 129 *
130 130 * Subclass should override this method to have custom handeling
131 131 *
132 132 * @method handle_codemirror_keyevent
133 133 * @param {CodeMirror} editor - The codemirror instance bound to the cell
134 134 * @param {event} event -
135 135 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
136 136 */
137 137 TextCell.prototype.handle_codemirror_keyevent = function (editor, event) {
138 138 var that = this;
139 139
140 140 if (event.keyCode === 13 && (event.shiftKey || event.ctrlKey || event.altKey)) {
141 141 // Always ignore shift-enter in CodeMirror as we handle it.
142 142 return true;
143 } else if (event.which === key.UPARROW && event.type === 'keydown') {
143 } else if (event.which === keycodes.up && event.type === 'keydown') {
144 144 // If we are not at the top, let CM handle the up arrow and
145 145 // prevent the global keydown handler from handling it.
146 146 if (!that.at_top()) {
147 147 event.stop();
148 148 return false;
149 149 } else {
150 150 return true;
151 }
152 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
151 };
152 } else if (event.which === keycodes.down && event.type === 'keydown') {
153 153 // If we are not at the bottom, let CM handle the down arrow and
154 154 // prevent the global keydown handler from handling it.
155 155 if (!that.at_bottom()) {
156 156 event.stop();
157 157 return false;
158 158 } else {
159 159 return true;
160 }
161 } else if (event.which === key.ESC && event.type === 'keydown') {
160 };
161 } else if (event.which === keycodes.esc && event.type === 'keydown') {
162 162 if (that.code_mirror.options.keyMap === "vim-insert") {
163 163 // vim keyMap is active and in insert mode. In this case we leave vim
164 164 // insert mode, but remain in notebook edit mode.
165 165 // Let' CM handle this event and prevent global handling.
166 166 event.stop();
167 167 return false;
168 168 } else {
169 169 // vim keyMap is not active. Leave notebook edit mode.
170 170 // Don't let CM handle the event, defer to global handling.
171 171 return true;
172 172 }
173 173 }
174 174 return false;
175 175 };
176 176
177 177 // Cell level actions
178 178
179 179 TextCell.prototype.select = function () {
180 180 var cont = IPython.Cell.prototype.select.apply(this);
181 181 if (cont) {
182 182 if (this.mode === 'edit') {
183 183 this.code_mirror.refresh();
184 184 }
185 185 }
186 186 return cont;
187 187 };
188 188
189 189 TextCell.prototype.unrender = function () {
190 190 if (this.read_only) return;
191 191 var cont = IPython.Cell.prototype.unrender.apply(this);
192 192 if (cont) {
193 193 var text_cell = this.element;
194 194 var output = text_cell.find("div.text_cell_render");
195 195 output.hide();
196 196 text_cell.find('div.input_area').show();
197 197 if (this.get_text() === this.placeholder) {
198 198 this.set_text('');
199 199 }
200 200 this.refresh();
201 201 }
202 202 return cont;
203 203 };
204 204
205 205 TextCell.prototype.execute = function () {
206 206 this.render();
207 207 };
208 208
209 209 /**
210 210 * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
211 211 * @method get_text
212 212 * @retrun {string} CodeMirror current text value
213 213 */
214 214 TextCell.prototype.get_text = function() {
215 215 return this.code_mirror.getValue();
216 216 };
217 217
218 218 /**
219 219 * @param {string} text - Codemiror text value
220 220 * @see TextCell#get_text
221 221 * @method set_text
222 222 * */
223 223 TextCell.prototype.set_text = function(text) {
224 224 this.code_mirror.setValue(text);
225 225 this.code_mirror.refresh();
226 226 };
227 227
228 228 /**
229 229 * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
230 230 * @method get_rendered
231 231 * @return {html} html of rendered element
232 232 * */
233 233 TextCell.prototype.get_rendered = function() {
234 234 return this.element.find('div.text_cell_render').html();
235 235 };
236 236
237 237 /**
238 238 * @method set_rendered
239 239 */
240 240 TextCell.prototype.set_rendered = function(text) {
241 241 this.element.find('div.text_cell_render').html(text);
242 242 };
243 243
244 244 /**
245 245 * @method at_top
246 246 * @return {Boolean}
247 247 */
248 248 TextCell.prototype.at_top = function () {
249 249 if (this.rendered) {
250 250 return true;
251 251 } else {
252 252 var cursor = this.code_mirror.getCursor();
253 253 if (cursor.line === 0 && cursor.ch === 0) {
254 254 return true;
255 255 } else {
256 256 return false;
257 257 }
258 258 }
259 259 };
260 260
261 261 /**
262 262 * @method at_bottom
263 263 * @return {Boolean}
264 264 * */
265 265 TextCell.prototype.at_bottom = function () {
266 266 if (this.rendered) {
267 267 return true;
268 268 } else {
269 269 var cursor = this.code_mirror.getCursor();
270 270 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
271 271 return true;
272 272 } else {
273 273 return false;
274 274 }
275 275 }
276 276 };
277 277
278 278 /**
279 279 * Create Text cell from JSON
280 280 * @param {json} data - JSON serialized text-cell
281 281 * @method fromJSON
282 282 */
283 283 TextCell.prototype.fromJSON = function (data) {
284 284 IPython.Cell.prototype.fromJSON.apply(this, arguments);
285 285 if (data.cell_type === this.cell_type) {
286 286 if (data.source !== undefined) {
287 287 this.set_text(data.source);
288 288 // make this value the starting point, so that we can only undo
289 289 // to this state, instead of a blank cell
290 290 this.code_mirror.clearHistory();
291 291 // TODO: This HTML needs to be treated as potentially dangerous
292 292 // user input and should be handled before set_rendered.
293 293 this.set_rendered(data.rendered || '');
294 294 this.rendered = false;
295 295 this.render();
296 296 }
297 297 }
298 298 };
299 299
300 300 /** Generate JSON from cell
301 301 * @return {object} cell data serialised to json
302 302 */
303 303 TextCell.prototype.toJSON = function () {
304 304 var data = IPython.Cell.prototype.toJSON.apply(this);
305 305 data.source = this.get_text();
306 306 if (data.source == this.placeholder) {
307 307 data.source = "";
308 308 }
309 309 return data;
310 310 };
311 311
312 312
313 313 /**
314 314 * @class MarkdownCell
315 315 * @constructor MarkdownCell
316 316 * @extends IPython.HTMLCell
317 317 */
318 318 var MarkdownCell = function (options) {
319 319 options = this.mergeopt(MarkdownCell, options);
320 320
321 321 this.cell_type = 'markdown';
322 322 TextCell.apply(this, [options]);
323 323 };
324 324
325 325 MarkdownCell.options_default = {
326 326 cm_config: {
327 327 mode: 'gfm'
328 328 },
329 329 placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
330 330 };
331 331
332 332 MarkdownCell.prototype = new TextCell();
333 333
334 334 /**
335 335 * @method render
336 336 */
337 337 MarkdownCell.prototype.render = function () {
338 338 var cont = IPython.TextCell.prototype.render.apply(this);
339 339 if (cont) {
340 340 var text = this.get_text();
341 341 var math = null;
342 342 if (text === "") { text = this.placeholder; }
343 343 var text_and_math = IPython.mathjaxutils.remove_math(text);
344 344 text = text_and_math[0];
345 345 math = text_and_math[1];
346 346 var html = marked.parser(marked.lexer(text));
347 347 html = $(IPython.mathjaxutils.replace_math(html, math));
348 348 // Links in markdown cells should open in new tabs.
349 349 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
350 350 try {
351 351 // TODO: This HTML needs to be treated as potentially dangerous
352 352 // user input and should be handled before set_rendered.
353 353 this.set_rendered(html);
354 354 } catch (e) {
355 355 console.log("Error running Javascript in Markdown:");
356 356 console.log(e);
357 357 this.set_rendered(
358 358 $("<div/>")
359 359 .append($("<div/>").text('Error rendering Markdown!').addClass("js-error"))
360 360 .append($("<div/>").text(e.toString()).addClass("js-error"))
361 361 .html()
362 362 );
363 363 }
364 364 this.element.find('div.input_area').hide();
365 365 this.element.find("div.text_cell_render").show();
366 366 this.typeset();
367 367 }
368 368 return cont;
369 369 };
370 370
371 371
372 372 // RawCell
373 373
374 374 /**
375 375 * @class RawCell
376 376 * @constructor RawCell
377 377 * @extends IPython.TextCell
378 378 */
379 379 var RawCell = function (options) {
380 380
381 381 options = this.mergeopt(RawCell,options);
382 382 TextCell.apply(this, [options]);
383 383 this.cell_type = 'raw';
384 384 // RawCell should always hide its rendered div
385 385 this.element.find('div.text_cell_render').hide();
386 386 };
387 387
388 388 RawCell.options_default = {
389 389 placeholder : "Write raw LaTeX or other formats here, for use with nbconvert.\n" +
390 390 "It will not be rendered in the notebook.\n" +
391 391 "When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
392 392 };
393 393
394 394 RawCell.prototype = new TextCell();
395 395
396 396 /** @method bind_events **/
397 397 RawCell.prototype.bind_events = function () {
398 398 TextCell.prototype.bind_events.apply(this);
399 399 var that = this;
400 400 this.element.focusout(function() {
401 401 that.auto_highlight();
402 402 });
403 403 };
404 404
405 405 /**
406 406 * Trigger autodetection of highlight scheme for current cell
407 407 * @method auto_highlight
408 408 */
409 409 RawCell.prototype.auto_highlight = function () {
410 410 this._auto_highlight(IPython.config.raw_cell_highlight);
411 411 };
412 412
413 413 /** @method render **/
414 414 RawCell.prototype.render = function () {
415 415 // Make sure that this cell type can never be rendered
416 416 if (this.rendered) {
417 417 this.unrender();
418 418 }
419 419 var text = this.get_text();
420 420 if (text === "") { text = this.placeholder; }
421 421 this.set_text(text);
422 422 };
423 423
424 424
425 425 /**
426 426 * @class HeadingCell
427 427 * @extends IPython.TextCell
428 428 */
429 429
430 430 /**
431 431 * @constructor HeadingCell
432 432 * @extends IPython.TextCell
433 433 */
434 434 var HeadingCell = function (options) {
435 435 options = this.mergeopt(HeadingCell, options);
436 436
437 437 this.level = 1;
438 438 this.cell_type = 'heading';
439 439 TextCell.apply(this, [options]);
440 440
441 441 /**
442 442 * heading level of the cell, use getter and setter to access
443 443 * @property level
444 444 */
445 445 };
446 446
447 447 HeadingCell.options_default = {
448 448 placeholder: "Type Heading Here"
449 449 };
450 450
451 451 HeadingCell.prototype = new TextCell();
452 452
453 453 /** @method fromJSON */
454 454 HeadingCell.prototype.fromJSON = function (data) {
455 455 if (data.level !== undefined){
456 456 this.level = data.level;
457 457 }
458 458 TextCell.prototype.fromJSON.apply(this, arguments);
459 459 };
460 460
461 461
462 462 /** @method toJSON */
463 463 HeadingCell.prototype.toJSON = function () {
464 464 var data = TextCell.prototype.toJSON.apply(this);
465 465 data.level = this.get_level();
466 466 return data;
467 467 };
468 468
469 469 /**
470 470 * can the cell be split into two cells
471 471 * @method is_splittable
472 472 **/
473 473 HeadingCell.prototype.is_splittable = function () {
474 474 return false;
475 475 };
476 476
477 477
478 478 /**
479 479 * can the cell be merged with other cells
480 480 * @method is_mergeable
481 481 **/
482 482 HeadingCell.prototype.is_mergeable = function () {
483 483 return false;
484 484 };
485 485
486 486 /**
487 487 * Change heading level of cell, and re-render
488 488 * @method set_level
489 489 */
490 490 HeadingCell.prototype.set_level = function (level) {
491 491 this.level = level;
492 492 if (this.rendered) {
493 493 this.rendered = false;
494 494 this.render();
495 495 }
496 496 };
497 497
498 498 /** The depth of header cell, based on html (h1 to h6)
499 499 * @method get_level
500 500 * @return {integer} level - for 1 to 6
501 501 */
502 502 HeadingCell.prototype.get_level = function () {
503 503 return this.level;
504 504 };
505 505
506 506
507 507 HeadingCell.prototype.set_rendered = function (html) {
508 508 this.element.find("div.text_cell_render").html(html);
509 509 };
510 510
511 511
512 512 HeadingCell.prototype.get_rendered = function () {
513 513 var r = this.element.find("div.text_cell_render");
514 514 return r.children().first().html();
515 515 };
516 516
517 517
518 518 HeadingCell.prototype.render = function () {
519 519 var cont = IPython.TextCell.prototype.render.apply(this);
520 520 if (cont) {
521 521 var text = this.get_text();
522 522 var math = null;
523 523 // Markdown headings must be a single line
524 524 text = text.replace(/\n/g, ' ');
525 525 if (text === "") { text = this.placeholder; }
526 526 text = Array(this.level + 1).join("#") + " " + text;
527 527 var text_and_math = IPython.mathjaxutils.remove_math(text);
528 528 text = text_and_math[0];
529 529 math = text_and_math[1];
530 530 var html = marked.parser(marked.lexer(text));
531 531 var h = $(IPython.mathjaxutils.replace_math(html, math));
532 532 // add id and linkback anchor
533 533 var hash = h.text().replace(/ /g, '-');
534 534 h.attr('id', hash);
535 535 h.append(
536 536 $('<a/>')
537 537 .addClass('anchor-link')
538 538 .attr('href', '#' + hash)
539 539 .text('¶')
540 540 );
541 541 // TODO: This HTML needs to be treated as potentially dangerous
542 542 // user input and should be handled before set_rendered.
543 543 this.set_rendered(h);
544 544 this.typeset();
545 545 this.element.find('div.input_area').hide();
546 546 this.element.find("div.text_cell_render").show();
547 547
548 548 }
549 549 return cont;
550 550 };
551 551
552 552 IPython.TextCell = TextCell;
553 553 IPython.MarkdownCell = MarkdownCell;
554 554 IPython.RawCell = RawCell;
555 555 IPython.HeadingCell = HeadingCell;
556 556
557 557
558 558 return IPython;
559 559
560 560 }(IPython));
561 561
@@ -1,352 +1,353 b''
1 1 {% extends "page.html" %}
2 2
3 3 {% block stylesheet %}
4 4
5 5 {% if mathjax_url %}
6 6 <script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML-full&delayStartupUntil=configured" charset="utf-8"></script>
7 7 {% endif %}
8 8 <script type="text/javascript">
9 9 // MathJax disabled, set as null to distingish from *missing* MathJax,
10 10 // where it will be undefined, and should prompt a dialog later.
11 11 window.mathjax_url = "{{mathjax_url}}";
12 12 </script>
13 13
14 14 <link rel="stylesheet" href="{{ static_url("components/codemirror/lib/codemirror.css") }}">
15 15
16 16 {{super()}}
17 17
18 18 <link rel="stylesheet" href="{{ static_url("notebook/css/override.css") }}" type="text/css" />
19 19
20 20 {% endblock %}
21 21
22 22 {% block params %}
23 23
24 24 data-project="{{project}}"
25 25 data-base-url="{{base_url}}"
26 26 data-notebook-name="{{notebook_name}}"
27 27 data-notebook-path="{{notebook_path}}"
28 28 class="notebook_app"
29 29
30 30 {% endblock %}
31 31
32 32
33 33 {% block header %}
34 34
35 35 <span id="save_widget" class="nav pull-left">
36 36 <span id="notebook_name"></span>
37 37 <span id="checkpoint_status"></span>
38 38 <span id="autosave_status"></span>
39 39 </span>
40 40
41 41 {% endblock %}
42 42
43 43
44 44 {% block site %}
45 45
46 46 <div id="menubar-container" class="container">
47 47 <div id="menubar">
48 48 <div class="navbar">
49 49 <div class="navbar-inner">
50 50 <div class="container">
51 51 <ul id="menus" class="nav">
52 52 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a>
53 53 <ul id="file_menu" class="dropdown-menu">
54 54 <li id="new_notebook"
55 55 title="Make a new notebook (Opens a new window)">
56 56 <a href="#">New</a></li>
57 57 <li id="open_notebook"
58 58 title="Opens a new window with the Dashboard view">
59 59 <a href="#">Open...</a></li>
60 60 <!-- <hr/> -->
61 61 <li class="divider"></li>
62 62 <li id="copy_notebook"
63 63 title="Open a copy of this notebook's contents and start a new kernel">
64 64 <a href="#">Make a Copy...</a></li>
65 65 <li id="rename_notebook"><a href="#">Rename...</a></li>
66 66 <li id="save_checkpoint"><a href="#">Save and Checkpoint</a></li>
67 67 <!-- <hr/> -->
68 68 <li class="divider"></li>
69 69 <li id="restore_checkpoint" class="dropdown-submenu"><a href="#">Revert to Checkpoint</a>
70 70 <ul class="dropdown-menu">
71 71 <li><a href="#"></a></li>
72 72 <li><a href="#"></a></li>
73 73 <li><a href="#"></a></li>
74 74 <li><a href="#"></a></li>
75 75 <li><a href="#"></a></li>
76 76 </ul>
77 77 </li>
78 78 <li class="divider"></li>
79 79 <li id="print_preview"><a href="#">Print Preview</a></li>
80 80 <li class="dropdown-submenu"><a href="#">Download as</a>
81 81 <ul class="dropdown-menu">
82 82 <li id="download_ipynb"><a href="#">IPython Notebook (.ipynb)</a></li>
83 83 <li id="download_py"><a href="#">Python (.py)</a></li>
84 84 <li id="download_html"><a href="#">HTML (.html)</a></li>
85 85 <li id="download_rst"><a href="#">reST (.rst)</a></li>
86 86 </ul>
87 87 </li>
88 88 <li class="divider"></li>
89 89
90 90 <li id="kill_and_exit"
91 91 title="Shutdown this notebook's kernel, and close this window">
92 92 <a href="#" >Close and halt</a></li>
93 93 </ul>
94 94 </li>
95 95 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit</a>
96 96 <ul id="edit_menu" class="dropdown-menu">
97 97 <li id="cut_cell"><a href="#">Cut Cell</a></li>
98 98 <li id="copy_cell"><a href="#">Copy Cell</a></li>
99 99 <li id="paste_cell_above" class="disabled"><a href="#">Paste Cell Above</a></li>
100 100 <li id="paste_cell_below" class="disabled"><a href="#">Paste Cell Below</a></li>
101 101 <li id="paste_cell_replace" class="disabled"><a href="#">Paste Cell &amp; Replace</a></li>
102 102 <li id="delete_cell"><a href="#">Delete Cell</a></li>
103 103 <li id="undelete_cell" class="disabled"><a href="#">Undo Delete Cell</a></li>
104 104 <li class="divider"></li>
105 105 <li id="split_cell"><a href="#">Split Cell</a></li>
106 106 <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
107 107 <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
108 108 <li class="divider"></li>
109 109 <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
110 110 <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
111 111 <li class="divider"></li>
112 112 <li id="edit_nb_metadata"><a href="#">Edit Notebook Metadata</a></li>
113 113 </ul>
114 114 </li>
115 115 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
116 116 <ul id="view_menu" class="dropdown-menu">
117 117 <li id="toggle_header"
118 118 title="Show/Hide the IPython Notebook logo and notebook title (above menu bar)">
119 119 <a href="#">Toggle Header</a></li>
120 120 <li id="toggle_toolbar"
121 121 title="Show/Hide the action icons (below menu bar)">
122 122 <a href="#">Toggle Toolbar</a></li>
123 123 </ul>
124 124 </li>
125 125 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Insert</a>
126 126 <ul id="insert_menu" class="dropdown-menu">
127 127 <li id="insert_cell_above"
128 128 title="Insert an empty Code cell above the currently active cell">
129 129 <a href="#">Insert Cell Above</a></li>
130 130 <li id="insert_cell_below"
131 131 title="Insert an empty Code cell below the currently active cell">
132 132 <a href="#">Insert Cell Below</a></li>
133 133 </ul>
134 134 </li>
135 135 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Cell</a>
136 136 <ul id="cell_menu" class="dropdown-menu">
137 137 <li id="run_cell" title="Run this cell, and move cursor to the next one">
138 138 <a href="#">Run</a></li>
139 139 <li id="run_cell_select_below" title="Run this cell, select below">
140 140 <a href="#">Run and Select Below</a></li>
141 141 <li id="run_cell_insert_below" title="Run this cell, insert below">
142 142 <a href="#">Run and Insert Below</a></li>
143 143 <li id="run_all_cells" title="Run all cells in the notebook">
144 144 <a href="#">Run All</a></li>
145 145 <li id="run_all_cells_above" title="Run all cells above (but not including) this cell">
146 146 <a href="#">Run All Above</a></li>
147 147 <li id="run_all_cells_below" title="Run this cell and all cells below it">
148 148 <a href="#">Run All Below</a></li>
149 149 <li class="divider"></li>
150 150 <li id="change_cell_type" class="dropdown-submenu"
151 151 title="All cells in the notebook have a cell type. By default, new cells are created as 'Code' cells">
152 152 <a href="#">Cell Type</a>
153 153 <ul class="dropdown-menu">
154 154 <li id="to_code"
155 155 title="Contents will be sent to the kernel for execution, and output will display in the footer of cell">
156 156 <a href="#">Code</a></li>
157 157 <li id="to_markdown"
158 158 title="Contents will be rendered as HTML and serve as explanatory text">
159 159 <a href="#">Markdown</a></li>
160 160 <li id="to_raw"
161 161 title="Contents will pass through nbconvert unmodified">
162 162 <a href="#">Raw NBConvert</a></li>
163 163 <li id="to_heading1"><a href="#">Heading 1</a></li>
164 164 <li id="to_heading2"><a href="#">Heading 2</a></li>
165 165 <li id="to_heading3"><a href="#">Heading 3</a></li>
166 166 <li id="to_heading4"><a href="#">Heading 4</a></li>
167 167 <li id="to_heading5"><a href="#">Heading 5</a></li>
168 168 <li id="to_heading6"><a href="#">Heading 6</a></li>
169 169 </ul>
170 170 </li>
171 171 <li class="divider"></li>
172 172 <li id="current_outputs" class="dropdown-submenu"><a href="#">Current Output</a>
173 173 <ul class="dropdown-menu">
174 174 <li id="toggle_current_output"
175 175 title="Hide/Show the output of the current cell">
176 176 <a href="#">Toggle</a>
177 177 </li>
178 178 <li id="toggle_current_output_scroll"
179 179 title="Scroll the output of the current cell">
180 180 <a href="#">Toggle Scrolling</a>
181 181 </li>
182 182 <li id="clear_current_output"
183 183 title="Clear the output of the current cell">
184 184 <a href="#">Clear</a>
185 185 </li>
186 186 </ul>
187 187 </li>
188 188 <li id="all_outputs" class="dropdown-submenu"><a href="#">All Output</a>
189 189 <ul class="dropdown-menu">
190 190 <li id="toggle_all_output"
191 191 title="Hide/Show the output of all cells">
192 192 <a href="#">Toggle</a>
193 193 </li>
194 194 <li id="toggle_all_output_scroll"
195 195 title="Scroll the output of all cells">
196 196 <a href="#">Toggle Scrolling</a>
197 197 </li>
198 198 <li id="clear_all_output"
199 199 title="Clear the output of all cells">
200 200 <a href="#">Clear</a>
201 201 </li>
202 202 </ul>
203 203 </li>
204 204 </ul>
205 205 </li>
206 206 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Kernel</a>
207 207 <ul id="kernel_menu" class="dropdown-menu">
208 208 <li id="int_kernel"
209 209 title="Send KeyboardInterrupt (CTRL-C) to the Kernel">
210 210 <a href="#">Interrupt</a></li>
211 211 <li id="restart_kernel"
212 212 title="Restart the Kernel">
213 213 <a href="#">Restart</a></li>
214 214 </ul>
215 215 </li>
216 216 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
217 217 <ul id="help_menu" class="dropdown-menu">
218 218 <li id="keyboard_shortcuts" title="Opens a tooltip with all keyboard shortcuts"><a href="#">Keyboard Shortcuts</a></li>
219 219 <li class="divider"></li>
220 220 {% set
221 221 sections = (
222 222 (
223 223 ("http://ipython.org/documentation.html","IPython Help",True),
224 224 ("http://nbviewer.ipython.org/github/ipython/ipython/tree/master/examples/notebooks/", "Notebook Examples", True),
225 225 ("http://ipython.org/ipython-doc/stable/interactive/notebook.html","Notebook Help",True),
226 226 ("http://ipython.org/ipython-doc/dev/interactive/cm_keyboard.html","Editor Shortcuts",True),
227 227 ),(
228 228 ("http://docs.python.org","Python",True),
229 229 ("http://docs.scipy.org/doc/numpy/reference/","NumPy",True),
230 230 ("http://docs.scipy.org/doc/scipy/reference/","SciPy",True),
231 231 ("http://matplotlib.org/contents.html","Matplotlib",True),
232 232 ("http://docs.sympy.org/dev/index.html","SymPy",True),
233 233 ("http://pandas.pydata.org/pandas-docs/stable/","pandas", True)
234 234 )
235 235 )
236 236 %}
237 237
238 238 {% for helplinks in sections %}
239 239 {% for link in helplinks %}
240 240 <li><a href="{{link[0]}}" {{'target="_blank" title="Opens in a new window"' if link[2]}}>
241 241 {{'<i class="icon-external-link menu-icon pull-right"></i>' if link[2]}}
242 242 {{link[1]}}
243 243 </a></li>
244 244 {% endfor %}
245 245 {% if not loop.last %}
246 246 <li class="divider"></li>
247 247 {% endif %}
248 248 {% endfor %}
249 249 </li>
250 250 </ul>
251 251 </li>
252 252 </ul>
253 253 <div id="kernel_indicator" class="indicator_area pull-right">
254 254 <i id="kernel_indicator_icon"></i>
255 255 </div>
256 256 <div id="modal_indicator" class="indicator_area pull-right">
257 257 <i id="modal_indicator_icon"></i>
258 258 </div>
259 259 <div id="notification_area"></div>
260 260 </div>
261 261 </div>
262 262 </div>
263 263 </div>
264 264 <div id="maintoolbar" class="navbar">
265 265 <div class="toolbar-inner navbar-inner navbar-nobg">
266 266 <div id="maintoolbar-container" class="container"></div>
267 267 </div>
268 268 </div>
269 269 </div>
270 270
271 271 <div id="ipython-main-app">
272 272
273 273 <div id="notebook_panel">
274 274 <div id="notebook"></div>
275 275 <div id="pager_splitter"></div>
276 276 <div id="pager">
277 277 <div id='pager_button_area'>
278 278 </div>
279 279 <div id="pager-container" class="container"></div>
280 280 </div>
281 281 </div>
282 282
283 283 </div>
284 284 <div id='tooltip' class='ipython_tooltip' style='display:none'></div>
285 285
286 286
287 287 {% endblock %}
288 288
289 289
290 290 {% block script %}
291 291
292 292 {{super()}}
293 293
294 294 <script src="{{ static_url("components/codemirror/lib/codemirror.js") }}" charset="utf-8"></script>
295 295 <script type="text/javascript">
296 296 CodeMirror.modeURL = "{{ static_url("components/codemirror/mode/%N/%N.js", include_version=False) }}";
297 297 </script>
298 298 <script src="{{ static_url("components/codemirror/addon/mode/loadmode.js") }}" charset="utf-8"></script>
299 299 <script src="{{ static_url("components/codemirror/addon/mode/multiplex.js") }}" charset="utf-8"></script>
300 300 <script src="{{ static_url("components/codemirror/addon/mode/overlay.js") }}" charset="utf-8"></script>
301 301 <script src="{{ static_url("components/codemirror/addon/edit/matchbrackets.js") }}" charset="utf-8"></script>
302 302 <script src="{{ static_url("components/codemirror/addon/edit/closebrackets.js") }}" charset="utf-8"></script>
303 303 <script src="{{ static_url("components/codemirror/addon/comment/comment.js") }}" charset="utf-8"></script>
304 304 <script src="{{ static_url("components/codemirror/mode/htmlmixed/htmlmixed.js") }}" charset="utf-8"></script>
305 305 <script src="{{ static_url("components/codemirror/mode/xml/xml.js") }}" charset="utf-8"></script>
306 306 <script src="{{ static_url("components/codemirror/mode/javascript/javascript.js") }}" charset="utf-8"></script>
307 307 <script src="{{ static_url("components/codemirror/mode/css/css.js") }}" charset="utf-8"></script>
308 308 <script src="{{ static_url("components/codemirror/mode/rst/rst.js") }}" charset="utf-8"></script>
309 309 <script src="{{ static_url("components/codemirror/mode/markdown/markdown.js") }}" charset="utf-8"></script>
310 310 <script src="{{ static_url("components/codemirror/mode/gfm/gfm.js") }}" charset="utf-8"></script>
311 311 <script src="{{ static_url("components/codemirror/mode/python/python.js") }}" charset="utf-8"></script>
312 312 <script src="{{ static_url("notebook/js/codemirror-ipython.js") }}" charset="utf-8"></script>
313 313
314 314 <script src="{{ static_url("components/highlight.js/build/highlight.pack.js") }}" charset="utf-8"></script>
315 315
316 316 <script src="{{ static_url("dateformat/date.format.js") }}" charset="utf-8"></script>
317 317
318 318 <script src="{{ static_url("base/js/events.js") }}" type="text/javascript" charset="utf-8"></script>
319 319 <script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
320 <script src="{{ static_url("base/js/keyboard.js") }}" type="text/javascript" charset="utf-8"></script>
320 321 <script src="{{ static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
321 322 <script src="{{ static_url("services/kernels/js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
322 323 <script src="{{ static_url("services/kernels/js/comm.js") }}" type="text/javascript" charset="utf-8"></script>
323 324 <script src="{{ static_url("services/sessions/js/session.js") }}" type="text/javascript" charset="utf-8"></script>
324 325 <script src="{{ static_url("notebook/js/layoutmanager.js") }}" type="text/javascript" charset="utf-8"></script>
325 326 <script src="{{ static_url("notebook/js/mathjaxutils.js") }}" type="text/javascript" charset="utf-8"></script>
326 327 <script src="{{ static_url("notebook/js/outputarea.js") }}" type="text/javascript" charset="utf-8"></script>
327 328 <script src="{{ static_url("notebook/js/cell.js") }}" type="text/javascript" charset="utf-8"></script>
328 329 <script src="{{ static_url("notebook/js/celltoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
329 330 <script src="{{ static_url("notebook/js/codecell.js") }}" type="text/javascript" charset="utf-8"></script>
330 331 <script src="{{ static_url("notebook/js/completer.js") }}" type="text/javascript" charset="utf-8"></script>
331 332 <script src="{{ static_url("notebook/js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
332 333 <script src="{{ static_url("notebook/js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
333 334 <script src="{{ static_url("notebook/js/quickhelp.js") }}" type="text/javascript" charset="utf-8"></script>
334 335 <script src="{{ static_url("notebook/js/pager.js") }}" type="text/javascript" charset="utf-8"></script>
335 336 <script src="{{ static_url("notebook/js/menubar.js") }}" type="text/javascript" charset="utf-8"></script>
336 337 <script src="{{ static_url("notebook/js/toolbar.js") }}" type="text/javascript" charset="utf-8"></script>
337 338 <script src="{{ static_url("notebook/js/maintoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
338 339 <script src="{{ static_url("notebook/js/notebook.js") }}" type="text/javascript" charset="utf-8"></script>
339 340 <script src="{{ static_url("notebook/js/keyboardmanager.js") }}" type="text/javascript" charset="utf-8"></script>
340 341 <script src="{{ static_url("notebook/js/notificationwidget.js") }}" type="text/javascript" charset="utf-8"></script>
341 342 <script src="{{ static_url("notebook/js/notificationarea.js") }}" type="text/javascript" charset="utf-8"></script>
342 343 <script src="{{ static_url("notebook/js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
343 344 <script src="{{ static_url("notebook/js/config.js") }}" type="text/javascript" charset="utf-8"></script>
344 345 <script src="{{ static_url("notebook/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
345 346
346 347 <script src="{{ static_url("notebook/js/contexthint.js") }}" charset="utf-8"></script>
347 348
348 349 <script src="{{ static_url("notebook/js/celltoolbarpresets/default.js") }}" type="text/javascript" charset="utf-8"></script>
349 350 <script src="{{ static_url("notebook/js/celltoolbarpresets/rawcell.js") }}" type="text/javascript" charset="utf-8"></script>
350 351 <script src="{{ static_url("notebook/js/celltoolbarpresets/slideshow.js") }}" type="text/javascript" charset="utf-8"></script>
351 352
352 353 {% endblock %}
@@ -1,24 +1,24 b''
1 1 //
2 2 // Check for errors with up and down arrow presses in a non-empty notebook.
3 3 //
4 4 casper.notebook_test(function () {
5 5 var result = this.evaluate(function() {
6 6 IPython.notebook.command_mode();
7 7 pos0 = IPython.notebook.get_selected_index();
8 IPython.utils.press(IPython.keycodes.b)
8 IPython.keyboard.trigger_keydown('b');
9 9 pos1 = IPython.notebook.get_selected_index();
10 IPython.utils.press(IPython.keycodes.b)
10 IPython.keyboard.trigger_keydown('b');
11 11 pos2 = IPython.notebook.get_selected_index();
12 12 // Simulate the "up arrow" and "down arrow" keys.
13 IPython.utils.press_up();
13 IPython.keyboard.trigger_keydown('up');
14 14 pos3 = IPython.notebook.get_selected_index();
15 IPython.utils.press_down();
15 IPython.keyboard.trigger_keydown('down');
16 16 pos4 = IPython.notebook.get_selected_index();
17 17 return pos0 == 0 &&
18 18 pos1 == 1 &&
19 19 pos2 == 2 &&
20 20 pos3 == 1 &&
21 21 pos4 == 2;
22 22 });
23 23 this.test.assertTrue(result, 'Up/down arrow okay in non-empty notebook.');
24 24 });
@@ -1,21 +1,21 b''
1 1 //
2 2 // Check for errors with up and down arrow presses in an empty notebook.
3 3 //
4 4 casper.notebook_test(function () {
5 5 var result = this.evaluate(function() {
6 6 var ncells = IPython.notebook.ncells();
7 7 var i;
8 8
9 9 // Delete all cells.
10 10 for (i = 0; i < ncells; i++) {
11 11 IPython.notebook.delete_cell();
12 12 }
13 13
14 14 // Simulate the "up arrow" and "down arrow" keys.
15 15 //
16 IPython.utils.press_up();
17 IPython.utils.press_down();
16 IPython.keyboard.trigger_keydown('up');
17 IPython.keyboard.trigger_keydown('down');
18 18 return true;
19 19 });
20 20 this.test.assertTrue(result, 'Up/down arrow okay in empty notebook.');
21 21 });
@@ -1,71 +1,71 b''
1 1 //
2 2 // Test code cell execution.
3 3 //
4 4 casper.notebook_test(function () {
5 5 this.evaluate(function () {
6 6 var cell = IPython.notebook.get_cell(0);
7 7 cell.set_text('a=10; print(a)');
8 8 cell.execute();
9 9 });
10 10
11 11 this.wait_for_output(0);
12 12
13 13 // refactor this into just a get_output(0)
14 14 this.then(function () {
15 15 var result = this.get_output_cell(0);
16 16 this.test.assertEquals(result.text, '10\n', 'cell execute (using js)');
17 17 });
18 18
19 19
20 20 // do it again with the keyboard shortcut
21 21 this.thenEvaluate(function () {
22 22 var cell = IPython.notebook.get_cell(0);
23 23 cell.set_text('a=11; print(a)');
24 24 cell.clear_output();
25 IPython.utils.press_shift_enter();
25 IPython.keyboard.trigger_keydown('shift+enter');
26 26 });
27 27
28 28 this.wait_for_output(0);
29 29
30 30 this.then(function () {
31 31 var result = this.get_output_cell(0);
32 32 var num_cells = this.get_cells_length();
33 33 this.test.assertEquals(result.text, '11\n', 'cell execute (using ctrl-enter)');
34 34 this.test.assertEquals(num_cells, 2, 'shift-enter adds a new cell at the bottom')
35 35 });
36 36
37 37 // do it again with the keyboard shortcut
38 38 this.thenEvaluate(function () {
39 39 IPython.notebook.select(1);
40 40 IPython.notebook.delete_cell();
41 41 var cell = IPython.notebook.get_cell(0);
42 42 cell.set_text('a=12; print(a)');
43 43 cell.clear_output();
44 IPython.utils.press_ctrl_enter();
44 IPython.keyboard.trigger_keydown('ctrl+enter');
45 45 });
46 46
47 47 this.wait_for_output(0);
48 48
49 49 this.then(function () {
50 50 var result = this.get_output_cell(0);
51 51 var num_cells = this.get_cells_length();
52 52 this.test.assertEquals(result.text, '12\n', 'cell execute (using shift-enter)');
53 53 this.test.assertEquals(num_cells, 1, 'ctrl-enter adds no new cell at the bottom')
54 54 });
55 55
56 56 // press the "play" triangle button in the toolbar
57 57 this.thenEvaluate(function () {
58 58 var cell = IPython.notebook.get_cell(0);
59 59 IPython.notebook.select(0);
60 60 cell.clear_output();
61 61 cell.set_text('a=13; print(a)');
62 62 $('#run_b').click();
63 63 });
64 64
65 65 this.wait_for_output(0);
66 66
67 67 this.then(function () {
68 68 var result = this.get_output_cell(0);
69 69 this.test.assertEquals(result.text, '13\n', 'cell execute (using "play" toolbar button)')
70 70 });
71 71 });
@@ -1,44 +1,44 b''
1 1 //
2 2 // Test kernel interrupt
3 3 //
4 4 casper.notebook_test(function () {
5 5 this.evaluate(function () {
6 6 var cell = IPython.notebook.get_cell(0);
7 7 cell.set_text(
8 8 'import time'+
9 9 '\nfor x in range(3):'+
10 10 '\n time.sleep(1)'
11 11 );
12 12 cell.execute();
13 13 });
14 14
15 15 this.wait_for_busy();
16 16
17 17 // interrupt using menu item (Kernel -> Interrupt)
18 18 this.thenClick('li#int_kernel');
19 19
20 20 this.wait_for_output(0);
21 21
22 22 this.then(function () {
23 23 var result = this.get_output_cell(0);
24 24 this.test.assertEquals(result.ename, 'KeyboardInterrupt', 'keyboard interrupt (mouseclick)');
25 25 });
26 26
27 27 // run cell 0 again, now interrupting using keyboard shortcut
28 28 this.thenEvaluate(function () {
29 29 cell.clear_output();
30 30 cell.execute();
31 31 });
32 32
33 33 // interrupt using Ctrl-M I keyboard shortcut
34 34 this.thenEvaluate( function() {
35 IPython.utils.press_ghetto(IPython.utils.keycodes.I)
35 IPython.keyboard.trigger_keydown('i');
36 36 });
37 37
38 38 this.wait_for_output(0);
39 39
40 40 this.then(function () {
41 41 var result = this.get_output_cell(0);
42 42 this.test.assertEquals(result.ename, 'KeyboardInterrupt', 'keyboard interrupt (shortcut)');
43 43 });
44 44 });
@@ -1,38 +1,38 b''
1 1 //
2 2 // Test merging two notebook cells.
3 3 //
4 4 casper.notebook_test(function() {
5 5 var output = this.evaluate(function () {
6 6 // Fill in test data.
7 7 IPython.notebook.command_mode();
8 8 var set_cell_text = function () {
9 9 var cell_one = IPython.notebook.get_selected_cell();
10 10 cell_one.set_text('a = 5');
11 11
12 IPython.utils.press(IPython.keycodes.b)
12 IPython.keyboard.trigger_keydown('b');
13 13 var cell_two = IPython.notebook.get_selected_cell();
14 14 cell_two.set_text('print(a)');
15 15 };
16 16
17 17 // merge_cell_above()
18 18 set_cell_text();
19 19 IPython.notebook.merge_cell_above();
20 20 var merged_above = IPython.notebook.get_selected_cell();
21 21
22 22 // merge_cell_below()
23 23 set_cell_text();
24 24 IPython.notebook.select(0);
25 25 IPython.notebook.merge_cell_below();
26 26 var merged_below = IPython.notebook.get_selected_cell();
27 27
28 28 return {
29 29 above: merged_above.get_text(),
30 30 below: merged_below.get_text()
31 31 };
32 32 });
33 33
34 34 this.test.assertEquals(output.above, 'a = 5\nprint(a)',
35 35 'Successful merge_cell_above().');
36 36 this.test.assertEquals(output.below, 'a = 5\nprint(a)',
37 37 'Successful merge_cell_below().');
38 38 });
General Comments 0
You need to be logged in to leave comments. Login now