diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index 70a1b13..14e5487 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -363,7 +363,8 @@ def json_errors(method): message = e.log_message self.log.warn(message) self.set_status(e.status_code) - self.finish(json.dumps(dict(message=message))) + reply = dict(message=message, reason=e.reason) + self.finish(json.dumps(reply)) except Exception: self.log.error("Unhandled error in API request", exc_info=True) status = 500 @@ -371,7 +372,7 @@ def json_errors(method): t, value, tb = sys.exc_info() self.set_status(status) tb_text = ''.join(traceback.format_exception(t, value, tb)) - reply = dict(message=message, traceback=tb_text) + reply = dict(message=message, reason=None, traceback=tb_text) self.finish(json.dumps(reply)) else: return result diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py index 51e0d85..707ca93 100644 --- a/IPython/html/services/contents/filemanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -281,7 +281,7 @@ class FileContentsManager(ContentsManager): model['content'] = bcontent.decode('utf8') except UnicodeError as e: if format == 'text': - raise web.HTTPError(400, "%s is not UTF-8 encoded" % path) + raise web.HTTPError(400, "%s is not UTF-8 encoded" % path, reason='bad format') else: model['format'] = 'text' default_mime = 'text/plain' @@ -348,14 +348,14 @@ class FileContentsManager(ContentsManager): if os.path.isdir(os_path): if type_ not in (None, 'directory'): raise web.HTTPError(400, - u'%s is a directory, not a %s' % (path, type_)) + u'%s is a directory, not a %s' % (path, type_), reason='bad type') model = self._dir_model(path, content=content) elif type_ == 'notebook' or (type_ is None and path.endswith('.ipynb')): model = self._notebook_model(path, content=content) else: if type_ == 'directory': raise web.HTTPError(400, - u'%s is not a directory') + u'%s is not a directory', reason='bad type') model = self._file_model(path, content=content, format=format) return model diff --git a/IPython/html/services/contents/handlers.py b/IPython/html/services/contents/handlers.py index 121e325..a03a2e2 100644 --- a/IPython/html/services/contents/handlers.py +++ b/IPython/html/services/contents/handlers.py @@ -47,6 +47,7 @@ class ContentsHandler(IPythonHandler): location = self.location_url(model['path']) self.set_header('Location', location) self.set_header('Last-Modified', model['last_modified']) + self.set_header('Content-Type', 'application/json') self.finish(json.dumps(model, default=date_default)) @web.authenticated diff --git a/IPython/html/static/edit/js/editor.js b/IPython/html/static/edit/js/editor.js index 53320b6..319b472 100644 --- a/IPython/html/static/edit/js/editor.js +++ b/IPython/html/static/edit/js/editor.js @@ -6,20 +6,32 @@ define([ 'base/js/utils', 'codemirror/lib/codemirror', 'codemirror/mode/meta', - 'codemirror/addon/search/search' + 'codemirror/addon/comment/comment', + 'codemirror/addon/dialog/dialog', + 'codemirror/addon/edit/closebrackets', + 'codemirror/addon/edit/matchbrackets', + 'codemirror/addon/search/searchcursor', + 'codemirror/addon/search/search', + 'codemirror/keymap/emacs', + 'codemirror/keymap/sublime', + 'codemirror/keymap/vim', ], function($, utils, CodeMirror ) { + "use strict"; + var Editor = function(selector, options) { + var that = this; this.selector = selector; this.contents = options.contents; this.events = options.events; this.base_url = options.base_url; this.file_path = options.file_path; - - this.codemirror = CodeMirror($(this.selector)[0]); + this.config = options.config; + this.codemirror = new CodeMirror($(this.selector)[0]); + this.generation = -1; // It appears we have to set commands on the CodeMirror class, not the // instance. I'd like to be wrong, but since there should only be one CM @@ -27,27 +39,55 @@ function($, CodeMirror.commands.save = $.proxy(this.save, this); this.save_enabled = false; + + this.config.loaded.then(function () { + // load codemirror config + var cfg = that.config.data.Editor || {}; + var cmopts = $.extend(true, {}, // true = recursive copy + Editor.default_codemirror_options, + cfg.codemirror_options || {} + ); + that._set_codemirror_options(cmopts); + that.events.trigger('config_changed.Editor', {config: that.config}); + }); + }; + + // default CodeMirror options + Editor.default_codemirror_options = { + extraKeys: { + "Tab" : "indentMore", + }, + indentUnit: 4, + theme: "ipython", + lineNumbers: true, + lineWrapping: true, }; Editor.prototype.load = function() { + /** load the file */ var that = this; var cm = this.codemirror; - this.contents.get(this.file_path, {type: 'file', format: 'text'}) + return this.contents.get(this.file_path, {type: 'file', format: 'text'}) .then(function(model) { cm.setValue(model.content); // Setting the file's initial value creates a history entry, // which we don't want. cm.clearHistory(); - - // Find and load the highlighting mode - utils.requireCodeMirrorMode(model.mimetype, function(spec) { - var mode = CodeMirror.getMode({}, spec); - cm.setOption('mode', mode); - }); + that._set_mode_for_model(model); that.save_enabled = true; + that.generation = cm.changeGeneration(); + that.events.trigger("file_loaded.Editor", model); }, function(error) { + that.events.trigger("file_load_failed.Editor", error); + if (error.xhr.responseJSON.reason === 'bad format') { + window.location = utils.url_path_join( + that.base_url, + 'files', + that.file_path + ); + } cm.setValue("Error! " + error.message + "\nSaving disabled."); that.save_enabled = false; @@ -55,7 +95,55 @@ function($, ); }; - Editor.prototype.save = function() { + Editor.prototype._set_mode_for_model = function (model) { + /** Set the CodeMirror mode based on the file model */ + + // Find and load the highlighting mode, + // first by mime-type, then by file extension + var modeinfo = CodeMirror.findModeByMIME(model.mimetype); + if (modeinfo.mode === "null") { + // find by mime failed, use find by ext + var ext_idx = model.name.lastIndexOf('.'); + + if (ext_idx > 0) { + // CodeMirror.findModeByExtension wants extension without '.' + modeinfo = CodeMirror.findModeByExtension(model.name.slice(ext_idx + 1)); + } + } + if (modeinfo) { + this.set_codemirror_mode(modeinfo); + } + }; + + Editor.prototype.set_codemirror_mode = function (modeinfo) { + /** set the codemirror mode from a modeinfo struct */ + var that = this; + utils.requireCodeMirrorMode(modeinfo, function () { + that.codemirror.setOption('mode', modeinfo.mode); + that.events.trigger("mode_changed.Editor", modeinfo); + }); + }; + + Editor.prototype.get_filename = function () { + return utils.url_path_split(this.file_path)[1]; + }; + + Editor.prototype.rename = function (new_name) { + /** rename the file */ + var that = this; + var parent = utils.url_path_split(this.file_path)[0]; + var new_path = utils.url_path_join(parent, new_name); + return this.contents.rename(this.file_path, new_path).then( + function (model) { + that.file_path = model.path; + that.events.trigger('file_renamed.Editor', model); + that._set_mode_for_model(model); + } + ); + }; + + Editor.prototype.save = function () { + /** save the file */ if (!this.save_enabled) { console.log("Not saving, save disabled"); return; @@ -67,10 +155,36 @@ function($, content: this.codemirror.getValue(), }; var that = this; - this.contents.save(this.file_path, model).then(function() { - that.events.trigger("save_succeeded.TextEditor"); + // record change generation for isClean + this.generation = this.codemirror.changeGeneration(); + return this.contents.save(this.file_path, model).then(function(data) { + that.events.trigger("file_saved.Editor", data); }); }; + + Editor.prototype._set_codemirror_options = function (options) { + // update codemirror options from a dict + var codemirror = this.codemirror; + $.map(options, function (value, opt) { + if (value === null) { + value = CodeMirror.defaults[opt]; + } + codemirror.setOption(opt, value); + }); + }; + + Editor.prototype.update_codemirror_options = function (options) { + /** update codemirror options locally and save changes in config */ + var that = this; + this._set_codemirror_options(options); + return this.config.update({ + Editor: { + codemirror_options: options + } + }).then( + that.events.trigger('config_changed.Editor', {config: that.config}) + ); + }; return {Editor: Editor}; }); diff --git a/IPython/html/static/edit/js/main.js b/IPython/html/static/edit/js/main.js index ec787d4..2ff2845 100644 --- a/IPython/html/static/edit/js/main.js +++ b/IPython/html/static/edit/js/main.js @@ -10,6 +10,7 @@ require([ 'services/config', 'edit/js/editor', 'edit/js/menubar', + 'edit/js/savewidget', 'edit/js/notificationarea', 'custom/custom', ], function( @@ -19,8 +20,9 @@ require([ events, contents, configmod, - editor, + editmod, menubar, + savewidget, notificationarea ){ page = new page.Page(); @@ -28,22 +30,30 @@ require([ var base_url = utils.get_body_data('baseUrl'); var file_path = utils.get_body_data('filePath'); contents = new contents.Contents({base_url: base_url}); - var config = new configmod.ConfigSection('edit', {base_url: base_url}) + var config = new configmod.ConfigSection('edit', {base_url: base_url}); config.load(); - var editor = new editor.Editor('#texteditor-container', { + var editor = new editmod.Editor('#texteditor-container', { base_url: base_url, events: events, contents: contents, file_path: file_path, + config: config, }); // Make it available for debugging IPython.editor = editor; + var save_widget = new savewidget.SaveWidget('span#save_widget', { + editor: editor, + events: events, + }); + var menus = new menubar.MenuBar('#menubar', { base_url: base_url, editor: editor, + events: events, + save_widget: save_widget, }); var notification_area = new notificationarea.EditorNotificationArea( @@ -61,4 +71,11 @@ require([ }); editor.load(); page.show(); + + window.onbeforeunload = function () { + if (editor.save_enabled && !editor.codemirror.isClean(editor.generation)) { + return "Unsaved changes will be lost. Close anyway?"; + } + }; + }); diff --git a/IPython/html/static/edit/js/menubar.js b/IPython/html/static/edit/js/menubar.js index 15b1b4e..374ebe2 100644 --- a/IPython/html/static/edit/js/menubar.js +++ b/IPython/html/static/edit/js/menubar.js @@ -2,11 +2,14 @@ // Distributed under the terms of the Modified BSD License. define([ - 'base/js/namespace', 'jquery', + 'base/js/namespace', 'base/js/utils', + 'base/js/dialog', + 'codemirror/lib/codemirror', + 'codemirror/mode/meta', 'bootstrap', -], function(IPython, $, utils, bootstrap) { +], function($, IPython, utils, dialog, CodeMirror) { "use strict"; var MenuBar = function (selector, options) { @@ -29,21 +32,123 @@ define([ this.base_url = options.base_url || utils.get_body_data("baseUrl"); this.selector = selector; this.editor = options.editor; + this.events = options.events; + this.save_widget = options.save_widget; if (this.selector !== undefined) { this.element = $(selector); this.bind_events(); } + this._load_mode_menu(); + Object.seal(this); }; MenuBar.prototype.bind_events = function () { - /** - * File - */ var that = this; - this.element.find('#save_file').click(function () { - that.editor.save(); + var editor = that.editor; + + // File + this.element.find('#new-file').click(function () { + var w = window.open(); + // Create a new file in the current directory + var parent = utils.url_path_split(editor.file_path)[0]; + editor.contents.new_untitled(parent, {type: "file"}).then( + function (data) { + w.location = utils.url_join_encode( + that.base_url, 'edit', data.path + ); + }, + function(error) { + w.close(); + dialog.modal({ + title : 'Creating New File Failed', + body : "The error was: " + error.message, + buttons : {'OK' : {'class' : 'btn-primary'}} + }); + } + ); + }); + this.element.find('#save-file').click(function () { + editor.save(); + }); + this.element.find('#rename-file').click(function () { + that.save_widget.rename(); + }); + + // Edit + this.element.find('#menu-find').click(function () { + editor.codemirror.execCommand("find"); + }); + this.element.find('#menu-replace').click(function () { + editor.codemirror.execCommand("replace"); + }); + this.element.find('#menu-keymap-default').click(function () { + editor.update_codemirror_options({ + vimMode: false, + keyMap: 'default' + }); + }); + this.element.find('#menu-keymap-sublime').click(function () { + editor.update_codemirror_options({ + vimMode: false, + keyMap: 'sublime' + }); }); + this.element.find('#menu-keymap-emacs').click(function () { + editor.update_codemirror_options({ + vimMode: false, + keyMap: 'emacs' + }); + }); + this.element.find('#menu-keymap-vim').click(function () { + editor.update_codemirror_options({ + vimMode: true, + keyMap: 'vim' + }); + }); + + // View + this.element.find('#menu-line-numbers').click(function () { + var current = editor.codemirror.getOption('lineNumbers'); + var value = Boolean(1-current); + editor.update_codemirror_options({lineNumbers: value}); + }); + + this.events.on("config_changed.Editor", function () { + var keyMap = editor.codemirror.getOption('keyMap') || "default"; + that.element.find(".selected-keymap").removeClass("selected-keymap"); + that.element.find("#menu-keymap-" + keyMap).addClass("selected-keymap"); + }); + + this.events.on("mode_changed.Editor", function (evt, modeinfo) { + that.element.find("#current-mode") + .text(modeinfo.name) + .attr( + 'title', + "The current language is " + modeinfo.name + ); + }); + }; + + MenuBar.prototype._load_mode_menu = function () { + var list = this.element.find("#mode-menu"); + var editor = this.editor; + function make_set_mode(info) { + return function () { + editor.set_codemirror_mode(info); + }; + } + for (var i = 0; i < CodeMirror.modeInfo.length; i++) { + var info = CodeMirror.modeInfo[i]; + list.append($("
  • ").append( + $("").attr("href", "#") + .text(info.name) + .click(make_set_mode(info)) + .attr('title', + "Set language to " + info.name + ) + )); + } }; return {'MenuBar': MenuBar}; diff --git a/IPython/html/static/edit/js/savewidget.js b/IPython/html/static/edit/js/savewidget.js new file mode 100644 index 0000000..5688780 --- /dev/null +++ b/IPython/html/static/edit/js/savewidget.js @@ -0,0 +1,202 @@ +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. + +define([ + 'base/js/namespace', + 'jquery', + 'base/js/utils', + 'base/js/dialog', + 'base/js/keyboard', + 'moment', +], function(IPython, $, utils, dialog, keyboard, moment) { + "use strict"; + + var SaveWidget = function (selector, options) { + this.editor = undefined; + this.selector = selector; + this.events = options.events; + this.editor = options.editor; + this._last_modified = undefined; + this.keyboard_manager = options.keyboard_manager; + if (this.selector !== undefined) { + this.element = $(selector); + this.bind_events(); + } + }; + + + SaveWidget.prototype.bind_events = function () { + var that = this; + this.element.find('span.filename').click(function () { + that.rename(); + }); + this.events.on('file_loaded.Editor', function (evt, model) { + that.update_filename(model.name); + that.update_document_title(model.name); + that.update_last_modified(model.last_modified); + }); + this.events.on('file_saved.Editor', function (evt, model) { + that.update_filename(model.name); + that.update_document_title(model.name); + that.update_last_modified(model.last_modified); + }); + this.events.on('file_renamed.Editor', function (evt, model) { + that.update_filename(model.name); + that.update_document_title(model.name); + that.update_address_bar(model.path); + }); + this.events.on('file_save_failed.Editor', function () { + that.set_save_status('Save Failed!'); + }); + }; + + + SaveWidget.prototype.rename = function (options) { + options = options || {}; + var that = this; + var dialog_body = $('
    ').append( + $("

    ").addClass("rename-message") + .text('Enter a new filename:') + ).append( + $("
    ") + ).append( + $('').attr('type','text').attr('size','25').addClass('form-control') + .val(that.editor.get_filename()) + ); + var d = dialog.modal({ + title: "Rename File", + body: dialog_body, + buttons : { + "OK": { + class: "btn-primary", + click: function () { + var new_name = d.find('input').val(); + d.find('.rename-message').text("Renaming..."); + d.find('input[type="text"]').prop('disabled', true); + that.editor.rename(new_name).then( + function () { + d.modal('hide'); + }, function (error) { + d.find('.rename-message').text(error.message || 'Unknown error'); + d.find('input[type="text"]').prop('disabled', false).focus().select(); + } + ); + return false; + } + }, + "Cancel": {} + }, + open : function () { + // Upon ENTER, click the OK button. + d.find('input[type="text"]').keydown(function (event) { + if (event.which === keyboard.keycodes.enter) { + d.find('.btn-primary').first().click(); + return false; + } + }); + d.find('input[type="text"]').focus().select(); + } + }); + }; + + + SaveWidget.prototype.update_filename = function (filename) { + this.element.find('span.filename').text(filename); + }; + + SaveWidget.prototype.update_document_title = function (filename) { + document.title = filename; + }; + + SaveWidget.prototype.update_address_bar = function (path) { + var state = {path : path}; + window.history.replaceState(state, "", utils.url_join_encode( + this.editor.base_url, + "edit", + path) + ); + }; + + SaveWidget.prototype.update_last_modified = function (last_modified) { + if (last_modified) { + this._last_modified = new Date(last_modified); + } else { + this._last_modified = null; + } + this._render_last_modified(); + }; + + SaveWidget.prototype._render_last_modified = function () { + /** actually set the text in the element, from our _last_modified value + + called directly, and periodically in timeouts. + */ + this._schedule_render_last_modified(); + var el = this.element.find('span.last_modified'); + if (!this._last_modified) { + el.text('').attr('title', 'never saved'); + return; + } + var chkd = moment(this._last_modified); + var long_date = chkd.format('llll'); + var human_date; + var tdelta = Math.ceil(new Date() - this._last_modified); + if (tdelta < 24 * H){ + // less than 24 hours old, use relative date + human_date = chkd.fromNow(); + } else { + // otherwise show calendar + // otherwise update every hour and show + // at hh,mm,ss + human_date = chkd.calendar(); + } + el.text(human_date).attr('title', long_date); + }; + + + var S = 1000; + var M = 60*S; + var H = 60*M; + var thresholds = { + s: 45 * S, + m: 45 * M, + h: 22 * H + }; + var _timeout_from_dt = function (ms) { + /** compute a timeout to update the last-modified timeout + + based on the delta in milliseconds + */ + if (ms < thresholds.s) { + return 5 * S; + } else if (ms < thresholds.m) { + return M; + } else { + return 5 * M; + } + }; + + SaveWidget.prototype._schedule_render_last_modified = function () { + /** schedule the next update to relative date + + periodically updated, so short values like 'a few seconds ago' don't get stale. + */ + var that = this; + if (!this._last_modified) { + return; + } + if ((this._last_modified_timeout)) { + clearTimeout(this._last_modified_timeout); + } + var dt = Math.ceil(new Date() - this._last_modified); + if (dt < 24 * H) { + this._last_modified_timeout = setTimeout( + $.proxy(this._render_last_modified, this), + _timeout_from_dt(dt) + ); + } + }; + + return {'SaveWidget': SaveWidget}; + +}); diff --git a/IPython/html/static/edit/less/edit.less b/IPython/html/static/edit/less/edit.less new file mode 100644 index 0000000..072ffd5 --- /dev/null +++ b/IPython/html/static/edit/less/edit.less @@ -0,0 +1,9 @@ +#texteditor-container { + border-bottom: 1px solid #ccc; +} + +#filename { + font-size: 16pt; + display: table; + padding: 0px 5px; +} diff --git a/IPython/html/static/edit/less/menubar.less b/IPython/html/static/edit/less/menubar.less new file mode 100644 index 0000000..f709333 --- /dev/null +++ b/IPython/html/static/edit/less/menubar.less @@ -0,0 +1,14 @@ +.selected-keymap { + i.fa { + padding: 0px 5px; + } + i.fa:before { + content: @fa-var-check; + } +} + +#mode-menu { + // truncate mode-menu, so it doesn't get longer than the screen + overflow: auto; + max-height: 20em; +} \ No newline at end of file diff --git a/IPython/html/static/edit/less/style.less b/IPython/html/static/edit/less/style.less new file mode 100644 index 0000000..b365b93 --- /dev/null +++ b/IPython/html/static/edit/less/style.less @@ -0,0 +1,7 @@ +/*! +* +* IPython text editor webapp +* +*/ +@import "menubar.less"; +@import "edit.less"; diff --git a/IPython/html/static/notebook/js/savewidget.js b/IPython/html/static/notebook/js/savewidget.js index d69b32c..6b9db96 100644 --- a/IPython/html/static/notebook/js/savewidget.js +++ b/IPython/html/static/notebook/js/savewidget.js @@ -29,7 +29,7 @@ define([ SaveWidget.prototype.bind_events = function () { var that = this; - this.element.find('span#notebook_name').click(function () { + this.element.find('span.filename').click(function () { that.rename_notebook({notebook: that.notebook}); }); this.events.on('notebook_loaded.Notebook', function () { @@ -130,7 +130,7 @@ define([ SaveWidget.prototype.update_notebook_name = function () { var nbname = this.notebook.get_notebook_name(); - this.element.find('span#notebook_name').text(nbname); + this.element.find('span.filename').text(nbname); }; @@ -152,11 +152,11 @@ define([ SaveWidget.prototype.set_save_status = function (msg) { - this.element.find('span#autosave_status').text(msg); + this.element.find('span.autosave_status').text(msg); }; SaveWidget.prototype._set_checkpoint_status = function (human_date, iso_date) { - var el = this.element.find('span#checkpoint_status'); + var el = this.element.find('span.checkpoint_status'); if(human_date){ el.text("Last Checkpoint: "+human_date).attr('title',iso_date); } else { @@ -223,7 +223,7 @@ define([ // update regularly for the first 6hours and show // ago - if(tdelta < tdelta < 6*3600*1000){ + if(tdelta < 6*3600*1000){ recall(_next_timeago_update(tdelta)); this._set_checkpoint_status(chkd.fromNow(), longdate); // otherwise update every hour and show diff --git a/IPython/html/static/notebook/less/notebook.less b/IPython/html/static/notebook/less/notebook.less index 58ac2df..86b5cb7 100644 --- a/IPython/html/static/notebook/less/notebook.less +++ b/IPython/html/static/notebook/less/notebook.less @@ -15,20 +15,6 @@ body { .border-box-sizing(); } -span#notebook_name { - height: 1em; - line-height: 1em; - padding: 3px; - border: none; - font-size: 146.5%; - &:hover{ - // ensure body is lighter on dark palette, - // and vice versa - background-color:contrast(@body-bg, lighten(@body-bg,30%), darken(@body-bg,10%)); - } - .corner-all; -} - div#notebook_panel { margin: 0px 0px 0px 0px; padding: 0px; diff --git a/IPython/html/static/notebook/less/savewidget.less b/IPython/html/static/notebook/less/savewidget.less index 73c7922..28954a1 100644 --- a/IPython/html/static/notebook/less/savewidget.less +++ b/IPython/html/static/notebook/less/savewidget.less @@ -1,33 +1,41 @@ -span#save_widget { +span.save_widget { margin-top: 6px; + + span.filename { + height: 1em; + line-height: 1em; + padding: 3px; + border: none; + font-size: 146.5%; + &:hover{ + // ensure body is lighter on dark palette, + // and vice versa + background-color:contrast(@body-bg, lighten(@body-bg,30%), darken(@body-bg,10%)); + } + .corner-all; + } } -span#checkpoint_status, span#autosave_status { +span.checkpoint_status, span.autosave_status { font-size: small; } @media (max-width: 767px) { - span#save_widget { + span.save_widget { font-size: small; } - span#checkpoint_status, span#autosave_status { - font-size: x-small; - } -} - -@media (max-width: 767px) { - span#checkpoint_status, span#autosave_status { - display: none; + span.checkpoint_status, span.autosave_status { + display: none; } } @media (min-width: 768px) and (max-width: 979px) { - span#checkpoint_status { + span.checkpoint_status { display: none; } - span#autosave_status { + span.autosave_status { font-size: x-small; } } - + diff --git a/IPython/html/static/services/contents.js b/IPython/html/static/services/contents.js index 768bdd4..994244d 100644 --- a/IPython/html/static/services/contents.js +++ b/IPython/html/static/services/contents.js @@ -162,6 +162,7 @@ define([ var settings = { processData : false, type : "PUT", + dataType: "json", data : JSON.stringify(model), contentType: 'application/json', }; diff --git a/IPython/html/static/style/style.less b/IPython/html/static/style/style.less index ad5130d..b4113bc 100644 --- a/IPython/html/static/style/style.less +++ b/IPython/html/static/style/style.less @@ -23,6 +23,9 @@ // tree @import "../tree/less/style.less"; +// edit +@import "../edit/less/style.less"; + // notebook @import "../notebook/less/style.less"; diff --git a/IPython/html/static/style/style.min.css b/IPython/html/static/style/style.min.css index 4a59dd8..c42bee0 100644 --- a/IPython/html/static/style/style.min.css +++ b/IPython/html/static/style/style.min.css @@ -7752,9 +7752,6 @@ div#header { /* Initially hidden to prevent FLOUC */ display: none; background-color: #ffffff; - box-sizing: border-box; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; /* Display over codemirror */ z-index: 100; } @@ -8119,6 +8116,29 @@ ul#new-notebook-menu { } /*! * +* IPython text editor webapp +* +*/ +.selected-keymap i.fa { + padding: 0px 5px; +} +.selected-keymap i.fa:before { + content: "\f00c"; +} +#mode-menu { + overflow: auto; + max-height: 20em; +} +#texteditor-container { + border-bottom: 1px solid #ccc; +} +#filename { + font-size: 16pt; + display: table; + padding: 0px 5px; +} +/*! +* * IPython notebook * */ @@ -9421,17 +9441,6 @@ body { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; } -span#notebook_name { - height: 1em; - line-height: 1em; - padding: 3px; - border: none; - font-size: 146.5%; - border-radius: 4px; -} -span#notebook_name:hover { - background-color: #e6e6e6; -} div#notebook_panel { margin: 0px 0px 0px 0px; padding: 0px; @@ -9681,7 +9690,6 @@ fieldset[disabled] #kernel_selector_widget > button.active { margin-top: 0px; } #menubar { - margin-top: 0px; box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; @@ -10159,33 +10167,38 @@ div#pager .ui-resizable-handle { /* Modern browsers */ flex: 1; } -span#save_widget { +span.save_widget { margin-top: 6px; } -span#checkpoint_status, -span#autosave_status { +span.save_widget span.filename { + height: 1em; + line-height: 1em; + padding: 3px; + border: none; + font-size: 146.5%; + border-radius: 4px; +} +span.save_widget span.filename:hover { + background-color: #e6e6e6; +} +span.checkpoint_status, +span.autosave_status { font-size: small; } @media (max-width: 767px) { - span#save_widget { + span.save_widget { font-size: small; } - span#checkpoint_status, - span#autosave_status { - font-size: x-small; - } -} -@media (max-width: 767px) { - span#checkpoint_status, - span#autosave_status { + span.checkpoint_status, + span.autosave_status { display: none; } } @media (min-width: 768px) and (max-width: 979px) { - span#checkpoint_status { + span.checkpoint_status { display: none; } - span#autosave_status { + span.autosave_status { font-size: x-small; } } diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index 984faf5..40c3f04 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -229,7 +229,7 @@ define([ NotebookList.uri_prefixes = { directory: 'tree', notebook: 'notebooks', - file: 'files', + file: 'edit', }; diff --git a/IPython/html/templates/edit.html b/IPython/html/templates/edit.html index 7841bd4..fdf30ca 100644 --- a/IPython/html/templates/edit.html +++ b/IPython/html/templates/edit.html @@ -5,18 +5,6 @@ {% block stylesheet %} - - {{super()}} {% endblock %} @@ -27,38 +15,70 @@ data-file-path="{{file_path}}" {% endblock %} -{% block header %} +{% block headercontainer %} -{{ basename }} + + + + {% endblock %} -{% block site %} +{% block header %}

    + +{% endblock %} + +{% block site %} +
    diff --git a/IPython/html/templates/notebook.html b/IPython/html/templates/notebook.html index 3cf4b0e..ec1c1e2 100644 --- a/IPython/html/templates/notebook.html +++ b/IPython/html/templates/notebook.html @@ -35,10 +35,10 @@ class="notebook_app" {% block headercontainer %} - - - - + + + +