diff --git a/IPython/html/static/edit/js/editor.js b/IPython/html/static/edit/js/editor.js index 4a0a453..d59c4cb 100644 --- a/IPython/html/static/edit/js/editor.js +++ b/IPython/html/static/edit/js/editor.js @@ -81,6 +81,7 @@ function($, }); that.save_enabled = true; that.generation = cm.changeGeneration(); + that.events.trigger("file_loaded.Editor", model); }, function(error) { cm.setValue("Error! " + error.message + @@ -89,8 +90,26 @@ function($, } ); }; + + Editor.prototype.get_filename = function () { + return utils.url_path_split(this.file_path)[1]; + + } - Editor.prototype.save = function() { + 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 (json) { + that.file_path = json.path; + that.events.trigger('file_renamed.Editor', json); + } + ); + }; + + Editor.prototype.save = function () { /** save the file */ if (!this.save_enabled) { console.log("Not saving, save disabled"); @@ -105,8 +124,8 @@ function($, var that = this; // record change generation for isClean this.generation = this.codemirror.changeGeneration(); - return this.contents.save(this.file_path, model).then(function() { - that.events.trigger("save_succeeded.TextEditor"); + return this.contents.save(this.file_path, model).then(function(data) { + that.events.trigger("file_saved.Editor", data); }); }; diff --git a/IPython/html/static/edit/js/main.js b/IPython/html/static/edit/js/main.js index 2bcaf6a..7c839a8 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( @@ -21,6 +22,7 @@ require([ configmod, editmod, menubar, + savewidget, notificationarea ){ page = new page.Page(); @@ -48,6 +50,11 @@ require([ events: events, }); + var save_widget = new savewidget.SaveWidget('span#save_widget', { + editor: editor, + events: events, + }); + var notification_area = new notificationarea.EditorNotificationArea( '#notification_area', { events: events, diff --git a/IPython/html/static/edit/js/savewidget.js b/IPython/html/static/edit/js/savewidget.js new file mode 100644 index 0000000..89069c5 --- /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({editor: that.editor}); + }); + 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(options.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/templates/edit.html b/IPython/html/templates/edit.html index cabc360..e80fc8c 100644 --- a/IPython/html/templates/edit.html +++ b/IPython/html/templates/edit.html @@ -17,7 +17,10 @@ data-file-path="{{file_path}}" {% block header %} -{{ basename }} + + + + {% endblock %}