##// END OF EJS Templates
Merge pull request #7186 from minrk/moment...
Matthias Bussonnier -
r19549:96038745 merge
parent child Browse files
Show More
@@ -1,829 +1,866 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 'base/js/namespace',
5 'base/js/namespace',
6 'jquery',
6 'jquery',
7 'codemirror/lib/codemirror',
7 'codemirror/lib/codemirror',
8 'moment',
8 // silently upgrades CodeMirror
9 // silently upgrades CodeMirror
9 'codemirror/mode/meta',
10 'codemirror/mode/meta',
10 ], function(IPython, $, CodeMirror){
11 ], function(IPython, $, CodeMirror, moment){
11 "use strict";
12 "use strict";
12
13
13 IPython.load_extensions = function () {
14 IPython.load_extensions = function () {
14 // load one or more IPython notebook extensions with requirejs
15 // load one or more IPython notebook extensions with requirejs
15
16
16 var extensions = [];
17 var extensions = [];
17 var extension_names = arguments;
18 var extension_names = arguments;
18 for (var i = 0; i < extension_names.length; i++) {
19 for (var i = 0; i < extension_names.length; i++) {
19 extensions.push("nbextensions/" + arguments[i]);
20 extensions.push("nbextensions/" + arguments[i]);
20 }
21 }
21
22
22 require(extensions,
23 require(extensions,
23 function () {
24 function () {
24 for (var i = 0; i < arguments.length; i++) {
25 for (var i = 0; i < arguments.length; i++) {
25 var ext = arguments[i];
26 var ext = arguments[i];
26 var ext_name = extension_names[i];
27 var ext_name = extension_names[i];
27 // success callback
28 // success callback
28 console.log("Loaded extension: " + ext_name);
29 console.log("Loaded extension: " + ext_name);
29 if (ext && ext.load_ipython_extension !== undefined) {
30 if (ext && ext.load_ipython_extension !== undefined) {
30 ext.load_ipython_extension();
31 ext.load_ipython_extension();
31 }
32 }
32 }
33 }
33 },
34 },
34 function (err) {
35 function (err) {
35 // failure callback
36 // failure callback
36 console.log("Failed to load extension(s):", err.requireModules, err);
37 console.log("Failed to load extension(s):", err.requireModules, err);
37 }
38 }
38 );
39 );
39 };
40 };
40
41
41 //============================================================================
42 //============================================================================
42 // Cross-browser RegEx Split
43 // Cross-browser RegEx Split
43 //============================================================================
44 //============================================================================
44
45
45 // This code has been MODIFIED from the code licensed below to not replace the
46 // This code has been MODIFIED from the code licensed below to not replace the
46 // default browser split. The license is reproduced here.
47 // default browser split. The license is reproduced here.
47
48
48 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
49 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
49 /*!
50 /*!
50 * Cross-Browser Split 1.1.1
51 * Cross-Browser Split 1.1.1
51 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
52 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
52 * Available under the MIT License
53 * Available under the MIT License
53 * ECMAScript compliant, uniform cross-browser split method
54 * ECMAScript compliant, uniform cross-browser split method
54 */
55 */
55
56
56 /**
57 /**
57 * Splits a string into an array of strings using a regex or string
58 * Splits a string into an array of strings using a regex or string
58 * separator. Matches of the separator are not included in the result array.
59 * separator. Matches of the separator are not included in the result array.
59 * However, if `separator` is a regex that contains capturing groups,
60 * However, if `separator` is a regex that contains capturing groups,
60 * backreferences are spliced into the result each time `separator` is
61 * backreferences are spliced into the result each time `separator` is
61 * matched. Fixes browser bugs compared to the native
62 * matched. Fixes browser bugs compared to the native
62 * `String.prototype.split` and can be used reliably cross-browser.
63 * `String.prototype.split` and can be used reliably cross-browser.
63 * @param {String} str String to split.
64 * @param {String} str String to split.
64 * @param {RegExp} separator Regex to use for separating
65 * @param {RegExp} separator Regex to use for separating
65 * the string.
66 * the string.
66 * @param {Number} [limit] Maximum number of items to include in the result
67 * @param {Number} [limit] Maximum number of items to include in the result
67 * array.
68 * array.
68 * @returns {Array} Array of substrings.
69 * @returns {Array} Array of substrings.
69 * @example
70 * @example
70 *
71 *
71 * // Basic use
72 * // Basic use
72 * regex_split('a b c d', ' ');
73 * regex_split('a b c d', ' ');
73 * // -> ['a', 'b', 'c', 'd']
74 * // -> ['a', 'b', 'c', 'd']
74 *
75 *
75 * // With limit
76 * // With limit
76 * regex_split('a b c d', ' ', 2);
77 * regex_split('a b c d', ' ', 2);
77 * // -> ['a', 'b']
78 * // -> ['a', 'b']
78 *
79 *
79 * // Backreferences in result array
80 * // Backreferences in result array
80 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
81 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
81 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
82 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
82 */
83 */
83 var regex_split = function (str, separator, limit) {
84 var regex_split = function (str, separator, limit) {
84 var output = [],
85 var output = [],
85 flags = (separator.ignoreCase ? "i" : "") +
86 flags = (separator.ignoreCase ? "i" : "") +
86 (separator.multiline ? "m" : "") +
87 (separator.multiline ? "m" : "") +
87 (separator.extended ? "x" : "") + // Proposed for ES6
88 (separator.extended ? "x" : "") + // Proposed for ES6
88 (separator.sticky ? "y" : ""), // Firefox 3+
89 (separator.sticky ? "y" : ""), // Firefox 3+
89 lastLastIndex = 0,
90 lastLastIndex = 0,
90 separator2, match, lastIndex, lastLength;
91 separator2, match, lastIndex, lastLength;
91 // Make `global` and avoid `lastIndex` issues by working with a copy
92 // Make `global` and avoid `lastIndex` issues by working with a copy
92 separator = new RegExp(separator.source, flags + "g");
93 separator = new RegExp(separator.source, flags + "g");
93
94
94 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
95 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
95 if (!compliantExecNpcg) {
96 if (!compliantExecNpcg) {
96 // Doesn't need flags gy, but they don't hurt
97 // Doesn't need flags gy, but they don't hurt
97 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
98 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
98 }
99 }
99 /* Values for `limit`, per the spec:
100 /* Values for `limit`, per the spec:
100 * If undefined: 4294967295 // Math.pow(2, 32) - 1
101 * If undefined: 4294967295 // Math.pow(2, 32) - 1
101 * If 0, Infinity, or NaN: 0
102 * If 0, Infinity, or NaN: 0
102 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
103 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
103 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
104 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
104 * If other: Type-convert, then use the above rules
105 * If other: Type-convert, then use the above rules
105 */
106 */
106 limit = typeof(limit) === "undefined" ?
107 limit = typeof(limit) === "undefined" ?
107 -1 >>> 0 : // Math.pow(2, 32) - 1
108 -1 >>> 0 : // Math.pow(2, 32) - 1
108 limit >>> 0; // ToUint32(limit)
109 limit >>> 0; // ToUint32(limit)
109 for (match = separator.exec(str); match; match = separator.exec(str)) {
110 for (match = separator.exec(str); match; match = separator.exec(str)) {
110 // `separator.lastIndex` is not reliable cross-browser
111 // `separator.lastIndex` is not reliable cross-browser
111 lastIndex = match.index + match[0].length;
112 lastIndex = match.index + match[0].length;
112 if (lastIndex > lastLastIndex) {
113 if (lastIndex > lastLastIndex) {
113 output.push(str.slice(lastLastIndex, match.index));
114 output.push(str.slice(lastLastIndex, match.index));
114 // Fix browsers whose `exec` methods don't consistently return `undefined` for
115 // Fix browsers whose `exec` methods don't consistently return `undefined` for
115 // nonparticipating capturing groups
116 // nonparticipating capturing groups
116 if (!compliantExecNpcg && match.length > 1) {
117 if (!compliantExecNpcg && match.length > 1) {
117 match[0].replace(separator2, function () {
118 match[0].replace(separator2, function () {
118 for (var i = 1; i < arguments.length - 2; i++) {
119 for (var i = 1; i < arguments.length - 2; i++) {
119 if (typeof(arguments[i]) === "undefined") {
120 if (typeof(arguments[i]) === "undefined") {
120 match[i] = undefined;
121 match[i] = undefined;
121 }
122 }
122 }
123 }
123 });
124 });
124 }
125 }
125 if (match.length > 1 && match.index < str.length) {
126 if (match.length > 1 && match.index < str.length) {
126 Array.prototype.push.apply(output, match.slice(1));
127 Array.prototype.push.apply(output, match.slice(1));
127 }
128 }
128 lastLength = match[0].length;
129 lastLength = match[0].length;
129 lastLastIndex = lastIndex;
130 lastLastIndex = lastIndex;
130 if (output.length >= limit) {
131 if (output.length >= limit) {
131 break;
132 break;
132 }
133 }
133 }
134 }
134 if (separator.lastIndex === match.index) {
135 if (separator.lastIndex === match.index) {
135 separator.lastIndex++; // Avoid an infinite loop
136 separator.lastIndex++; // Avoid an infinite loop
136 }
137 }
137 }
138 }
138 if (lastLastIndex === str.length) {
139 if (lastLastIndex === str.length) {
139 if (lastLength || !separator.test("")) {
140 if (lastLength || !separator.test("")) {
140 output.push("");
141 output.push("");
141 }
142 }
142 } else {
143 } else {
143 output.push(str.slice(lastLastIndex));
144 output.push(str.slice(lastLastIndex));
144 }
145 }
145 return output.length > limit ? output.slice(0, limit) : output;
146 return output.length > limit ? output.slice(0, limit) : output;
146 };
147 };
147
148
148 //============================================================================
149 //============================================================================
149 // End contributed Cross-browser RegEx Split
150 // End contributed Cross-browser RegEx Split
150 //============================================================================
151 //============================================================================
151
152
152
153
153 var uuid = function () {
154 var uuid = function () {
154 /**
155 /**
155 * http://www.ietf.org/rfc/rfc4122.txt
156 * http://www.ietf.org/rfc/rfc4122.txt
156 */
157 */
157 var s = [];
158 var s = [];
158 var hexDigits = "0123456789ABCDEF";
159 var hexDigits = "0123456789ABCDEF";
159 for (var i = 0; i < 32; i++) {
160 for (var i = 0; i < 32; i++) {
160 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
161 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
161 }
162 }
162 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
163 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
163 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
164 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
164
165
165 var uuid = s.join("");
166 var uuid = s.join("");
166 return uuid;
167 return uuid;
167 };
168 };
168
169
169
170
170 //Fix raw text to parse correctly in crazy XML
171 //Fix raw text to parse correctly in crazy XML
171 function xmlencode(string) {
172 function xmlencode(string) {
172 return string.replace(/\&/g,'&'+'amp;')
173 return string.replace(/\&/g,'&'+'amp;')
173 .replace(/</g,'&'+'lt;')
174 .replace(/</g,'&'+'lt;')
174 .replace(/>/g,'&'+'gt;')
175 .replace(/>/g,'&'+'gt;')
175 .replace(/\'/g,'&'+'apos;')
176 .replace(/\'/g,'&'+'apos;')
176 .replace(/\"/g,'&'+'quot;')
177 .replace(/\"/g,'&'+'quot;')
177 .replace(/`/g,'&'+'#96;');
178 .replace(/`/g,'&'+'#96;');
178 }
179 }
179
180
180
181
181 //Map from terminal commands to CSS classes
182 //Map from terminal commands to CSS classes
182 var ansi_colormap = {
183 var ansi_colormap = {
183 "01":"ansibold",
184 "01":"ansibold",
184
185
185 "30":"ansiblack",
186 "30":"ansiblack",
186 "31":"ansired",
187 "31":"ansired",
187 "32":"ansigreen",
188 "32":"ansigreen",
188 "33":"ansiyellow",
189 "33":"ansiyellow",
189 "34":"ansiblue",
190 "34":"ansiblue",
190 "35":"ansipurple",
191 "35":"ansipurple",
191 "36":"ansicyan",
192 "36":"ansicyan",
192 "37":"ansigray",
193 "37":"ansigray",
193
194
194 "40":"ansibgblack",
195 "40":"ansibgblack",
195 "41":"ansibgred",
196 "41":"ansibgred",
196 "42":"ansibggreen",
197 "42":"ansibggreen",
197 "43":"ansibgyellow",
198 "43":"ansibgyellow",
198 "44":"ansibgblue",
199 "44":"ansibgblue",
199 "45":"ansibgpurple",
200 "45":"ansibgpurple",
200 "46":"ansibgcyan",
201 "46":"ansibgcyan",
201 "47":"ansibggray"
202 "47":"ansibggray"
202 };
203 };
203
204
204 function _process_numbers(attrs, numbers) {
205 function _process_numbers(attrs, numbers) {
205 // process ansi escapes
206 // process ansi escapes
206 var n = numbers.shift();
207 var n = numbers.shift();
207 if (ansi_colormap[n]) {
208 if (ansi_colormap[n]) {
208 if ( ! attrs["class"] ) {
209 if ( ! attrs["class"] ) {
209 attrs["class"] = ansi_colormap[n];
210 attrs["class"] = ansi_colormap[n];
210 } else {
211 } else {
211 attrs["class"] += " " + ansi_colormap[n];
212 attrs["class"] += " " + ansi_colormap[n];
212 }
213 }
213 } else if (n == "38" || n == "48") {
214 } else if (n == "38" || n == "48") {
214 // VT100 256 color or 24 bit RGB
215 // VT100 256 color or 24 bit RGB
215 if (numbers.length < 2) {
216 if (numbers.length < 2) {
216 console.log("Not enough fields for VT100 color", numbers);
217 console.log("Not enough fields for VT100 color", numbers);
217 return;
218 return;
218 }
219 }
219
220
220 var index_or_rgb = numbers.shift();
221 var index_or_rgb = numbers.shift();
221 var r,g,b;
222 var r,g,b;
222 if (index_or_rgb == "5") {
223 if (index_or_rgb == "5") {
223 // 256 color
224 // 256 color
224 var idx = parseInt(numbers.shift(), 10);
225 var idx = parseInt(numbers.shift(), 10);
225 if (idx < 16) {
226 if (idx < 16) {
226 // indexed ANSI
227 // indexed ANSI
227 // ignore bright / non-bright distinction
228 // ignore bright / non-bright distinction
228 idx = idx % 8;
229 idx = idx % 8;
229 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
230 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
230 if ( ! attrs["class"] ) {
231 if ( ! attrs["class"] ) {
231 attrs["class"] = ansiclass;
232 attrs["class"] = ansiclass;
232 } else {
233 } else {
233 attrs["class"] += " " + ansiclass;
234 attrs["class"] += " " + ansiclass;
234 }
235 }
235 return;
236 return;
236 } else if (idx < 232) {
237 } else if (idx < 232) {
237 // 216 color 6x6x6 RGB
238 // 216 color 6x6x6 RGB
238 idx = idx - 16;
239 idx = idx - 16;
239 b = idx % 6;
240 b = idx % 6;
240 g = Math.floor(idx / 6) % 6;
241 g = Math.floor(idx / 6) % 6;
241 r = Math.floor(idx / 36) % 6;
242 r = Math.floor(idx / 36) % 6;
242 // convert to rgb
243 // convert to rgb
243 r = (r * 51);
244 r = (r * 51);
244 g = (g * 51);
245 g = (g * 51);
245 b = (b * 51);
246 b = (b * 51);
246 } else {
247 } else {
247 // grayscale
248 // grayscale
248 idx = idx - 231;
249 idx = idx - 231;
249 // it's 1-24 and should *not* include black or white,
250 // it's 1-24 and should *not* include black or white,
250 // so a 26 point scale
251 // so a 26 point scale
251 r = g = b = Math.floor(idx * 256 / 26);
252 r = g = b = Math.floor(idx * 256 / 26);
252 }
253 }
253 } else if (index_or_rgb == "2") {
254 } else if (index_or_rgb == "2") {
254 // Simple 24 bit RGB
255 // Simple 24 bit RGB
255 if (numbers.length > 3) {
256 if (numbers.length > 3) {
256 console.log("Not enough fields for RGB", numbers);
257 console.log("Not enough fields for RGB", numbers);
257 return;
258 return;
258 }
259 }
259 r = numbers.shift();
260 r = numbers.shift();
260 g = numbers.shift();
261 g = numbers.shift();
261 b = numbers.shift();
262 b = numbers.shift();
262 } else {
263 } else {
263 console.log("unrecognized control", numbers);
264 console.log("unrecognized control", numbers);
264 return;
265 return;
265 }
266 }
266 if (r !== undefined) {
267 if (r !== undefined) {
267 // apply the rgb color
268 // apply the rgb color
268 var line;
269 var line;
269 if (n == "38") {
270 if (n == "38") {
270 line = "color: ";
271 line = "color: ";
271 } else {
272 } else {
272 line = "background-color: ";
273 line = "background-color: ";
273 }
274 }
274 line = line + "rgb(" + r + "," + g + "," + b + ");";
275 line = line + "rgb(" + r + "," + g + "," + b + ");";
275 if ( !attrs.style ) {
276 if ( !attrs.style ) {
276 attrs.style = line;
277 attrs.style = line;
277 } else {
278 } else {
278 attrs.style += " " + line;
279 attrs.style += " " + line;
279 }
280 }
280 }
281 }
281 }
282 }
282 }
283 }
283
284
284 function ansispan(str) {
285 function ansispan(str) {
285 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
286 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
286 // regular ansi escapes (using the table above)
287 // regular ansi escapes (using the table above)
287 var is_open = false;
288 var is_open = false;
288 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
289 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
289 if (!pattern) {
290 if (!pattern) {
290 // [(01|22|39|)m close spans
291 // [(01|22|39|)m close spans
291 if (is_open) {
292 if (is_open) {
292 is_open = false;
293 is_open = false;
293 return "</span>";
294 return "</span>";
294 } else {
295 } else {
295 return "";
296 return "";
296 }
297 }
297 } else {
298 } else {
298 is_open = true;
299 is_open = true;
299
300
300 // consume sequence of color escapes
301 // consume sequence of color escapes
301 var numbers = pattern.match(/\d+/g);
302 var numbers = pattern.match(/\d+/g);
302 var attrs = {};
303 var attrs = {};
303 while (numbers.length > 0) {
304 while (numbers.length > 0) {
304 _process_numbers(attrs, numbers);
305 _process_numbers(attrs, numbers);
305 }
306 }
306
307
307 var span = "<span ";
308 var span = "<span ";
308 Object.keys(attrs).map(function (attr) {
309 Object.keys(attrs).map(function (attr) {
309 span = span + " " + attr + '="' + attrs[attr] + '"';
310 span = span + " " + attr + '="' + attrs[attr] + '"';
310 });
311 });
311 return span + ">";
312 return span + ">";
312 }
313 }
313 });
314 });
314 }
315 }
315
316
316 // Transform ANSI color escape codes into HTML <span> tags with css
317 // Transform ANSI color escape codes into HTML <span> tags with css
317 // classes listed in the above ansi_colormap object. The actual color used
318 // classes listed in the above ansi_colormap object. The actual color used
318 // are set in the css file.
319 // are set in the css file.
319 function fixConsole(txt) {
320 function fixConsole(txt) {
320 txt = xmlencode(txt);
321 txt = xmlencode(txt);
321
322
322 // Strip all ANSI codes that are not color related. Matches
323 // Strip all ANSI codes that are not color related. Matches
323 // all ANSI codes that do not end with "m".
324 // all ANSI codes that do not end with "m".
324 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
325 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
325 txt = txt.replace(ignored_re, "");
326 txt = txt.replace(ignored_re, "");
326
327
327 // color ansi codes
328 // color ansi codes
328 txt = ansispan(txt);
329 txt = ansispan(txt);
329 return txt;
330 return txt;
330 }
331 }
331
332
332 // Remove chunks that should be overridden by the effect of
333 // Remove chunks that should be overridden by the effect of
333 // carriage return characters
334 // carriage return characters
334 function fixCarriageReturn(txt) {
335 function fixCarriageReturn(txt) {
335 var tmp = txt;
336 var tmp = txt;
336 do {
337 do {
337 txt = tmp;
338 txt = tmp;
338 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
339 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
339 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
340 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
340 } while (tmp.length < txt.length);
341 } while (tmp.length < txt.length);
341 return txt;
342 return txt;
342 }
343 }
343
344
344 // Locate any URLs and convert them to a anchor tag
345 // Locate any URLs and convert them to a anchor tag
345 function autoLinkUrls(txt) {
346 function autoLinkUrls(txt) {
346 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
347 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
347 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
348 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
348 }
349 }
349
350
350 var points_to_pixels = function (points) {
351 var points_to_pixels = function (points) {
351 /**
352 /**
352 * A reasonably good way of converting between points and pixels.
353 * A reasonably good way of converting between points and pixels.
353 */
354 */
354 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
355 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
355 $('body').append(test);
356 $('body').append(test);
356 var pixel_per_point = test.width()/10000;
357 var pixel_per_point = test.width()/10000;
357 test.remove();
358 test.remove();
358 return Math.floor(points*pixel_per_point);
359 return Math.floor(points*pixel_per_point);
359 };
360 };
360
361
361 var always_new = function (constructor) {
362 var always_new = function (constructor) {
362 /**
363 /**
363 * wrapper around contructor to avoid requiring `var a = new constructor()`
364 * wrapper around contructor to avoid requiring `var a = new constructor()`
364 * useful for passing constructors as callbacks,
365 * useful for passing constructors as callbacks,
365 * not for programmer laziness.
366 * not for programmer laziness.
366 * from http://programmers.stackexchange.com/questions/118798
367 * from http://programmers.stackexchange.com/questions/118798
367 */
368 */
368 return function () {
369 return function () {
369 var obj = Object.create(constructor.prototype);
370 var obj = Object.create(constructor.prototype);
370 constructor.apply(obj, arguments);
371 constructor.apply(obj, arguments);
371 return obj;
372 return obj;
372 };
373 };
373 };
374 };
374
375
375 var url_path_join = function () {
376 var url_path_join = function () {
376 /**
377 /**
377 * join a sequence of url components with '/'
378 * join a sequence of url components with '/'
378 */
379 */
379 var url = '';
380 var url = '';
380 for (var i = 0; i < arguments.length; i++) {
381 for (var i = 0; i < arguments.length; i++) {
381 if (arguments[i] === '') {
382 if (arguments[i] === '') {
382 continue;
383 continue;
383 }
384 }
384 if (url.length > 0 && url[url.length-1] != '/') {
385 if (url.length > 0 && url[url.length-1] != '/') {
385 url = url + '/' + arguments[i];
386 url = url + '/' + arguments[i];
386 } else {
387 } else {
387 url = url + arguments[i];
388 url = url + arguments[i];
388 }
389 }
389 }
390 }
390 url = url.replace(/\/\/+/, '/');
391 url = url.replace(/\/\/+/, '/');
391 return url;
392 return url;
392 };
393 };
393
394
394 var url_path_split = function (path) {
395 var url_path_split = function (path) {
395 /**
396 /**
396 * Like os.path.split for URLs.
397 * Like os.path.split for URLs.
397 * Always returns two strings, the directory path and the base filename
398 * Always returns two strings, the directory path and the base filename
398 */
399 */
399
400
400 var idx = path.lastIndexOf('/');
401 var idx = path.lastIndexOf('/');
401 if (idx === -1) {
402 if (idx === -1) {
402 return ['', path];
403 return ['', path];
403 } else {
404 } else {
404 return [ path.slice(0, idx), path.slice(idx + 1) ];
405 return [ path.slice(0, idx), path.slice(idx + 1) ];
405 }
406 }
406 };
407 };
407
408
408 var parse_url = function (url) {
409 var parse_url = function (url) {
409 /**
410 /**
410 * an `a` element with an href allows attr-access to the parsed segments of a URL
411 * an `a` element with an href allows attr-access to the parsed segments of a URL
411 * a = parse_url("http://localhost:8888/path/name#hash")
412 * a = parse_url("http://localhost:8888/path/name#hash")
412 * a.protocol = "http:"
413 * a.protocol = "http:"
413 * a.host = "localhost:8888"
414 * a.host = "localhost:8888"
414 * a.hostname = "localhost"
415 * a.hostname = "localhost"
415 * a.port = 8888
416 * a.port = 8888
416 * a.pathname = "/path/name"
417 * a.pathname = "/path/name"
417 * a.hash = "#hash"
418 * a.hash = "#hash"
418 */
419 */
419 var a = document.createElement("a");
420 var a = document.createElement("a");
420 a.href = url;
421 a.href = url;
421 return a;
422 return a;
422 };
423 };
423
424
424 var encode_uri_components = function (uri) {
425 var encode_uri_components = function (uri) {
425 /**
426 /**
426 * encode just the components of a multi-segment uri,
427 * encode just the components of a multi-segment uri,
427 * leaving '/' separators
428 * leaving '/' separators
428 */
429 */
429 return uri.split('/').map(encodeURIComponent).join('/');
430 return uri.split('/').map(encodeURIComponent).join('/');
430 };
431 };
431
432
432 var url_join_encode = function () {
433 var url_join_encode = function () {
433 /**
434 /**
434 * join a sequence of url components with '/',
435 * join a sequence of url components with '/',
435 * encoding each component with encodeURIComponent
436 * encoding each component with encodeURIComponent
436 */
437 */
437 return encode_uri_components(url_path_join.apply(null, arguments));
438 return encode_uri_components(url_path_join.apply(null, arguments));
438 };
439 };
439
440
440
441
441 var splitext = function (filename) {
442 var splitext = function (filename) {
442 /**
443 /**
443 * mimic Python os.path.splitext
444 * mimic Python os.path.splitext
444 * Returns ['base', '.ext']
445 * Returns ['base', '.ext']
445 */
446 */
446 var idx = filename.lastIndexOf('.');
447 var idx = filename.lastIndexOf('.');
447 if (idx > 0) {
448 if (idx > 0) {
448 return [filename.slice(0, idx), filename.slice(idx)];
449 return [filename.slice(0, idx), filename.slice(idx)];
449 } else {
450 } else {
450 return [filename, ''];
451 return [filename, ''];
451 }
452 }
452 };
453 };
453
454
454
455
455 var escape_html = function (text) {
456 var escape_html = function (text) {
456 /**
457 /**
457 * escape text to HTML
458 * escape text to HTML
458 */
459 */
459 return $("<div/>").text(text).html();
460 return $("<div/>").text(text).html();
460 };
461 };
461
462
462
463
463 var get_body_data = function(key) {
464 var get_body_data = function(key) {
464 /**
465 /**
465 * get a url-encoded item from body.data and decode it
466 * get a url-encoded item from body.data and decode it
466 * we should never have any encoded URLs anywhere else in code
467 * we should never have any encoded URLs anywhere else in code
467 * until we are building an actual request
468 * until we are building an actual request
468 */
469 */
469 return decodeURIComponent($('body').data(key));
470 return decodeURIComponent($('body').data(key));
470 };
471 };
471
472
472 var to_absolute_cursor_pos = function (cm, cursor) {
473 var to_absolute_cursor_pos = function (cm, cursor) {
473 /**
474 /**
474 * get the absolute cursor position from CodeMirror's col, ch
475 * get the absolute cursor position from CodeMirror's col, ch
475 */
476 */
476 if (!cursor) {
477 if (!cursor) {
477 cursor = cm.getCursor();
478 cursor = cm.getCursor();
478 }
479 }
479 var cursor_pos = cursor.ch;
480 var cursor_pos = cursor.ch;
480 for (var i = 0; i < cursor.line; i++) {
481 for (var i = 0; i < cursor.line; i++) {
481 cursor_pos += cm.getLine(i).length + 1;
482 cursor_pos += cm.getLine(i).length + 1;
482 }
483 }
483 return cursor_pos;
484 return cursor_pos;
484 };
485 };
485
486
486 var from_absolute_cursor_pos = function (cm, cursor_pos) {
487 var from_absolute_cursor_pos = function (cm, cursor_pos) {
487 /**
488 /**
488 * turn absolute cursor postion into CodeMirror col, ch cursor
489 * turn absolute cursor postion into CodeMirror col, ch cursor
489 */
490 */
490 var i, line;
491 var i, line;
491 var offset = 0;
492 var offset = 0;
492 for (i = 0, line=cm.getLine(i); line !== undefined; i++, line=cm.getLine(i)) {
493 for (i = 0, line=cm.getLine(i); line !== undefined; i++, line=cm.getLine(i)) {
493 if (offset + line.length < cursor_pos) {
494 if (offset + line.length < cursor_pos) {
494 offset += line.length + 1;
495 offset += line.length + 1;
495 } else {
496 } else {
496 return {
497 return {
497 line : i,
498 line : i,
498 ch : cursor_pos - offset,
499 ch : cursor_pos - offset,
499 };
500 };
500 }
501 }
501 }
502 }
502 // reached end, return endpoint
503 // reached end, return endpoint
503 return {
504 return {
504 ch : line.length - 1,
505 ch : line.length - 1,
505 line : i - 1,
506 line : i - 1,
506 };
507 };
507 };
508 };
508
509
509 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
510 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
510 var browser = (function() {
511 var browser = (function() {
511 if (typeof navigator === 'undefined') {
512 if (typeof navigator === 'undefined') {
512 // navigator undefined in node
513 // navigator undefined in node
513 return 'None';
514 return 'None';
514 }
515 }
515 var N= navigator.appName, ua= navigator.userAgent, tem;
516 var N= navigator.appName, ua= navigator.userAgent, tem;
516 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
517 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
517 if (M && (tem= ua.match(/version\/([\.\d]+)/i)) !== null) M[2]= tem[1];
518 if (M && (tem= ua.match(/version\/([\.\d]+)/i)) !== null) M[2]= tem[1];
518 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
519 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
519 return M;
520 return M;
520 })();
521 })();
521
522
522 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
523 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
523 var platform = (function () {
524 var platform = (function () {
524 if (typeof navigator === 'undefined') {
525 if (typeof navigator === 'undefined') {
525 // navigator undefined in node
526 // navigator undefined in node
526 return 'None';
527 return 'None';
527 }
528 }
528 var OSName="None";
529 var OSName="None";
529 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
530 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
530 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
531 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
531 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
532 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
532 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
533 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
533 return OSName;
534 return OSName;
534 })();
535 })();
535
536
536 var get_url_param = function (name) {
537 var get_url_param = function (name) {
537 // get a URL parameter. I cannot believe we actually need this.
538 // get a URL parameter. I cannot believe we actually need this.
538 // Based on http://stackoverflow.com/a/25359264/938949
539 // Based on http://stackoverflow.com/a/25359264/938949
539 var match = new RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
540 var match = new RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
540 if (match){
541 if (match){
541 return decodeURIComponent(match[1] || '');
542 return decodeURIComponent(match[1] || '');
542 }
543 }
543 };
544 };
544
545
545 var is_or_has = function (a, b) {
546 var is_or_has = function (a, b) {
546 /**
547 /**
547 * Is b a child of a or a itself?
548 * Is b a child of a or a itself?
548 */
549 */
549 return a.has(b).length !==0 || a.is(b);
550 return a.has(b).length !==0 || a.is(b);
550 };
551 };
551
552
552 var is_focused = function (e) {
553 var is_focused = function (e) {
553 /**
554 /**
554 * Is element e, or one of its children focused?
555 * Is element e, or one of its children focused?
555 */
556 */
556 e = $(e);
557 e = $(e);
557 var target = $(document.activeElement);
558 var target = $(document.activeElement);
558 if (target.length > 0) {
559 if (target.length > 0) {
559 if (is_or_has(e, target)) {
560 if (is_or_has(e, target)) {
560 return true;
561 return true;
561 } else {
562 } else {
562 return false;
563 return false;
563 }
564 }
564 } else {
565 } else {
565 return false;
566 return false;
566 }
567 }
567 };
568 };
568
569
569 var mergeopt = function(_class, options, overwrite){
570 var mergeopt = function(_class, options, overwrite){
570 options = options || {};
571 options = options || {};
571 overwrite = overwrite || {};
572 overwrite = overwrite || {};
572 return $.extend(true, {}, _class.options_default, options, overwrite);
573 return $.extend(true, {}, _class.options_default, options, overwrite);
573 };
574 };
574
575
575 var ajax_error_msg = function (jqXHR) {
576 var ajax_error_msg = function (jqXHR) {
576 /**
577 /**
577 * Return a JSON error message if there is one,
578 * Return a JSON error message if there is one,
578 * otherwise the basic HTTP status text.
579 * otherwise the basic HTTP status text.
579 */
580 */
580 if (jqXHR.responseJSON && jqXHR.responseJSON.traceback) {
581 if (jqXHR.responseJSON && jqXHR.responseJSON.traceback) {
581 return jqXHR.responseJSON.traceback;
582 return jqXHR.responseJSON.traceback;
582 } else if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
583 } else if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
583 return jqXHR.responseJSON.message;
584 return jqXHR.responseJSON.message;
584 } else {
585 } else {
585 return jqXHR.statusText;
586 return jqXHR.statusText;
586 }
587 }
587 };
588 };
588 var log_ajax_error = function (jqXHR, status, error) {
589 var log_ajax_error = function (jqXHR, status, error) {
589 /**
590 /**
590 * log ajax failures with informative messages
591 * log ajax failures with informative messages
591 */
592 */
592 var msg = "API request failed (" + jqXHR.status + "): ";
593 var msg = "API request failed (" + jqXHR.status + "): ";
593 console.log(jqXHR);
594 console.log(jqXHR);
594 msg += ajax_error_msg(jqXHR);
595 msg += ajax_error_msg(jqXHR);
595 console.log(msg);
596 console.log(msg);
596 };
597 };
597
598
598 var requireCodeMirrorMode = function (mode, callback, errback) {
599 var requireCodeMirrorMode = function (mode, callback, errback) {
599 /**
600 /**
600 * find a predefined mode or detect from CM metadata then
601 * find a predefined mode or detect from CM metadata then
601 * require and callback with the resolveable mode string: mime or
602 * require and callback with the resolveable mode string: mime or
602 * custom name
603 * custom name
603 */
604 */
604
605
605 var modename = (typeof mode == "string") ? mode :
606 var modename = (typeof mode == "string") ? mode :
606 mode.mode || mode.name;
607 mode.mode || mode.name;
607
608
608 // simplest, cheapest check by mode name: mode may also have config
609 // simplest, cheapest check by mode name: mode may also have config
609 if (CodeMirror.modes.hasOwnProperty(modename)) {
610 if (CodeMirror.modes.hasOwnProperty(modename)) {
610 // return the full mode object, if it has a name
611 // return the full mode object, if it has a name
611 callback(mode.name ? mode : modename);
612 callback(mode.name ? mode : modename);
612 return;
613 return;
613 }
614 }
614
615
615 // *somehow* get back a CM.modeInfo-like object that has .mode and
616 // *somehow* get back a CM.modeInfo-like object that has .mode and
616 // .mime
617 // .mime
617 var info = (mode && mode.mode && mode.mime && mode) ||
618 var info = (mode && mode.mode && mode.mime && mode) ||
618 CodeMirror.findModeByName(modename) ||
619 CodeMirror.findModeByName(modename) ||
619 CodeMirror.findModeByExtension(modename.split(".").slice(-1)) ||
620 CodeMirror.findModeByExtension(modename.split(".").slice(-1)) ||
620 CodeMirror.findModeByMIME(modename) ||
621 CodeMirror.findModeByMIME(modename) ||
621 {mode: modename, mime: modename};
622 {mode: modename, mime: modename};
622
623
623 require([
624 require([
624 // might want to use CodeMirror.modeURL here
625 // might want to use CodeMirror.modeURL here
625 ['codemirror/mode', info.mode, info.mode].join('/'),
626 ['codemirror/mode', info.mode, info.mode].join('/'),
626 ], function() {
627 ], function() {
627 // return the original mode, as from a kernelspec on first load
628 // return the original mode, as from a kernelspec on first load
628 // or the mimetype, as for most highlighting
629 // or the mimetype, as for most highlighting
629 callback(mode.name ? mode : info.mime);
630 callback(mode.name ? mode : info.mime);
630 }, errback
631 }, errback
631 );
632 );
632 };
633 };
633
634
634 /** Error type for wrapped XHR errors. */
635 /** Error type for wrapped XHR errors. */
635 var XHR_ERROR = 'XhrError';
636 var XHR_ERROR = 'XhrError';
636
637
637 /**
638 /**
638 * Wraps an AJAX error as an Error object.
639 * Wraps an AJAX error as an Error object.
639 */
640 */
640 var wrap_ajax_error = function (jqXHR, status, error) {
641 var wrap_ajax_error = function (jqXHR, status, error) {
641 var wrapped_error = new Error(ajax_error_msg(jqXHR));
642 var wrapped_error = new Error(ajax_error_msg(jqXHR));
642 wrapped_error.name = XHR_ERROR;
643 wrapped_error.name = XHR_ERROR;
643 // provide xhr response
644 // provide xhr response
644 wrapped_error.xhr = jqXHR;
645 wrapped_error.xhr = jqXHR;
645 wrapped_error.xhr_status = status;
646 wrapped_error.xhr_status = status;
646 wrapped_error.xhr_error = error;
647 wrapped_error.xhr_error = error;
647 return wrapped_error;
648 return wrapped_error;
648 };
649 };
649
650
650 var promising_ajax = function(url, settings) {
651 var promising_ajax = function(url, settings) {
651 /**
652 /**
652 * Like $.ajax, but returning an ES6 promise. success and error settings
653 * Like $.ajax, but returning an ES6 promise. success and error settings
653 * will be ignored.
654 * will be ignored.
654 */
655 */
655 settings = settings || {};
656 settings = settings || {};
656 return new Promise(function(resolve, reject) {
657 return new Promise(function(resolve, reject) {
657 settings.success = function(data, status, jqXHR) {
658 settings.success = function(data, status, jqXHR) {
658 resolve(data);
659 resolve(data);
659 };
660 };
660 settings.error = function(jqXHR, status, error) {
661 settings.error = function(jqXHR, status, error) {
661 log_ajax_error(jqXHR, status, error);
662 log_ajax_error(jqXHR, status, error);
662 reject(wrap_ajax_error(jqXHR, status, error));
663 reject(wrap_ajax_error(jqXHR, status, error));
663 };
664 };
664 $.ajax(url, settings);
665 $.ajax(url, settings);
665 });
666 });
666 };
667 };
667
668
668 var WrappedError = function(message, error){
669 var WrappedError = function(message, error){
669 /**
670 /**
670 * Wrappable Error class
671 * Wrappable Error class
671 *
672 *
672 * The Error class doesn't actually act on `this`. Instead it always
673 * The Error class doesn't actually act on `this`. Instead it always
673 * returns a new instance of Error. Here we capture that instance so we
674 * returns a new instance of Error. Here we capture that instance so we
674 * can apply it's properties to `this`.
675 * can apply it's properties to `this`.
675 */
676 */
676 var tmp = Error.apply(this, [message]);
677 var tmp = Error.apply(this, [message]);
677
678
678 // Copy the properties of the error over to this.
679 // Copy the properties of the error over to this.
679 var properties = Object.getOwnPropertyNames(tmp);
680 var properties = Object.getOwnPropertyNames(tmp);
680 for (var i = 0; i < properties.length; i++) {
681 for (var i = 0; i < properties.length; i++) {
681 this[properties[i]] = tmp[properties[i]];
682 this[properties[i]] = tmp[properties[i]];
682 }
683 }
683
684
684 // Keep a stack of the original error messages.
685 // Keep a stack of the original error messages.
685 if (error instanceof WrappedError) {
686 if (error instanceof WrappedError) {
686 this.error_stack = error.error_stack;
687 this.error_stack = error.error_stack;
687 } else {
688 } else {
688 this.error_stack = [error];
689 this.error_stack = [error];
689 }
690 }
690 this.error_stack.push(tmp);
691 this.error_stack.push(tmp);
691
692
692 return this;
693 return this;
693 };
694 };
694
695
695 WrappedError.prototype = Object.create(Error.prototype, {});
696 WrappedError.prototype = Object.create(Error.prototype, {});
696
697
697
698
698 var load_class = function(class_name, module_name, registry) {
699 var load_class = function(class_name, module_name, registry) {
699 /**
700 /**
700 * Tries to load a class
701 * Tries to load a class
701 *
702 *
702 * Tries to load a class from a module using require.js, if a module
703 * Tries to load a class from a module using require.js, if a module
703 * is specified, otherwise tries to load a class from the global
704 * is specified, otherwise tries to load a class from the global
704 * registry, if the global registry is provided.
705 * registry, if the global registry is provided.
705 */
706 */
706 return new Promise(function(resolve, reject) {
707 return new Promise(function(resolve, reject) {
707
708
708 // Try loading the view module using require.js
709 // Try loading the view module using require.js
709 if (module_name) {
710 if (module_name) {
710 require([module_name], function(module) {
711 require([module_name], function(module) {
711 if (module[class_name] === undefined) {
712 if (module[class_name] === undefined) {
712 reject(new Error('Class '+class_name+' not found in module '+module_name));
713 reject(new Error('Class '+class_name+' not found in module '+module_name));
713 } else {
714 } else {
714 resolve(module[class_name]);
715 resolve(module[class_name]);
715 }
716 }
716 }, reject);
717 }, reject);
717 } else {
718 } else {
718 if (registry && registry[class_name]) {
719 if (registry && registry[class_name]) {
719 resolve(registry[class_name]);
720 resolve(registry[class_name]);
720 } else {
721 } else {
721 reject(new Error('Class '+class_name+' not found in registry '));
722 reject(new Error('Class '+class_name+' not found in registry '));
722 }
723 }
723 }
724 }
724 });
725 });
725 };
726 };
726
727
727 var resolve_promises_dict = function(d) {
728 var resolve_promises_dict = function(d) {
728 /**
729 /**
729 * Resolve a promiseful dictionary.
730 * Resolve a promiseful dictionary.
730 * Returns a single Promise.
731 * Returns a single Promise.
731 */
732 */
732 var keys = Object.keys(d);
733 var keys = Object.keys(d);
733 var values = [];
734 var values = [];
734 keys.forEach(function(key) {
735 keys.forEach(function(key) {
735 values.push(d[key]);
736 values.push(d[key]);
736 });
737 });
737 return Promise.all(values).then(function(v) {
738 return Promise.all(values).then(function(v) {
738 d = {};
739 d = {};
739 for(var i=0; i<keys.length; i++) {
740 for(var i=0; i<keys.length; i++) {
740 d[keys[i]] = v[i];
741 d[keys[i]] = v[i];
741 }
742 }
742 return d;
743 return d;
743 });
744 });
744 };
745 };
745
746
746 var reject = function(message, log) {
747 var reject = function(message, log) {
747 /**
748 /**
748 * Creates a wrappable Promise rejection function.
749 * Creates a wrappable Promise rejection function.
749 *
750 *
750 * Creates a function that returns a Promise.reject with a new WrappedError
751 * Creates a function that returns a Promise.reject with a new WrappedError
751 * that has the provided message and wraps the original error that
752 * that has the provided message and wraps the original error that
752 * caused the promise to reject.
753 * caused the promise to reject.
753 */
754 */
754 return function(error) {
755 return function(error) {
755 var wrapped_error = new WrappedError(message, error);
756 var wrapped_error = new WrappedError(message, error);
756 if (log) console.error(wrapped_error);
757 if (log) console.error(wrapped_error);
757 return Promise.reject(wrapped_error);
758 return Promise.reject(wrapped_error);
758 };
759 };
759 };
760 };
760
761
761 var typeset = function(element, text) {
762 var typeset = function(element, text) {
762 /**
763 /**
763 * Apply MathJax rendering to an element, and optionally set its text
764 * Apply MathJax rendering to an element, and optionally set its text
764 *
765 *
765 * If MathJax is not available, make no changes.
766 * If MathJax is not available, make no changes.
766 *
767 *
767 * Returns the output any number of typeset elements, or undefined if
768 * Returns the output any number of typeset elements, or undefined if
768 * MathJax was not available.
769 * MathJax was not available.
769 *
770 *
770 * Parameters
771 * Parameters
771 * ----------
772 * ----------
772 * element: Node, NodeList, or jQuery selection
773 * element: Node, NodeList, or jQuery selection
773 * text: option string
774 * text: option string
774 */
775 */
775 if(!window.MathJax){
776 if(!window.MathJax){
776 return;
777 return;
777 }
778 }
778 var $el = element.jquery ? element : $(element);
779 var $el = element.jquery ? element : $(element);
779 if(arguments.length > 1){
780 if(arguments.length > 1){
780 $el.text(text);
781 $el.text(text);
781 }
782 }
782 return $el.map(function(){
783 return $el.map(function(){
783 // MathJax takes a DOM node: $.map makes `this` the context
784 // MathJax takes a DOM node: $.map makes `this` the context
784 return MathJax.Hub.Queue(["Typeset", MathJax.Hub, this]);
785 return MathJax.Hub.Queue(["Typeset", MathJax.Hub, this]);
785 });
786 });
786 };
787 };
787
788
789 var time = {};
790 time.milliseconds = {};
791 time.milliseconds.s = 1000;
792 time.milliseconds.m = 60 * time.milliseconds.s;
793 time.milliseconds.h = 60 * time.milliseconds.m;
794 time.milliseconds.d = 24 * time.milliseconds.h;
795
796 time.thresholds = {
797 // moment.js thresholds in milliseconds
798 s: moment.relativeTimeThreshold('s') * time.milliseconds.s,
799 m: moment.relativeTimeThreshold('m') * time.milliseconds.m,
800 h: moment.relativeTimeThreshold('h') * time.milliseconds.h,
801 d: moment.relativeTimeThreshold('d') * time.milliseconds.d,
802 };
803
804 time.timeout_from_dt = function (dt) {
805 /** compute a timeout based on dt
806
807 input and output both in milliseconds
808
809 use moment's relative time thresholds:
810
811 - 10 seconds if in 'seconds ago' territory
812 - 1 minute if in 'minutes ago'
813 - 1 hour otherwise
814 */
815 if (dt < time.thresholds.s) {
816 return 10 * time.milliseconds.s;
817 } else if (dt < time.thresholds.m) {
818 return time.milliseconds.m;
819 } else {
820 return time.milliseconds.h;
821 }
822 };
823
788 var utils = {
824 var utils = {
789 regex_split : regex_split,
825 regex_split : regex_split,
790 uuid : uuid,
826 uuid : uuid,
791 fixConsole : fixConsole,
827 fixConsole : fixConsole,
792 fixCarriageReturn : fixCarriageReturn,
828 fixCarriageReturn : fixCarriageReturn,
793 autoLinkUrls : autoLinkUrls,
829 autoLinkUrls : autoLinkUrls,
794 points_to_pixels : points_to_pixels,
830 points_to_pixels : points_to_pixels,
795 get_body_data : get_body_data,
831 get_body_data : get_body_data,
796 parse_url : parse_url,
832 parse_url : parse_url,
797 url_path_split : url_path_split,
833 url_path_split : url_path_split,
798 url_path_join : url_path_join,
834 url_path_join : url_path_join,
799 url_join_encode : url_join_encode,
835 url_join_encode : url_join_encode,
800 encode_uri_components : encode_uri_components,
836 encode_uri_components : encode_uri_components,
801 splitext : splitext,
837 splitext : splitext,
802 escape_html : escape_html,
838 escape_html : escape_html,
803 always_new : always_new,
839 always_new : always_new,
804 to_absolute_cursor_pos : to_absolute_cursor_pos,
840 to_absolute_cursor_pos : to_absolute_cursor_pos,
805 from_absolute_cursor_pos : from_absolute_cursor_pos,
841 from_absolute_cursor_pos : from_absolute_cursor_pos,
806 browser : browser,
842 browser : browser,
807 platform: platform,
843 platform: platform,
808 get_url_param: get_url_param,
844 get_url_param: get_url_param,
809 is_or_has : is_or_has,
845 is_or_has : is_or_has,
810 is_focused : is_focused,
846 is_focused : is_focused,
811 mergeopt: mergeopt,
847 mergeopt: mergeopt,
812 ajax_error_msg : ajax_error_msg,
848 ajax_error_msg : ajax_error_msg,
813 log_ajax_error : log_ajax_error,
849 log_ajax_error : log_ajax_error,
814 requireCodeMirrorMode : requireCodeMirrorMode,
850 requireCodeMirrorMode : requireCodeMirrorMode,
815 XHR_ERROR : XHR_ERROR,
851 XHR_ERROR : XHR_ERROR,
816 wrap_ajax_error : wrap_ajax_error,
852 wrap_ajax_error : wrap_ajax_error,
817 promising_ajax : promising_ajax,
853 promising_ajax : promising_ajax,
818 WrappedError: WrappedError,
854 WrappedError: WrappedError,
819 load_class: load_class,
855 load_class: load_class,
820 resolve_promises_dict: resolve_promises_dict,
856 resolve_promises_dict: resolve_promises_dict,
821 reject: reject,
857 reject: reject,
822 typeset: typeset,
858 typeset: typeset,
859 time: time,
823 };
860 };
824
861
825 // Backwards compatability.
862 // Backwards compatability.
826 IPython.utils = utils;
863 IPython.utils = utils;
827
864
828 return utils;
865 return utils;
829 });
866 });
@@ -1,1 +1,1 b''
1 Subproject commit 87ff70d96567bf055eb94161a41e7b3e6da31b23
1 Subproject commit 6b578bb789708ded9b6611039d162c71135dfd68
@@ -1,202 +1,175 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 'base/js/namespace',
5 'base/js/namespace',
6 'jquery',
6 'jquery',
7 'base/js/utils',
7 'base/js/utils',
8 'base/js/dialog',
8 'base/js/dialog',
9 'base/js/keyboard',
9 'base/js/keyboard',
10 'moment',
10 'moment',
11 ], function(IPython, $, utils, dialog, keyboard, moment) {
11 ], function(IPython, $, utils, dialog, keyboard, moment) {
12 "use strict";
12 "use strict";
13
13
14 var SaveWidget = function (selector, options) {
14 var SaveWidget = function (selector, options) {
15 this.editor = undefined;
15 this.editor = undefined;
16 this.selector = selector;
16 this.selector = selector;
17 this.events = options.events;
17 this.events = options.events;
18 this.editor = options.editor;
18 this.editor = options.editor;
19 this._last_modified = undefined;
19 this._last_modified = undefined;
20 this.keyboard_manager = options.keyboard_manager;
20 this.keyboard_manager = options.keyboard_manager;
21 if (this.selector !== undefined) {
21 if (this.selector !== undefined) {
22 this.element = $(selector);
22 this.element = $(selector);
23 this.bind_events();
23 this.bind_events();
24 }
24 }
25 };
25 };
26
26
27
27
28 SaveWidget.prototype.bind_events = function () {
28 SaveWidget.prototype.bind_events = function () {
29 var that = this;
29 var that = this;
30 this.element.find('span.filename').click(function () {
30 this.element.find('span.filename').click(function () {
31 that.rename();
31 that.rename();
32 });
32 });
33 this.events.on('file_loaded.Editor', function (evt, model) {
33 this.events.on('file_loaded.Editor', function (evt, model) {
34 that.update_filename(model.name);
34 that.update_filename(model.name);
35 that.update_document_title(model.name);
35 that.update_document_title(model.name);
36 that.update_last_modified(model.last_modified);
36 that.update_last_modified(model.last_modified);
37 });
37 });
38 this.events.on('file_saved.Editor', function (evt, model) {
38 this.events.on('file_saved.Editor', function (evt, model) {
39 that.update_filename(model.name);
39 that.update_filename(model.name);
40 that.update_document_title(model.name);
40 that.update_document_title(model.name);
41 that.update_last_modified(model.last_modified);
41 that.update_last_modified(model.last_modified);
42 });
42 });
43 this.events.on('file_renamed.Editor', function (evt, model) {
43 this.events.on('file_renamed.Editor', function (evt, model) {
44 that.update_filename(model.name);
44 that.update_filename(model.name);
45 that.update_document_title(model.name);
45 that.update_document_title(model.name);
46 that.update_address_bar(model.path);
46 that.update_address_bar(model.path);
47 });
47 });
48 this.events.on('file_save_failed.Editor', function () {
48 this.events.on('file_save_failed.Editor', function () {
49 that.set_save_status('Save Failed!');
49 that.set_save_status('Save Failed!');
50 });
50 });
51 };
51 };
52
52
53
53
54 SaveWidget.prototype.rename = function (options) {
54 SaveWidget.prototype.rename = function (options) {
55 options = options || {};
55 options = options || {};
56 var that = this;
56 var that = this;
57 var dialog_body = $('<div/>').append(
57 var dialog_body = $('<div/>').append(
58 $("<p/>").addClass("rename-message")
58 $("<p/>").addClass("rename-message")
59 .text('Enter a new filename:')
59 .text('Enter a new filename:')
60 ).append(
60 ).append(
61 $("<br/>")
61 $("<br/>")
62 ).append(
62 ).append(
63 $('<input/>').attr('type','text').attr('size','25').addClass('form-control')
63 $('<input/>').attr('type','text').attr('size','25').addClass('form-control')
64 .val(that.editor.get_filename())
64 .val(that.editor.get_filename())
65 );
65 );
66 var d = dialog.modal({
66 var d = dialog.modal({
67 title: "Rename File",
67 title: "Rename File",
68 body: dialog_body,
68 body: dialog_body,
69 buttons : {
69 buttons : {
70 "OK": {
70 "OK": {
71 class: "btn-primary",
71 class: "btn-primary",
72 click: function () {
72 click: function () {
73 var new_name = d.find('input').val();
73 var new_name = d.find('input').val();
74 d.find('.rename-message').text("Renaming...");
74 d.find('.rename-message').text("Renaming...");
75 d.find('input[type="text"]').prop('disabled', true);
75 d.find('input[type="text"]').prop('disabled', true);
76 that.editor.rename(new_name).then(
76 that.editor.rename(new_name).then(
77 function () {
77 function () {
78 d.modal('hide');
78 d.modal('hide');
79 }, function (error) {
79 }, function (error) {
80 d.find('.rename-message').text(error.message || 'Unknown error');
80 d.find('.rename-message').text(error.message || 'Unknown error');
81 d.find('input[type="text"]').prop('disabled', false).focus().select();
81 d.find('input[type="text"]').prop('disabled', false).focus().select();
82 }
82 }
83 );
83 );
84 return false;
84 return false;
85 }
85 }
86 },
86 },
87 "Cancel": {}
87 "Cancel": {}
88 },
88 },
89 open : function () {
89 open : function () {
90 // Upon ENTER, click the OK button.
90 // Upon ENTER, click the OK button.
91 d.find('input[type="text"]').keydown(function (event) {
91 d.find('input[type="text"]').keydown(function (event) {
92 if (event.which === keyboard.keycodes.enter) {
92 if (event.which === keyboard.keycodes.enter) {
93 d.find('.btn-primary').first().click();
93 d.find('.btn-primary').first().click();
94 return false;
94 return false;
95 }
95 }
96 });
96 });
97 d.find('input[type="text"]').focus().select();
97 d.find('input[type="text"]').focus().select();
98 }
98 }
99 });
99 });
100 };
100 };
101
101
102
102
103 SaveWidget.prototype.update_filename = function (filename) {
103 SaveWidget.prototype.update_filename = function (filename) {
104 this.element.find('span.filename').text(filename);
104 this.element.find('span.filename').text(filename);
105 };
105 };
106
106
107 SaveWidget.prototype.update_document_title = function (filename) {
107 SaveWidget.prototype.update_document_title = function (filename) {
108 document.title = filename;
108 document.title = filename;
109 };
109 };
110
110
111 SaveWidget.prototype.update_address_bar = function (path) {
111 SaveWidget.prototype.update_address_bar = function (path) {
112 var state = {path : path};
112 var state = {path : path};
113 window.history.replaceState(state, "", utils.url_join_encode(
113 window.history.replaceState(state, "", utils.url_join_encode(
114 this.editor.base_url,
114 this.editor.base_url,
115 "edit",
115 "edit",
116 path)
116 path)
117 );
117 );
118 };
118 };
119
119
120 SaveWidget.prototype.update_last_modified = function (last_modified) {
120 SaveWidget.prototype.update_last_modified = function (last_modified) {
121 if (last_modified) {
121 if (last_modified) {
122 this._last_modified = new Date(last_modified);
122 this._last_modified = new Date(last_modified);
123 } else {
123 } else {
124 this._last_modified = null;
124 this._last_modified = null;
125 }
125 }
126 this._render_last_modified();
126 this._render_last_modified();
127 };
127 };
128
128
129 SaveWidget.prototype._render_last_modified = function () {
129 SaveWidget.prototype._render_last_modified = function () {
130 /** actually set the text in the element, from our _last_modified value
130 /** actually set the text in the element, from our _last_modified value
131
131
132 called directly, and periodically in timeouts.
132 called directly, and periodically in timeouts.
133 */
133 */
134 this._schedule_render_last_modified();
134 this._schedule_render_last_modified();
135 var el = this.element.find('span.last_modified');
135 var el = this.element.find('span.last_modified');
136 if (!this._last_modified) {
136 if (!this._last_modified) {
137 el.text('').attr('title', 'never saved');
137 el.text('').attr('title', 'never saved');
138 return;
138 return;
139 }
139 }
140 var chkd = moment(this._last_modified);
140 var chkd = moment(this._last_modified);
141 var long_date = chkd.format('llll');
141 var long_date = chkd.format('llll');
142 var human_date;
142 var human_date;
143 var tdelta = Math.ceil(new Date() - this._last_modified);
143 var tdelta = Math.ceil(new Date() - this._last_modified);
144 if (tdelta < 24 * H){
144 if (tdelta < utils.time.milliseconds.d){
145 // less than 24 hours old, use relative date
145 // less than 24 hours old, use relative date
146 human_date = chkd.fromNow();
146 human_date = chkd.fromNow();
147 } else {
147 } else {
148 // otherwise show calendar
148 // otherwise show calendar
149 // otherwise update every hour and show
150 // <Today | yesterday|...> at hh,mm,ss
149 // <Today | yesterday|...> at hh,mm,ss
151 human_date = chkd.calendar();
150 human_date = chkd.calendar();
152 }
151 }
153 el.text(human_date).attr('title', long_date);
152 el.text(human_date).attr('title', long_date);
154 };
153 };
155
156
157 var S = 1000;
158 var M = 60*S;
159 var H = 60*M;
160 var thresholds = {
161 s: 45 * S,
162 m: 45 * M,
163 h: 22 * H
164 };
165 var _timeout_from_dt = function (ms) {
166 /** compute a timeout to update the last-modified timeout
167
168 based on the delta in milliseconds
169 */
170 if (ms < thresholds.s) {
171 return 5 * S;
172 } else if (ms < thresholds.m) {
173 return M;
174 } else {
175 return 5 * M;
176 }
177 };
178
154
179 SaveWidget.prototype._schedule_render_last_modified = function () {
155 SaveWidget.prototype._schedule_render_last_modified = function () {
180 /** schedule the next update to relative date
156 /** schedule the next update to relative date
181
157
182 periodically updated, so short values like 'a few seconds ago' don't get stale.
158 periodically updated, so short values like 'a few seconds ago' don't get stale.
183 */
159 */
184 var that = this;
185 if (!this._last_modified) {
160 if (!this._last_modified) {
186 return;
161 return;
187 }
162 }
188 if ((this._last_modified_timeout)) {
163 if ((this._last_modified_timeout)) {
189 clearTimeout(this._last_modified_timeout);
164 clearTimeout(this._last_modified_timeout);
190 }
165 }
191 var dt = Math.ceil(new Date() - this._last_modified);
166 var dt = Math.ceil(new Date() - this._last_modified);
192 if (dt < 24 * H) {
167 this._last_modified_timeout = setTimeout(
193 this._last_modified_timeout = setTimeout(
168 $.proxy(this._render_last_modified, this),
194 $.proxy(this._render_last_modified, this),
169 utils.time.timeout_from_dt(dt)
195 _timeout_from_dt(dt)
170 );
196 );
197 }
198 };
171 };
199
172
200 return {'SaveWidget': SaveWidget};
173 return {'SaveWidget': SaveWidget};
201
174
202 });
175 });
@@ -1,260 +1,225 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 'base/js/namespace',
5 'base/js/namespace',
6 'jquery',
6 'jquery',
7 'base/js/utils',
7 'base/js/utils',
8 'base/js/dialog',
8 'base/js/dialog',
9 'base/js/keyboard',
9 'base/js/keyboard',
10 'moment',
10 'moment',
11 ], function(IPython, $, utils, dialog, keyboard, moment) {
11 ], function(IPython, $, utils, dialog, keyboard, moment) {
12 "use strict";
12 "use strict";
13
13
14 var SaveWidget = function (selector, options) {
14 var SaveWidget = function (selector, options) {
15 /**
15 /**
16 * TODO: Remove circular ref.
16 * TODO: Remove circular ref.
17 */
17 */
18 this.notebook = undefined;
18 this.notebook = undefined;
19 this.selector = selector;
19 this.selector = selector;
20 this.events = options.events;
20 this.events = options.events;
21 this._checkpoint_date = undefined;
21 this._checkpoint_date = undefined;
22 this.keyboard_manager = options.keyboard_manager;
22 this.keyboard_manager = options.keyboard_manager;
23 if (this.selector !== undefined) {
23 if (this.selector !== undefined) {
24 this.element = $(selector);
24 this.element = $(selector);
25 this.bind_events();
25 this.bind_events();
26 }
26 }
27 };
27 };
28
28
29
29
30 SaveWidget.prototype.bind_events = function () {
30 SaveWidget.prototype.bind_events = function () {
31 var that = this;
31 var that = this;
32 this.element.find('span.filename').click(function () {
32 this.element.find('span.filename').click(function () {
33 that.rename_notebook({notebook: that.notebook});
33 that.rename_notebook({notebook: that.notebook});
34 });
34 });
35 this.events.on('notebook_loaded.Notebook', function () {
35 this.events.on('notebook_loaded.Notebook', function () {
36 that.update_notebook_name();
36 that.update_notebook_name();
37 that.update_document_title();
37 that.update_document_title();
38 });
38 });
39 this.events.on('notebook_saved.Notebook', function () {
39 this.events.on('notebook_saved.Notebook', function () {
40 that.update_notebook_name();
40 that.update_notebook_name();
41 that.update_document_title();
41 that.update_document_title();
42 });
42 });
43 this.events.on('notebook_renamed.Notebook', function () {
43 this.events.on('notebook_renamed.Notebook', function () {
44 that.update_notebook_name();
44 that.update_notebook_name();
45 that.update_document_title();
45 that.update_document_title();
46 that.update_address_bar();
46 that.update_address_bar();
47 });
47 });
48 this.events.on('notebook_save_failed.Notebook', function () {
48 this.events.on('notebook_save_failed.Notebook', function () {
49 that.set_save_status('Autosave Failed!');
49 that.set_save_status('Autosave Failed!');
50 });
50 });
51 this.events.on('notebook_read_only.Notebook', function () {
51 this.events.on('notebook_read_only.Notebook', function () {
52 that.set_save_status('(read only)');
52 that.set_save_status('(read only)');
53 // disable future set_save_status
53 // disable future set_save_status
54 that.set_save_status = function () {};
54 that.set_save_status = function () {};
55 });
55 });
56 this.events.on('checkpoints_listed.Notebook', function (event, data) {
56 this.events.on('checkpoints_listed.Notebook', function (event, data) {
57 that._set_last_checkpoint(data[0]);
57 that._set_last_checkpoint(data[0]);
58 });
58 });
59
59
60 this.events.on('checkpoint_created.Notebook', function (event, data) {
60 this.events.on('checkpoint_created.Notebook', function (event, data) {
61 that._set_last_checkpoint(data);
61 that._set_last_checkpoint(data);
62 });
62 });
63 this.events.on('set_dirty.Notebook', function (event, data) {
63 this.events.on('set_dirty.Notebook', function (event, data) {
64 that.set_autosaved(data.value);
64 that.set_autosaved(data.value);
65 });
65 });
66 };
66 };
67
67
68
68
69 SaveWidget.prototype.rename_notebook = function (options) {
69 SaveWidget.prototype.rename_notebook = function (options) {
70 options = options || {};
70 options = options || {};
71 var that = this;
71 var that = this;
72 var dialog_body = $('<div/>').append(
72 var dialog_body = $('<div/>').append(
73 $("<p/>").addClass("rename-message")
73 $("<p/>").addClass("rename-message")
74 .text('Enter a new notebook name:')
74 .text('Enter a new notebook name:')
75 ).append(
75 ).append(
76 $("<br/>")
76 $("<br/>")
77 ).append(
77 ).append(
78 $('<input/>').attr('type','text').attr('size','25').addClass('form-control')
78 $('<input/>').attr('type','text').attr('size','25').addClass('form-control')
79 .val(options.notebook.get_notebook_name())
79 .val(options.notebook.get_notebook_name())
80 );
80 );
81 var d = dialog.modal({
81 var d = dialog.modal({
82 title: "Rename Notebook",
82 title: "Rename Notebook",
83 body: dialog_body,
83 body: dialog_body,
84 notebook: options.notebook,
84 notebook: options.notebook,
85 keyboard_manager: this.keyboard_manager,
85 keyboard_manager: this.keyboard_manager,
86 buttons : {
86 buttons : {
87 "OK": {
87 "OK": {
88 class: "btn-primary",
88 class: "btn-primary",
89 click: function () {
89 click: function () {
90 var new_name = d.find('input').val();
90 var new_name = d.find('input').val();
91 if (!options.notebook.test_notebook_name(new_name)) {
91 if (!options.notebook.test_notebook_name(new_name)) {
92 d.find('.rename-message').text(
92 d.find('.rename-message').text(
93 "Invalid notebook name. Notebook names must "+
93 "Invalid notebook name. Notebook names must "+
94 "have 1 or more characters and can contain any characters " +
94 "have 1 or more characters and can contain any characters " +
95 "except :/\\. Please enter a new notebook name:"
95 "except :/\\. Please enter a new notebook name:"
96 );
96 );
97 return false;
97 return false;
98 } else {
98 } else {
99 d.find('.rename-message').text("Renaming...");
99 d.find('.rename-message').text("Renaming...");
100 d.find('input[type="text"]').prop('disabled', true);
100 d.find('input[type="text"]').prop('disabled', true);
101 that.notebook.rename(new_name).then(
101 that.notebook.rename(new_name).then(
102 function () {
102 function () {
103 d.modal('hide');
103 d.modal('hide');
104 }, function (error) {
104 }, function (error) {
105 d.find('.rename-message').text(error.message || 'Unknown error');
105 d.find('.rename-message').text(error.message || 'Unknown error');
106 d.find('input[type="text"]').prop('disabled', false).focus().select();
106 d.find('input[type="text"]').prop('disabled', false).focus().select();
107 }
107 }
108 );
108 );
109 return false;
109 return false;
110 }
110 }
111 }
111 }
112 },
112 },
113 "Cancel": {}
113 "Cancel": {}
114 },
114 },
115 open : function () {
115 open : function () {
116 /**
116 /**
117 * Upon ENTER, click the OK button.
117 * Upon ENTER, click the OK button.
118 */
118 */
119 d.find('input[type="text"]').keydown(function (event) {
119 d.find('input[type="text"]').keydown(function (event) {
120 if (event.which === keyboard.keycodes.enter) {
120 if (event.which === keyboard.keycodes.enter) {
121 d.find('.btn-primary').first().click();
121 d.find('.btn-primary').first().click();
122 return false;
122 return false;
123 }
123 }
124 });
124 });
125 d.find('input[type="text"]').focus().select();
125 d.find('input[type="text"]').focus().select();
126 }
126 }
127 });
127 });
128 };
128 };
129
129
130
130
131 SaveWidget.prototype.update_notebook_name = function () {
131 SaveWidget.prototype.update_notebook_name = function () {
132 var nbname = this.notebook.get_notebook_name();
132 var nbname = this.notebook.get_notebook_name();
133 this.element.find('span.filename').text(nbname);
133 this.element.find('span.filename').text(nbname);
134 };
134 };
135
135
136
136
137 SaveWidget.prototype.update_document_title = function () {
137 SaveWidget.prototype.update_document_title = function () {
138 var nbname = this.notebook.get_notebook_name();
138 var nbname = this.notebook.get_notebook_name();
139 document.title = nbname;
139 document.title = nbname;
140 };
140 };
141
141
142 SaveWidget.prototype.update_address_bar = function(){
142 SaveWidget.prototype.update_address_bar = function(){
143 var base_url = this.notebook.base_url;
143 var base_url = this.notebook.base_url;
144 var path = this.notebook.notebook_path;
144 var path = this.notebook.notebook_path;
145 var state = {path : path};
145 var state = {path : path};
146 window.history.replaceState(state, "", utils.url_join_encode(
146 window.history.replaceState(state, "", utils.url_join_encode(
147 base_url,
147 base_url,
148 "notebooks",
148 "notebooks",
149 path)
149 path)
150 );
150 );
151 };
151 };
152
152
153
153
154 SaveWidget.prototype.set_save_status = function (msg) {
154 SaveWidget.prototype.set_save_status = function (msg) {
155 this.element.find('span.autosave_status').text(msg);
155 this.element.find('span.autosave_status').text(msg);
156 };
156 };
157
157
158 SaveWidget.prototype._set_checkpoint_status = function (human_date, iso_date) {
158 SaveWidget.prototype._set_last_checkpoint = function (checkpoint) {
159 var el = this.element.find('span.checkpoint_status');
159 if (checkpoint) {
160 if(human_date){
160 this._checkpoint_date = new Date(checkpoint.last_modified);
161 el.text("Last Checkpoint: "+human_date).attr('title',iso_date);
162 } else {
161 } else {
163 el.text('').attr('title', 'no-checkpoint');
162 this._checkpoint_date = null;
164 }
165 };
166
167 // compute (roughly) the remaining time in millisecond until the next
168 // moment.js relative time update of the string, which by default
169 // happend at
170 // (a few seconds ago)
171 // - 45sec,
172 // (a minute ago)
173 // - 90sec,
174 // ( x minutes ago)
175 // - then every minutes until
176 // - 45 min,
177 // (an hour ago)
178 // - 1h45,
179 // (x hours ago )
180 // - then every hours
181 // - 22 hours ago
182 var _next_timeago_update = function(deltatime_ms){
183 var s = 1000;
184 var m = 60*s;
185 var h = 60*m;
186
187 var mtt = moment.relativeTimeThreshold;
188
189 if(deltatime_ms < mtt.s*s){
190 return mtt.s*s-deltatime_ms;
191 } else if (deltatime_ms < (mtt.s*s+m)) {
192 return (mtt.s*s+m)-deltatime_ms;
193 } else if (deltatime_ms < mtt.m*m){
194 return m;
195 } else if (deltatime_ms < (mtt.m*m+h)){
196 return (mtt.m*m+h)-deltatime_ms;
197 } else {
198 return h;
199 }
163 }
164 this._render_checkpoint();
200 };
165 };
201
166
202 SaveWidget.prototype._regularly_update_checkpoint_date = function(){
167 SaveWidget.prototype._render_checkpoint = function () {
203 if (!this._checkpoint_date) {
168 /** actually set the text in the element, from our _checkpoint value
204 this._set_checkpoint_status(null);
169
205 console.log('no checkpoint done');
170 called directly, and periodically in timeouts.
171 */
172 this._schedule_render_checkpoint();
173 var el = this.element.find('span.checkpoint_status');
174 if (!this._checkpoint_date) {
175 el.text('').attr('title', 'no checkpoint');
206 return;
176 return;
207 }
177 }
208 var chkd = moment(this._checkpoint_date);
178 var chkd = moment(this._checkpoint_date);
209 var longdate = chkd.format('llll');
179 var long_date = chkd.format('llll');
210
180 var human_date;
211 var that = this;
181 var tdelta = Math.ceil(new Date() - this._checkpoint_date);
212 var recall = function(t){
182 if (tdelta < utils.time.milliseconds.d){
213 /**
183 // less than 24 hours old, use relative date
214 * recall slightly later (1s) as long timeout in js might be imprecise,
184 human_date = chkd.fromNow();
215 * and you want to be call **after** the change of formatting should happend.
185 } else {
216 */
186 // otherwise show calendar
217 return setTimeout(
187 // <Today | yesterday|...> at hh,mm,ss
218 $.proxy(that._regularly_update_checkpoint_date, that),
188 human_date = chkd.calendar();
219 t + 1000
220 );
221 };
222 var tdelta = Math.ceil(new Date()-this._checkpoint_date);
223
224 // update regularly for the first 6hours and show
225 // <x time> ago
226 if(tdelta < 6*3600*1000){
227 recall(_next_timeago_update(tdelta));
228 this._set_checkpoint_status(chkd.fromNow(), longdate);
229 // otherwise update every hour and show
230 // <Today | yesterday|...> at hh,mm,ss
231 } else {
232 recall(1*3600*1000);
233 this._set_checkpoint_status(chkd.calendar(), longdate);
234 }
189 }
190 el.text('Last Checkpoint: ' + human_date).attr('title', long_date);
235 };
191 };
236
192
237 SaveWidget.prototype._set_last_checkpoint = function (checkpoint) {
193
238 if (checkpoint) {
194 SaveWidget.prototype._schedule_render_checkpoint = function () {
239 this._checkpoint_date = new Date(checkpoint.last_modified);
195 /** schedule the next update to relative date
240 } else {
196
241 this._checkpoint_date = null;
197 periodically updated, so short values like 'a few seconds ago' don't get stale.
198 */
199 if (!this._checkpoint_date) {
200 return;
242 }
201 }
243 this._regularly_update_checkpoint_date();
202 if ((this._checkpoint_timeout)) {
244
203 clearTimeout(this._checkpoint_timeout);
204 }
205 var dt = Math.ceil(new Date() - this._checkpoint_date);
206 this._checkpoint_timeout = setTimeout(
207 $.proxy(this._render_checkpoint, this),
208 utils.time.timeout_from_dt(dt)
209 );
245 };
210 };
246
211
247 SaveWidget.prototype.set_autosaved = function (dirty) {
212 SaveWidget.prototype.set_autosaved = function (dirty) {
248 if (dirty) {
213 if (dirty) {
249 this.set_save_status("(unsaved changes)");
214 this.set_save_status("(unsaved changes)");
250 } else {
215 } else {
251 this.set_save_status("(autosaved)");
216 this.set_save_status("(autosaved)");
252 }
217 }
253 };
218 };
254
219
255 // Backwards compatibility.
220 // Backwards compatibility.
256 IPython.SaveWidget = SaveWidget;
221 IPython.SaveWidget = SaveWidget;
257
222
258 return {'SaveWidget': SaveWidget};
223 return {'SaveWidget': SaveWidget};
259
224
260 });
225 });
General Comments 0
You need to be logged in to leave comments. Login now