Show More
@@ -0,0 +1,202 b'' | |||
|
1 | // Copyright (c) IPython Development Team. | |
|
2 | // Distributed under the terms of the Modified BSD License. | |
|
3 | ||
|
4 | define([ | |
|
5 | 'base/js/namespace', | |
|
6 | 'jquery', | |
|
7 | 'base/js/utils', | |
|
8 | 'base/js/dialog', | |
|
9 | 'base/js/keyboard', | |
|
10 | 'moment', | |
|
11 | ], function(IPython, $, utils, dialog, keyboard, moment) { | |
|
12 | "use strict"; | |
|
13 | ||
|
14 | var SaveWidget = function (selector, options) { | |
|
15 | this.editor = undefined; | |
|
16 | this.selector = selector; | |
|
17 | this.events = options.events; | |
|
18 | this.editor = options.editor; | |
|
19 | this._last_modified = undefined; | |
|
20 | this.keyboard_manager = options.keyboard_manager; | |
|
21 | if (this.selector !== undefined) { | |
|
22 | this.element = $(selector); | |
|
23 | this.bind_events(); | |
|
24 | } | |
|
25 | }; | |
|
26 | ||
|
27 | ||
|
28 | SaveWidget.prototype.bind_events = function () { | |
|
29 | var that = this; | |
|
30 | this.element.find('span.filename').click(function () { | |
|
31 | that.rename(); | |
|
32 | }); | |
|
33 | this.events.on('file_loaded.Editor', function (evt, model) { | |
|
34 | that.update_filename(model.name); | |
|
35 | that.update_document_title(model.name); | |
|
36 | that.update_last_modified(model.last_modified); | |
|
37 | }); | |
|
38 | this.events.on('file_saved.Editor', function (evt, model) { | |
|
39 | that.update_filename(model.name); | |
|
40 | that.update_document_title(model.name); | |
|
41 | that.update_last_modified(model.last_modified); | |
|
42 | }); | |
|
43 | this.events.on('file_renamed.Editor', function (evt, model) { | |
|
44 | that.update_filename(model.name); | |
|
45 | that.update_document_title(model.name); | |
|
46 | that.update_address_bar(model.path); | |
|
47 | }); | |
|
48 | this.events.on('file_save_failed.Editor', function () { | |
|
49 | that.set_save_status('Save Failed!'); | |
|
50 | }); | |
|
51 | }; | |
|
52 | ||
|
53 | ||
|
54 | SaveWidget.prototype.rename = function (options) { | |
|
55 | options = options || {}; | |
|
56 | var that = this; | |
|
57 | var dialog_body = $('<div/>').append( | |
|
58 | $("<p/>").addClass("rename-message") | |
|
59 | .text('Enter a new filename:') | |
|
60 | ).append( | |
|
61 | $("<br/>") | |
|
62 | ).append( | |
|
63 | $('<input/>').attr('type','text').attr('size','25').addClass('form-control') | |
|
64 | .val(that.editor.get_filename()) | |
|
65 | ); | |
|
66 | var d = dialog.modal({ | |
|
67 | title: "Rename File", | |
|
68 | body: dialog_body, | |
|
69 | buttons : { | |
|
70 | "OK": { | |
|
71 | class: "btn-primary", | |
|
72 | click: function () { | |
|
73 | var new_name = d.find('input').val(); | |
|
74 | d.find('.rename-message').text("Renaming..."); | |
|
75 | d.find('input[type="text"]').prop('disabled', true); | |
|
76 | that.editor.rename(new_name).then( | |
|
77 | function () { | |
|
78 | d.modal('hide'); | |
|
79 | }, function (error) { | |
|
80 | d.find('.rename-message').text(error.message || 'Unknown error'); | |
|
81 | d.find('input[type="text"]').prop('disabled', false).focus().select(); | |
|
82 | } | |
|
83 | ); | |
|
84 | return false; | |
|
85 | } | |
|
86 | }, | |
|
87 | "Cancel": {} | |
|
88 | }, | |
|
89 | open : function () { | |
|
90 | // Upon ENTER, click the OK button. | |
|
91 | d.find('input[type="text"]').keydown(function (event) { | |
|
92 | if (event.which === keyboard.keycodes.enter) { | |
|
93 | d.find('.btn-primary').first().click(); | |
|
94 | return false; | |
|
95 | } | |
|
96 | }); | |
|
97 | d.find('input[type="text"]').focus().select(); | |
|
98 | } | |
|
99 | }); | |
|
100 | }; | |
|
101 | ||
|
102 | ||
|
103 | SaveWidget.prototype.update_filename = function (filename) { | |
|
104 | this.element.find('span.filename').text(filename); | |
|
105 | }; | |
|
106 | ||
|
107 | SaveWidget.prototype.update_document_title = function (filename) { | |
|
108 | document.title = filename; | |
|
109 | }; | |
|
110 | ||
|
111 | SaveWidget.prototype.update_address_bar = function (path) { | |
|
112 | var state = {path : path}; | |
|
113 | window.history.replaceState(state, "", utils.url_join_encode( | |
|
114 | this.editor.base_url, | |
|
115 | "edit", | |
|
116 | path) | |
|
117 | ); | |
|
118 | }; | |
|
119 | ||
|
120 | SaveWidget.prototype.update_last_modified = function (last_modified) { | |
|
121 | if (last_modified) { | |
|
122 | this._last_modified = new Date(last_modified); | |
|
123 | } else { | |
|
124 | this._last_modified = null; | |
|
125 | } | |
|
126 | this._render_last_modified(); | |
|
127 | }; | |
|
128 | ||
|
129 | SaveWidget.prototype._render_last_modified = function () { | |
|
130 | /** actually set the text in the element, from our _last_modified value | |
|
131 | ||
|
132 | called directly, and periodically in timeouts. | |
|
133 | */ | |
|
134 | this._schedule_render_last_modified(); | |
|
135 | var el = this.element.find('span.last_modified'); | |
|
136 | if (!this._last_modified) { | |
|
137 | el.text('').attr('title', 'never saved'); | |
|
138 | return; | |
|
139 | } | |
|
140 | var chkd = moment(this._last_modified); | |
|
141 | var long_date = chkd.format('llll'); | |
|
142 | var human_date; | |
|
143 | var tdelta = Math.ceil(new Date() - this._last_modified); | |
|
144 | if (tdelta < 24 * H){ | |
|
145 | // less than 24 hours old, use relative date | |
|
146 | human_date = chkd.fromNow(); | |
|
147 | } else { | |
|
148 | // otherwise show calendar | |
|
149 | // otherwise update every hour and show | |
|
150 | // <Today | yesterday|...> at hh,mm,ss | |
|
151 | human_date = chkd.calendar(); | |
|
152 | } | |
|
153 | el.text(human_date).attr('title', long_date); | |
|
154 | }; | |
|
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 | ||
|
179 | SaveWidget.prototype._schedule_render_last_modified = function () { | |
|
180 | /** schedule the next update to relative date | |
|
181 | ||
|
182 | periodically updated, so short values like 'a few seconds ago' don't get stale. | |
|
183 | */ | |
|
184 | var that = this; | |
|
185 | if (!this._last_modified) { | |
|
186 | return; | |
|
187 | } | |
|
188 | if ((this._last_modified_timeout)) { | |
|
189 | clearTimeout(this._last_modified_timeout); | |
|
190 | } | |
|
191 | var dt = Math.ceil(new Date() - this._last_modified); | |
|
192 | if (dt < 24 * H) { | |
|
193 | this._last_modified_timeout = setTimeout( | |
|
194 | $.proxy(this._render_last_modified, this), | |
|
195 | _timeout_from_dt(dt) | |
|
196 | ); | |
|
197 | } | |
|
198 | }; | |
|
199 | ||
|
200 | return {'SaveWidget': SaveWidget}; | |
|
201 | ||
|
202 | }); |
@@ -0,0 +1,9 b'' | |||
|
1 | #texteditor-container { | |
|
2 | border-bottom: 1px solid #ccc; | |
|
3 | } | |
|
4 | ||
|
5 | #filename { | |
|
6 | font-size: 16pt; | |
|
7 | display: table; | |
|
8 | padding: 0px 5px; | |
|
9 | } |
@@ -0,0 +1,14 b'' | |||
|
1 | .selected-keymap { | |
|
2 | i.fa { | |
|
3 | padding: 0px 5px; | |
|
4 | } | |
|
5 | i.fa:before { | |
|
6 | content: @fa-var-check; | |
|
7 | } | |
|
8 | } | |
|
9 | ||
|
10 | #mode-menu { | |
|
11 | // truncate mode-menu, so it doesn't get longer than the screen | |
|
12 | overflow: auto; | |
|
13 | max-height: 20em; | |
|
14 | } No newline at end of file |
@@ -0,0 +1,7 b'' | |||
|
1 | /*! | |
|
2 | * | |
|
3 | * IPython text editor webapp | |
|
4 | * | |
|
5 | */ | |
|
6 | @import "menubar.less"; | |
|
7 | @import "edit.less"; |
@@ -363,7 +363,8 b' def json_errors(method):' | |||
|
363 | 363 | message = e.log_message |
|
364 | 364 | self.log.warn(message) |
|
365 | 365 | self.set_status(e.status_code) |
|
366 |
|
|
|
366 | reply = dict(message=message, reason=e.reason) | |
|
367 | self.finish(json.dumps(reply)) | |
|
367 | 368 | except Exception: |
|
368 | 369 | self.log.error("Unhandled error in API request", exc_info=True) |
|
369 | 370 | status = 500 |
@@ -371,7 +372,7 b' def json_errors(method):' | |||
|
371 | 372 | t, value, tb = sys.exc_info() |
|
372 | 373 | self.set_status(status) |
|
373 | 374 | tb_text = ''.join(traceback.format_exception(t, value, tb)) |
|
374 | reply = dict(message=message, traceback=tb_text) | |
|
375 | reply = dict(message=message, reason=None, traceback=tb_text) | |
|
375 | 376 | self.finish(json.dumps(reply)) |
|
376 | 377 | else: |
|
377 | 378 | return result |
@@ -281,7 +281,7 b' class FileContentsManager(ContentsManager):' | |||
|
281 | 281 | model['content'] = bcontent.decode('utf8') |
|
282 | 282 | except UnicodeError as e: |
|
283 | 283 | if format == 'text': |
|
284 | raise web.HTTPError(400, "%s is not UTF-8 encoded" % path) | |
|
284 | raise web.HTTPError(400, "%s is not UTF-8 encoded" % path, reason='bad format') | |
|
285 | 285 | else: |
|
286 | 286 | model['format'] = 'text' |
|
287 | 287 | default_mime = 'text/plain' |
@@ -348,14 +348,14 b' class FileContentsManager(ContentsManager):' | |||
|
348 | 348 | if os.path.isdir(os_path): |
|
349 | 349 | if type_ not in (None, 'directory'): |
|
350 | 350 | raise web.HTTPError(400, |
|
351 | u'%s is a directory, not a %s' % (path, type_)) | |
|
351 | u'%s is a directory, not a %s' % (path, type_), reason='bad type') | |
|
352 | 352 | model = self._dir_model(path, content=content) |
|
353 | 353 | elif type_ == 'notebook' or (type_ is None and path.endswith('.ipynb')): |
|
354 | 354 | model = self._notebook_model(path, content=content) |
|
355 | 355 | else: |
|
356 | 356 | if type_ == 'directory': |
|
357 | 357 | raise web.HTTPError(400, |
|
358 | u'%s is not a directory') | |
|
358 | u'%s is not a directory', reason='bad type') | |
|
359 | 359 | model = self._file_model(path, content=content, format=format) |
|
360 | 360 | return model |
|
361 | 361 |
@@ -47,6 +47,7 b' class ContentsHandler(IPythonHandler):' | |||
|
47 | 47 | location = self.location_url(model['path']) |
|
48 | 48 | self.set_header('Location', location) |
|
49 | 49 | self.set_header('Last-Modified', model['last_modified']) |
|
50 | self.set_header('Content-Type', 'application/json') | |
|
50 | 51 | self.finish(json.dumps(model, default=date_default)) |
|
51 | 52 | |
|
52 | 53 | @web.authenticated |
@@ -6,20 +6,32 b' define([' | |||
|
6 | 6 | 'base/js/utils', |
|
7 | 7 | 'codemirror/lib/codemirror', |
|
8 | 8 | 'codemirror/mode/meta', |
|
9 |
'codemirror/addon/ |
|
|
9 | 'codemirror/addon/comment/comment', | |
|
10 | 'codemirror/addon/dialog/dialog', | |
|
11 | 'codemirror/addon/edit/closebrackets', | |
|
12 | 'codemirror/addon/edit/matchbrackets', | |
|
13 | 'codemirror/addon/search/searchcursor', | |
|
14 | 'codemirror/addon/search/search', | |
|
15 | 'codemirror/keymap/emacs', | |
|
16 | 'codemirror/keymap/sublime', | |
|
17 | 'codemirror/keymap/vim', | |
|
10 | 18 | ], |
|
11 | 19 | function($, |
|
12 | 20 | utils, |
|
13 | 21 | CodeMirror |
|
14 | 22 | ) { |
|
23 | "use strict"; | |
|
24 | ||
|
15 | 25 | var Editor = function(selector, options) { |
|
26 | var that = this; | |
|
16 | 27 | this.selector = selector; |
|
17 | 28 | this.contents = options.contents; |
|
18 | 29 | this.events = options.events; |
|
19 | 30 | this.base_url = options.base_url; |
|
20 | 31 | this.file_path = options.file_path; |
|
21 | ||
|
22 | this.codemirror = CodeMirror($(this.selector)[0]); | |
|
32 | this.config = options.config; | |
|
33 | this.codemirror = new CodeMirror($(this.selector)[0]); | |
|
34 | this.generation = -1; | |
|
23 | 35 | |
|
24 | 36 | // It appears we have to set commands on the CodeMirror class, not the |
|
25 | 37 | // instance. I'd like to be wrong, but since there should only be one CM |
@@ -27,27 +39,55 b' function($,' | |||
|
27 | 39 | CodeMirror.commands.save = $.proxy(this.save, this); |
|
28 | 40 | |
|
29 | 41 | this.save_enabled = false; |
|
42 | ||
|
43 | this.config.loaded.then(function () { | |
|
44 | // load codemirror config | |
|
45 | var cfg = that.config.data.Editor || {}; | |
|
46 | var cmopts = $.extend(true, {}, // true = recursive copy | |
|
47 | Editor.default_codemirror_options, | |
|
48 | cfg.codemirror_options || {} | |
|
49 | ); | |
|
50 | that._set_codemirror_options(cmopts); | |
|
51 | that.events.trigger('config_changed.Editor', {config: that.config}); | |
|
52 | }); | |
|
53 | }; | |
|
54 | ||
|
55 | // default CodeMirror options | |
|
56 | Editor.default_codemirror_options = { | |
|
57 | extraKeys: { | |
|
58 | "Tab" : "indentMore", | |
|
59 | }, | |
|
60 | indentUnit: 4, | |
|
61 | theme: "ipython", | |
|
62 | lineNumbers: true, | |
|
63 | lineWrapping: true, | |
|
30 | 64 | }; |
|
31 | 65 | |
|
32 | 66 | Editor.prototype.load = function() { |
|
67 | /** load the file */ | |
|
33 | 68 | var that = this; |
|
34 | 69 | var cm = this.codemirror; |
|
35 | this.contents.get(this.file_path, {type: 'file', format: 'text'}) | |
|
70 | return this.contents.get(this.file_path, {type: 'file', format: 'text'}) | |
|
36 | 71 | .then(function(model) { |
|
37 | 72 | cm.setValue(model.content); |
|
38 | 73 | |
|
39 | 74 | // Setting the file's initial value creates a history entry, |
|
40 | 75 | // which we don't want. |
|
41 | 76 | cm.clearHistory(); |
|
42 | ||
|
43 | // Find and load the highlighting mode | |
|
44 | utils.requireCodeMirrorMode(model.mimetype, function(spec) { | |
|
45 | var mode = CodeMirror.getMode({}, spec); | |
|
46 | cm.setOption('mode', mode); | |
|
47 | }); | |
|
77 | that._set_mode_for_model(model); | |
|
48 | 78 | that.save_enabled = true; |
|
79 | that.generation = cm.changeGeneration(); | |
|
80 | that.events.trigger("file_loaded.Editor", model); | |
|
49 | 81 | }, |
|
50 | 82 | function(error) { |
|
83 | that.events.trigger("file_load_failed.Editor", error); | |
|
84 | if (error.xhr.responseJSON.reason === 'bad format') { | |
|
85 | window.location = utils.url_path_join( | |
|
86 | that.base_url, | |
|
87 | 'files', | |
|
88 | that.file_path | |
|
89 | ); | |
|
90 | } | |
|
51 | 91 | cm.setValue("Error! " + error.message + |
|
52 | 92 | "\nSaving disabled."); |
|
53 | 93 | that.save_enabled = false; |
@@ -55,7 +95,55 b' function($,' | |||
|
55 | 95 | ); |
|
56 | 96 | }; |
|
57 | 97 | |
|
58 |
Editor.prototype. |
|
|
98 | Editor.prototype._set_mode_for_model = function (model) { | |
|
99 | /** Set the CodeMirror mode based on the file model */ | |
|
100 | ||
|
101 | // Find and load the highlighting mode, | |
|
102 | // first by mime-type, then by file extension | |
|
103 | var modeinfo = CodeMirror.findModeByMIME(model.mimetype); | |
|
104 | if (modeinfo.mode === "null") { | |
|
105 | // find by mime failed, use find by ext | |
|
106 | var ext_idx = model.name.lastIndexOf('.'); | |
|
107 | ||
|
108 | if (ext_idx > 0) { | |
|
109 | // CodeMirror.findModeByExtension wants extension without '.' | |
|
110 | modeinfo = CodeMirror.findModeByExtension(model.name.slice(ext_idx + 1)); | |
|
111 | } | |
|
112 | } | |
|
113 | if (modeinfo) { | |
|
114 | this.set_codemirror_mode(modeinfo); | |
|
115 | } | |
|
116 | }; | |
|
117 | ||
|
118 | Editor.prototype.set_codemirror_mode = function (modeinfo) { | |
|
119 | /** set the codemirror mode from a modeinfo struct */ | |
|
120 | var that = this; | |
|
121 | utils.requireCodeMirrorMode(modeinfo, function () { | |
|
122 | that.codemirror.setOption('mode', modeinfo.mode); | |
|
123 | that.events.trigger("mode_changed.Editor", modeinfo); | |
|
124 | }); | |
|
125 | }; | |
|
126 | ||
|
127 | Editor.prototype.get_filename = function () { | |
|
128 | return utils.url_path_split(this.file_path)[1]; | |
|
129 | }; | |
|
130 | ||
|
131 | Editor.prototype.rename = function (new_name) { | |
|
132 | /** rename the file */ | |
|
133 | var that = this; | |
|
134 | var parent = utils.url_path_split(this.file_path)[0]; | |
|
135 | var new_path = utils.url_path_join(parent, new_name); | |
|
136 | return this.contents.rename(this.file_path, new_path).then( | |
|
137 | function (model) { | |
|
138 | that.file_path = model.path; | |
|
139 | that.events.trigger('file_renamed.Editor', model); | |
|
140 | that._set_mode_for_model(model); | |
|
141 | } | |
|
142 | ); | |
|
143 | }; | |
|
144 | ||
|
145 | Editor.prototype.save = function () { | |
|
146 | /** save the file */ | |
|
59 | 147 | if (!this.save_enabled) { |
|
60 | 148 | console.log("Not saving, save disabled"); |
|
61 | 149 | return; |
@@ -67,10 +155,36 b' function($,' | |||
|
67 | 155 | content: this.codemirror.getValue(), |
|
68 | 156 | }; |
|
69 | 157 | var that = this; |
|
70 | this.contents.save(this.file_path, model).then(function() { | |
|
71 | that.events.trigger("save_succeeded.TextEditor"); | |
|
158 | // record change generation for isClean | |
|
159 | this.generation = this.codemirror.changeGeneration(); | |
|
160 | return this.contents.save(this.file_path, model).then(function(data) { | |
|
161 | that.events.trigger("file_saved.Editor", data); | |
|
72 | 162 | }); |
|
73 | 163 | }; |
|
164 | ||
|
165 | Editor.prototype._set_codemirror_options = function (options) { | |
|
166 | // update codemirror options from a dict | |
|
167 | var codemirror = this.codemirror; | |
|
168 | $.map(options, function (value, opt) { | |
|
169 | if (value === null) { | |
|
170 | value = CodeMirror.defaults[opt]; | |
|
171 | } | |
|
172 | codemirror.setOption(opt, value); | |
|
173 | }); | |
|
174 | }; | |
|
175 | ||
|
176 | Editor.prototype.update_codemirror_options = function (options) { | |
|
177 | /** update codemirror options locally and save changes in config */ | |
|
178 | var that = this; | |
|
179 | this._set_codemirror_options(options); | |
|
180 | return this.config.update({ | |
|
181 | Editor: { | |
|
182 | codemirror_options: options | |
|
183 | } | |
|
184 | }).then( | |
|
185 | that.events.trigger('config_changed.Editor', {config: that.config}) | |
|
186 | ); | |
|
187 | }; | |
|
74 | 188 | |
|
75 | 189 | return {Editor: Editor}; |
|
76 | 190 | }); |
@@ -10,6 +10,7 b' require([' | |||
|
10 | 10 | 'services/config', |
|
11 | 11 | 'edit/js/editor', |
|
12 | 12 | 'edit/js/menubar', |
|
13 | 'edit/js/savewidget', | |
|
13 | 14 | 'edit/js/notificationarea', |
|
14 | 15 | 'custom/custom', |
|
15 | 16 | ], function( |
@@ -19,8 +20,9 b' require([' | |||
|
19 | 20 | events, |
|
20 | 21 | contents, |
|
21 | 22 | configmod, |
|
22 |
edit |
|
|
23 | editmod, | |
|
23 | 24 | menubar, |
|
25 | savewidget, | |
|
24 | 26 | notificationarea |
|
25 | 27 | ){ |
|
26 | 28 | page = new page.Page(); |
@@ -28,22 +30,30 b' require([' | |||
|
28 | 30 | var base_url = utils.get_body_data('baseUrl'); |
|
29 | 31 | var file_path = utils.get_body_data('filePath'); |
|
30 | 32 | contents = new contents.Contents({base_url: base_url}); |
|
31 | var config = new configmod.ConfigSection('edit', {base_url: base_url}) | |
|
33 | var config = new configmod.ConfigSection('edit', {base_url: base_url}); | |
|
32 | 34 | config.load(); |
|
33 | 35 | |
|
34 |
var editor = new edit |
|
|
36 | var editor = new editmod.Editor('#texteditor-container', { | |
|
35 | 37 | base_url: base_url, |
|
36 | 38 | events: events, |
|
37 | 39 | contents: contents, |
|
38 | 40 | file_path: file_path, |
|
41 | config: config, | |
|
39 | 42 | }); |
|
40 | 43 | |
|
41 | 44 | // Make it available for debugging |
|
42 | 45 | IPython.editor = editor; |
|
43 | 46 | |
|
47 | var save_widget = new savewidget.SaveWidget('span#save_widget', { | |
|
48 | editor: editor, | |
|
49 | events: events, | |
|
50 | }); | |
|
51 | ||
|
44 | 52 | var menus = new menubar.MenuBar('#menubar', { |
|
45 | 53 | base_url: base_url, |
|
46 | 54 | editor: editor, |
|
55 | events: events, | |
|
56 | save_widget: save_widget, | |
|
47 | 57 | }); |
|
48 | 58 | |
|
49 | 59 | var notification_area = new notificationarea.EditorNotificationArea( |
@@ -61,4 +71,11 b' require([' | |||
|
61 | 71 | }); |
|
62 | 72 | editor.load(); |
|
63 | 73 | page.show(); |
|
74 | ||
|
75 | window.onbeforeunload = function () { | |
|
76 | if (editor.save_enabled && !editor.codemirror.isClean(editor.generation)) { | |
|
77 | return "Unsaved changes will be lost. Close anyway?"; | |
|
78 | } | |
|
79 | }; | |
|
80 | ||
|
64 | 81 | }); |
@@ -2,11 +2,14 b'' | |||
|
2 | 2 | // Distributed under the terms of the Modified BSD License. |
|
3 | 3 | |
|
4 | 4 | define([ |
|
5 | 'base/js/namespace', | |
|
6 | 5 | 'jquery', |
|
6 | 'base/js/namespace', | |
|
7 | 7 | 'base/js/utils', |
|
8 | 'base/js/dialog', | |
|
9 | 'codemirror/lib/codemirror', | |
|
10 | 'codemirror/mode/meta', | |
|
8 | 11 | 'bootstrap', |
|
9 |
], function( |
|
|
12 | ], function($, IPython, utils, dialog, CodeMirror) { | |
|
10 | 13 | "use strict"; |
|
11 | 14 | |
|
12 | 15 | var MenuBar = function (selector, options) { |
@@ -29,21 +32,123 b' define([' | |||
|
29 | 32 | this.base_url = options.base_url || utils.get_body_data("baseUrl"); |
|
30 | 33 | this.selector = selector; |
|
31 | 34 | this.editor = options.editor; |
|
35 | this.events = options.events; | |
|
36 | this.save_widget = options.save_widget; | |
|
32 | 37 | |
|
33 | 38 | if (this.selector !== undefined) { |
|
34 | 39 | this.element = $(selector); |
|
35 | 40 | this.bind_events(); |
|
36 | 41 | } |
|
42 | this._load_mode_menu(); | |
|
43 | Object.seal(this); | |
|
37 | 44 | }; |
|
38 | 45 | |
|
39 | 46 | MenuBar.prototype.bind_events = function () { |
|
40 | /** | |
|
41 | * File | |
|
42 | */ | |
|
43 | 47 | var that = this; |
|
44 | this.element.find('#save_file').click(function () { | |
|
45 | that.editor.save(); | |
|
48 | var editor = that.editor; | |
|
49 | ||
|
50 | // File | |
|
51 | this.element.find('#new-file').click(function () { | |
|
52 | var w = window.open(); | |
|
53 | // Create a new file in the current directory | |
|
54 | var parent = utils.url_path_split(editor.file_path)[0]; | |
|
55 | editor.contents.new_untitled(parent, {type: "file"}).then( | |
|
56 | function (data) { | |
|
57 | w.location = utils.url_join_encode( | |
|
58 | that.base_url, 'edit', data.path | |
|
59 | ); | |
|
60 | }, | |
|
61 | function(error) { | |
|
62 | w.close(); | |
|
63 | dialog.modal({ | |
|
64 | title : 'Creating New File Failed', | |
|
65 | body : "The error was: " + error.message, | |
|
66 | buttons : {'OK' : {'class' : 'btn-primary'}} | |
|
67 | }); | |
|
68 | } | |
|
69 | ); | |
|
70 | }); | |
|
71 | this.element.find('#save-file').click(function () { | |
|
72 | editor.save(); | |
|
73 | }); | |
|
74 | this.element.find('#rename-file').click(function () { | |
|
75 | that.save_widget.rename(); | |
|
76 | }); | |
|
77 | ||
|
78 | // Edit | |
|
79 | this.element.find('#menu-find').click(function () { | |
|
80 | editor.codemirror.execCommand("find"); | |
|
81 | }); | |
|
82 | this.element.find('#menu-replace').click(function () { | |
|
83 | editor.codemirror.execCommand("replace"); | |
|
84 | }); | |
|
85 | this.element.find('#menu-keymap-default').click(function () { | |
|
86 | editor.update_codemirror_options({ | |
|
87 | vimMode: false, | |
|
88 | keyMap: 'default' | |
|
89 | }); | |
|
90 | }); | |
|
91 | this.element.find('#menu-keymap-sublime').click(function () { | |
|
92 | editor.update_codemirror_options({ | |
|
93 | vimMode: false, | |
|
94 | keyMap: 'sublime' | |
|
95 | }); | |
|
46 | 96 | }); |
|
97 | this.element.find('#menu-keymap-emacs').click(function () { | |
|
98 | editor.update_codemirror_options({ | |
|
99 | vimMode: false, | |
|
100 | keyMap: 'emacs' | |
|
101 | }); | |
|
102 | }); | |
|
103 | this.element.find('#menu-keymap-vim').click(function () { | |
|
104 | editor.update_codemirror_options({ | |
|
105 | vimMode: true, | |
|
106 | keyMap: 'vim' | |
|
107 | }); | |
|
108 | }); | |
|
109 | ||
|
110 | // View | |
|
111 | this.element.find('#menu-line-numbers').click(function () { | |
|
112 | var current = editor.codemirror.getOption('lineNumbers'); | |
|
113 | var value = Boolean(1-current); | |
|
114 | editor.update_codemirror_options({lineNumbers: value}); | |
|
115 | }); | |
|
116 | ||
|
117 | this.events.on("config_changed.Editor", function () { | |
|
118 | var keyMap = editor.codemirror.getOption('keyMap') || "default"; | |
|
119 | that.element.find(".selected-keymap").removeClass("selected-keymap"); | |
|
120 | that.element.find("#menu-keymap-" + keyMap).addClass("selected-keymap"); | |
|
121 | }); | |
|
122 | ||
|
123 | this.events.on("mode_changed.Editor", function (evt, modeinfo) { | |
|
124 | that.element.find("#current-mode") | |
|
125 | .text(modeinfo.name) | |
|
126 | .attr( | |
|
127 | 'title', | |
|
128 | "The current language is " + modeinfo.name | |
|
129 | ); | |
|
130 | }); | |
|
131 | }; | |
|
132 | ||
|
133 | MenuBar.prototype._load_mode_menu = function () { | |
|
134 | var list = this.element.find("#mode-menu"); | |
|
135 | var editor = this.editor; | |
|
136 | function make_set_mode(info) { | |
|
137 | return function () { | |
|
138 | editor.set_codemirror_mode(info); | |
|
139 | }; | |
|
140 | } | |
|
141 | for (var i = 0; i < CodeMirror.modeInfo.length; i++) { | |
|
142 | var info = CodeMirror.modeInfo[i]; | |
|
143 | list.append($("<li>").append( | |
|
144 | $("<a>").attr("href", "#") | |
|
145 | .text(info.name) | |
|
146 | .click(make_set_mode(info)) | |
|
147 | .attr('title', | |
|
148 | "Set language to " + info.name | |
|
149 | ) | |
|
150 | )); | |
|
151 | } | |
|
47 | 152 | }; |
|
48 | 153 | |
|
49 | 154 | return {'MenuBar': MenuBar}; |
@@ -29,7 +29,7 b' define([' | |||
|
29 | 29 | |
|
30 | 30 | SaveWidget.prototype.bind_events = function () { |
|
31 | 31 | var that = this; |
|
32 |
this.element.find('span |
|
|
32 | this.element.find('span.filename').click(function () { | |
|
33 | 33 | that.rename_notebook({notebook: that.notebook}); |
|
34 | 34 | }); |
|
35 | 35 | this.events.on('notebook_loaded.Notebook', function () { |
@@ -130,7 +130,7 b' define([' | |||
|
130 | 130 | |
|
131 | 131 | SaveWidget.prototype.update_notebook_name = function () { |
|
132 | 132 | var nbname = this.notebook.get_notebook_name(); |
|
133 |
this.element.find('span |
|
|
133 | this.element.find('span.filename').text(nbname); | |
|
134 | 134 | }; |
|
135 | 135 | |
|
136 | 136 | |
@@ -152,11 +152,11 b' define([' | |||
|
152 | 152 | |
|
153 | 153 | |
|
154 | 154 | SaveWidget.prototype.set_save_status = function (msg) { |
|
155 |
this.element.find('span |
|
|
155 | this.element.find('span.autosave_status').text(msg); | |
|
156 | 156 | }; |
|
157 | 157 | |
|
158 | 158 | SaveWidget.prototype._set_checkpoint_status = function (human_date, iso_date) { |
|
159 |
var el = this.element.find('span |
|
|
159 | var el = this.element.find('span.checkpoint_status'); | |
|
160 | 160 | if(human_date){ |
|
161 | 161 | el.text("Last Checkpoint: "+human_date).attr('title',iso_date); |
|
162 | 162 | } else { |
@@ -223,7 +223,7 b' define([' | |||
|
223 | 223 | |
|
224 | 224 | // update regularly for the first 6hours and show |
|
225 | 225 | // <x time> ago |
|
226 |
if( |
|
|
226 | if(tdelta < 6*3600*1000){ | |
|
227 | 227 | recall(_next_timeago_update(tdelta)); |
|
228 | 228 | this._set_checkpoint_status(chkd.fromNow(), longdate); |
|
229 | 229 | // otherwise update every hour and show |
@@ -15,20 +15,6 b' body {' | |||
|
15 | 15 | .border-box-sizing(); |
|
16 | 16 | } |
|
17 | 17 | |
|
18 | span#notebook_name { | |
|
19 | height: 1em; | |
|
20 | line-height: 1em; | |
|
21 | padding: 3px; | |
|
22 | border: none; | |
|
23 | font-size: 146.5%; | |
|
24 | &:hover{ | |
|
25 | // ensure body is lighter on dark palette, | |
|
26 | // and vice versa | |
|
27 | background-color:contrast(@body-bg, lighten(@body-bg,30%), darken(@body-bg,10%)); | |
|
28 | } | |
|
29 | .corner-all; | |
|
30 | } | |
|
31 | ||
|
32 | 18 | div#notebook_panel { |
|
33 | 19 | margin: 0px 0px 0px 0px; |
|
34 | 20 | padding: 0px; |
@@ -1,33 +1,41 b'' | |||
|
1 |
span |
|
|
1 | span.save_widget { | |
|
2 | 2 | margin-top: 6px; |
|
3 | ||
|
4 | span.filename { | |
|
5 | height: 1em; | |
|
6 | line-height: 1em; | |
|
7 | padding: 3px; | |
|
8 | border: none; | |
|
9 | font-size: 146.5%; | |
|
10 | &:hover{ | |
|
11 | // ensure body is lighter on dark palette, | |
|
12 | // and vice versa | |
|
13 | background-color:contrast(@body-bg, lighten(@body-bg,30%), darken(@body-bg,10%)); | |
|
14 | } | |
|
15 | .corner-all; | |
|
16 | } | |
|
3 | 17 | } |
|
4 | 18 | |
|
5 |
span |
|
|
19 | span.checkpoint_status, span.autosave_status { | |
|
6 | 20 | font-size: small; |
|
7 | 21 | } |
|
8 | 22 | |
|
9 | 23 | @media (max-width: 767px) { |
|
10 |
span |
|
|
24 | span.save_widget { | |
|
11 | 25 | font-size: small; |
|
12 | 26 | } |
|
13 |
span |
|
|
14 | font-size: x-small; | |
|
15 | } | |
|
16 | } | |
|
17 | ||
|
18 | @media (max-width: 767px) { | |
|
19 | span#checkpoint_status, span#autosave_status { | |
|
20 | display: none; | |
|
27 | span.checkpoint_status, span.autosave_status { | |
|
28 | display: none; | |
|
21 | 29 | } |
|
22 | 30 | } |
|
23 | 31 | |
|
24 | 32 | @media (min-width: 768px) and (max-width: 979px) { |
|
25 |
span |
|
|
33 | span.checkpoint_status { | |
|
26 | 34 | display: none; |
|
27 | 35 | } |
|
28 |
span |
|
|
36 | span.autosave_status { | |
|
29 | 37 | font-size: x-small; |
|
30 | 38 | } |
|
31 | 39 | } |
|
32 | 40 | |
|
33 | ||
|
41 |
@@ -162,6 +162,7 b' define([' | |||
|
162 | 162 | var settings = { |
|
163 | 163 | processData : false, |
|
164 | 164 | type : "PUT", |
|
165 | dataType: "json", | |
|
165 | 166 | data : JSON.stringify(model), |
|
166 | 167 | contentType: 'application/json', |
|
167 | 168 | }; |
@@ -23,6 +23,9 b'' | |||
|
23 | 23 | // tree |
|
24 | 24 | @import "../tree/less/style.less"; |
|
25 | 25 | |
|
26 | // edit | |
|
27 | @import "../edit/less/style.less"; | |
|
28 | ||
|
26 | 29 | // notebook |
|
27 | 30 | @import "../notebook/less/style.less"; |
|
28 | 31 |
@@ -7752,9 +7752,6 b' div#header {' | |||
|
7752 | 7752 | /* Initially hidden to prevent FLOUC */ |
|
7753 | 7753 | display: none; |
|
7754 | 7754 | background-color: #ffffff; |
|
7755 | box-sizing: border-box; | |
|
7756 | -moz-box-sizing: border-box; | |
|
7757 | -webkit-box-sizing: border-box; | |
|
7758 | 7755 | /* Display over codemirror */ |
|
7759 | 7756 | z-index: 100; |
|
7760 | 7757 | } |
@@ -8119,6 +8116,29 b' ul#new-notebook-menu {' | |||
|
8119 | 8116 | } |
|
8120 | 8117 | /*! |
|
8121 | 8118 | * |
|
8119 | * IPython text editor webapp | |
|
8120 | * | |
|
8121 | */ | |
|
8122 | .selected-keymap i.fa { | |
|
8123 | padding: 0px 5px; | |
|
8124 | } | |
|
8125 | .selected-keymap i.fa:before { | |
|
8126 | content: "\f00c"; | |
|
8127 | } | |
|
8128 | #mode-menu { | |
|
8129 | overflow: auto; | |
|
8130 | max-height: 20em; | |
|
8131 | } | |
|
8132 | #texteditor-container { | |
|
8133 | border-bottom: 1px solid #ccc; | |
|
8134 | } | |
|
8135 | #filename { | |
|
8136 | font-size: 16pt; | |
|
8137 | display: table; | |
|
8138 | padding: 0px 5px; | |
|
8139 | } | |
|
8140 | /*! | |
|
8141 | * | |
|
8122 | 8142 | * IPython notebook |
|
8123 | 8143 | * |
|
8124 | 8144 | */ |
@@ -9421,17 +9441,6 b' body {' | |||
|
9421 | 9441 | -moz-box-sizing: border-box; |
|
9422 | 9442 | -webkit-box-sizing: border-box; |
|
9423 | 9443 | } |
|
9424 | span#notebook_name { | |
|
9425 | height: 1em; | |
|
9426 | line-height: 1em; | |
|
9427 | padding: 3px; | |
|
9428 | border: none; | |
|
9429 | font-size: 146.5%; | |
|
9430 | border-radius: 4px; | |
|
9431 | } | |
|
9432 | span#notebook_name:hover { | |
|
9433 | background-color: #e6e6e6; | |
|
9434 | } | |
|
9435 | 9444 | div#notebook_panel { |
|
9436 | 9445 | margin: 0px 0px 0px 0px; |
|
9437 | 9446 | padding: 0px; |
@@ -9681,7 +9690,6 b' fieldset[disabled] #kernel_selector_widget > button.active {' | |||
|
9681 | 9690 | margin-top: 0px; |
|
9682 | 9691 | } |
|
9683 | 9692 | #menubar { |
|
9684 | margin-top: 0px; | |
|
9685 | 9693 | box-sizing: border-box; |
|
9686 | 9694 | -moz-box-sizing: border-box; |
|
9687 | 9695 | -webkit-box-sizing: border-box; |
@@ -10159,33 +10167,38 b' div#pager .ui-resizable-handle {' | |||
|
10159 | 10167 | /* Modern browsers */ |
|
10160 | 10168 | flex: 1; |
|
10161 | 10169 | } |
|
10162 |
span |
|
|
10170 | span.save_widget { | |
|
10163 | 10171 | margin-top: 6px; |
|
10164 | 10172 | } |
|
10165 | span#checkpoint_status, | |
|
10166 | span#autosave_status { | |
|
10173 | span.save_widget span.filename { | |
|
10174 | height: 1em; | |
|
10175 | line-height: 1em; | |
|
10176 | padding: 3px; | |
|
10177 | border: none; | |
|
10178 | font-size: 146.5%; | |
|
10179 | border-radius: 4px; | |
|
10180 | } | |
|
10181 | span.save_widget span.filename:hover { | |
|
10182 | background-color: #e6e6e6; | |
|
10183 | } | |
|
10184 | span.checkpoint_status, | |
|
10185 | span.autosave_status { | |
|
10167 | 10186 | font-size: small; |
|
10168 | 10187 | } |
|
10169 | 10188 | @media (max-width: 767px) { |
|
10170 |
span |
|
|
10189 | span.save_widget { | |
|
10171 | 10190 | font-size: small; |
|
10172 | 10191 | } |
|
10173 |
span |
|
|
10174 |
span |
|
|
10175 | font-size: x-small; | |
|
10176 | } | |
|
10177 | } | |
|
10178 | @media (max-width: 767px) { | |
|
10179 | span#checkpoint_status, | |
|
10180 | span#autosave_status { | |
|
10192 | span.checkpoint_status, | |
|
10193 | span.autosave_status { | |
|
10181 | 10194 | display: none; |
|
10182 | 10195 | } |
|
10183 | 10196 | } |
|
10184 | 10197 | @media (min-width: 768px) and (max-width: 979px) { |
|
10185 |
span |
|
|
10198 | span.checkpoint_status { | |
|
10186 | 10199 | display: none; |
|
10187 | 10200 | } |
|
10188 |
span |
|
|
10201 | span.autosave_status { | |
|
10189 | 10202 | font-size: x-small; |
|
10190 | 10203 | } |
|
10191 | 10204 | } |
@@ -229,7 +229,7 b' define([' | |||
|
229 | 229 | NotebookList.uri_prefixes = { |
|
230 | 230 | directory: 'tree', |
|
231 | 231 | notebook: 'notebooks', |
|
232 |
file: ' |
|
|
232 | file: 'edit', | |
|
233 | 233 | }; |
|
234 | 234 | |
|
235 | 235 |
@@ -5,18 +5,6 b'' | |||
|
5 | 5 | {% block stylesheet %} |
|
6 | 6 | <link rel="stylesheet" href="{{ static_url('components/codemirror/lib/codemirror.css') }}"> |
|
7 | 7 | <link rel="stylesheet" href="{{ static_url('components/codemirror/addon/dialog/dialog.css') }}"> |
|
8 | <style> | |
|
9 | #texteditor-container { | |
|
10 | border-bottom: 1px solid #ccc; | |
|
11 | } | |
|
12 | ||
|
13 | #filename { | |
|
14 | font-size: 16pt; | |
|
15 | display: table; | |
|
16 | padding: 0px 5px; | |
|
17 | } | |
|
18 | </style> | |
|
19 | ||
|
20 | 8 | {{super()}} |
|
21 | 9 | {% endblock %} |
|
22 | 10 | |
@@ -27,38 +15,70 b' data-file-path="{{file_path}}"' | |||
|
27 | 15 | |
|
28 | 16 | {% endblock %} |
|
29 | 17 | |
|
30 | {% block header %} | |
|
18 | {% block headercontainer %} | |
|
31 | 19 | |
|
32 | <span id="filename">{{ basename }}</span> | |
|
20 | <span id="save_widget" class="pull-left save_widget"> | |
|
21 | <span class="filename"></span> | |
|
22 | <span class="last_modified"></span> | |
|
23 | </span> | |
|
33 | 24 | |
|
34 | 25 | {% endblock %} |
|
35 | 26 | |
|
36 |
{% block |
|
|
27 | {% block header %} | |
|
37 | 28 | |
|
38 | 29 | <div id="menubar-container" class="container"> |
|
39 | <div id="menubar"> | |
|
30 | <div id="menubar"> | |
|
40 | 31 | <div id="menus" class="navbar navbar-default" role="navigation"> |
|
41 |
|
|
|
42 |
|
|
|
43 |
|
|
|
44 |
|
|
|
45 |
|
|
|
46 |
|
|
|
47 |
|
|
|
48 |
|
|
|
49 |
|
|
|
50 |
|
|
|
51 |
|
|
|
52 |
|
|
|
53 |
|
|
|
54 | </ul> | |
|
55 | </li> | |
|
32 | <div class="container-fluid"> | |
|
33 | <button type="button" class="btn btn-default navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> | |
|
34 | <i class="fa fa-bars"></i> | |
|
35 | <span class="navbar-text">Menu</span> | |
|
36 | </button> | |
|
37 | <ul class="nav navbar-nav navbar-right"> | |
|
38 | <li id="notification_area"></li> | |
|
39 | </ul> | |
|
40 | <div class="navbar-collapse collapse"> | |
|
41 | <ul class="nav navbar-nav"> | |
|
42 | <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a> | |
|
43 | <ul id="file-menu" class="dropdown-menu"> | |
|
44 | <li id="new-file"><a href="#">New</a></li> | |
|
45 | <li id="save-file"><a href="#">Save</a></li> | |
|
46 | <li id="rename-file"><a href="#">Rename</a></li> | |
|
47 | </ul> | |
|
48 | </li> | |
|
49 | <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit</a> | |
|
50 | <ul id="edit-menu" class="dropdown-menu"> | |
|
51 | <li id="menu-find"><a href="#">Find</a></li> | |
|
52 | <li id="menu-replace"><a href="#">Find & Replace</a></li> | |
|
53 | <li class="divider"></li> | |
|
54 | <li class="dropdown-header">Key Map</li> | |
|
55 | <li id="menu-keymap-default"><a href="#">Default<i class="fa"></i></a></li> | |
|
56 | <li id="menu-keymap-sublime"><a href="#">Sublime Text<i class="fa"></i></a></li> | |
|
57 | <li id="menu-keymap-vim"><a href="#">Vim<i class="fa"></i></a></li> | |
|
58 | <li id="menu-keymap-emacs"><a href="#">emacs<i class="fa"></i></a></li> | |
|
59 | </ul> | |
|
60 | </li> | |
|
61 | <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a> | |
|
62 | <ul id="view-menu" class="dropdown-menu"> | |
|
63 | <li id="menu-line-numbers"><a href="#">Toggle Line Numbers</a></li> | |
|
64 | </ul> | |
|
65 | </li> | |
|
66 | <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Language</a> | |
|
67 | <ul id="mode-menu" class="dropdown-menu"> | |
|
56 | 68 | </ul> |
|
57 |
</ |
|
|
69 | </li> | |
|
70 | </ul> | |
|
71 | <p id="current-mode" class="navbar-text navbar-right">current mode</p> | |
|
58 | 72 | </div> |
|
73 | </div> | |
|
59 | 74 | </div> |
|
75 | </div> | |
|
60 | 76 | </div> |
|
61 | </div> | |
|
77 | ||
|
78 | {% endblock %} | |
|
79 | ||
|
80 | {% block site %} | |
|
81 | ||
|
62 | 82 | |
|
63 | 83 | <div id="texteditor-container" class="container"></div> |
|
64 | 84 |
@@ -35,10 +35,10 b' class="notebook_app"' | |||
|
35 | 35 | {% block headercontainer %} |
|
36 | 36 | |
|
37 | 37 | |
|
38 |
<span id="save_widget" class=" |
|
|
39 |
<span |
|
|
40 |
<span |
|
|
41 |
<span |
|
|
38 | <span id="save_widget" class="pull-left save_widget"> | |
|
39 | <span class="filename"></span> | |
|
40 | <span class="checkpoint_status"></span> | |
|
41 | <span class="autosave_status"></span> | |
|
42 | 42 | </span> |
|
43 | 43 | |
|
44 | 44 | <span id="kernel_selector_widget" class="pull-right dropdown"> |
General Comments 0
You need to be logged in to leave comments.
Login now