Show warning in editor when local draft is out of date

Fixes #41
This commit is contained in:
Matt Baer 2020-06-11 11:45:12 -04:00
parent 5c94d23466
commit 9624c4db00
5 changed files with 129 additions and 10 deletions

View file

@ -361,6 +361,24 @@ body#pad {
z-index: 10; z-index: 10;
} }
body#pad .alert {
position: fixed;
bottom: 0.25em;
left: 2em;
right: 2em;
font-size: 1.1em;
&#edited-elsewhere {
&.hidden {
display: none;
}
a {
font-weight: bold;
}
}
}
@media all and (max-height: 500px) { @media all and (max-height: 500px) {
body#pad { body#pad {
textarea { textarea {
@ -425,6 +443,10 @@ body#pad {
padding-left: 10%; padding-left: 10%;
padding-right: 10%; padding-right: 10%;
} }
.alert {
left: 10%;
right: 10%;
}
} }
} }
@media all and (min-width: 60em) { @media all and (min-width: 60em) {
@ -433,6 +455,10 @@ body#pad {
padding-left: 15%; padding-left: 15%;
padding-right: 15%; padding-right: 15%;
} }
.alert {
left: 15%;
right: 15%;
}
} }
} }
@media all and (min-width: 70em) { @media all and (min-width: 70em) {
@ -441,6 +467,10 @@ body#pad {
padding-left: 20%; padding-left: 20%;
padding-right: 20%; padding-right: 20%;
} }
.alert {
left: 20%;
right: 20%;
}
} }
} }
@media all and (min-width: 85em) { @media all and (min-width: 85em) {
@ -449,6 +479,10 @@ body#pad {
padding-left: 25%; padding-left: 25%;
padding-right: 25%; padding-right: 25%;
} }
.alert {
left: 25%;
right: 25%;
}
} }
} }
@media all and (min-width: 105em) { @media all and (min-width: 105em) {
@ -457,6 +491,10 @@ body#pad {
padding-left: 30%; padding-left: 30%;
padding-right: 30%; padding-right: 30%;
} }
.alert {
left: 30%;
right: 30%;
}
} }
} }
@media (pointer: coarse) { @media (pointer: coarse) {

View file

@ -135,6 +135,7 @@ type (
Views int64 Views int64
Font string Font string
Created time.Time Created time.Time
Updated time.Time
IsRTL sql.NullBool IsRTL sql.NullBool
Language sql.NullString Language sql.NullString
OwnerID int64 OwnerID int64
@ -1240,9 +1241,9 @@ func getRawPost(app *App, friendlyID string) *RawPost {
var isRTL sql.NullBool var isRTL sql.NullBool
var lang sql.NullString var lang sql.NullString
var ownerID sql.NullInt64 var ownerID sql.NullInt64
var created time.Time var created, updated time.Time
err := app.db.QueryRow("SELECT title, content, text_appearance, language, rtl, created, owner_id FROM posts WHERE id = ?", friendlyID).Scan(&title, &content, &font, &lang, &isRTL, &created, &ownerID) err := app.db.QueryRow("SELECT title, content, text_appearance, language, rtl, created, updated, owner_id FROM posts WHERE id = ?", friendlyID).Scan(&title, &content, &font, &lang, &isRTL, &created, &updated, &ownerID)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return &RawPost{Content: "", Found: false, Gone: false} return &RawPost{Content: "", Found: false, Gone: false}
@ -1250,7 +1251,7 @@ func getRawPost(app *App, friendlyID string) *RawPost {
return &RawPost{Content: "", Found: true, Gone: false} return &RawPost{Content: "", Found: true, Gone: false}
} }
return &RawPost{Title: title, Content: content, Font: font, Created: created, IsRTL: isRTL, Language: lang, OwnerID: ownerID.Int64, Found: true, Gone: content == ""} return &RawPost{Title: title, Content: content, Font: font, Created: created, Updated: updated, IsRTL: isRTL, Language: lang, OwnerID: ownerID.Int64, Found: true, Gone: content == ""}
} }
@ -1259,15 +1260,15 @@ func getRawCollectionPost(app *App, slug, collAlias string) *RawPost {
var id, title, content, font string var id, title, content, font string
var isRTL sql.NullBool var isRTL sql.NullBool
var lang sql.NullString var lang sql.NullString
var created time.Time var created, updated time.Time
var ownerID null.Int var ownerID null.Int
var views int64 var views int64
var err error var err error
if app.cfg.App.SingleUser { if app.cfg.App.SingleUser {
err = app.db.QueryRow("SELECT id, title, content, text_appearance, language, rtl, view_count, created, owner_id FROM posts WHERE slug = ? AND collection_id = 1", slug).Scan(&id, &title, &content, &font, &lang, &isRTL, &views, &created, &ownerID) err = app.db.QueryRow("SELECT id, title, content, text_appearance, language, rtl, view_count, created, updated, owner_id FROM posts WHERE slug = ? AND collection_id = 1", slug).Scan(&id, &title, &content, &font, &lang, &isRTL, &views, &created, &updated, &ownerID)
} else { } else {
err = app.db.QueryRow("SELECT id, title, content, text_appearance, language, rtl, view_count, created, owner_id FROM posts WHERE slug = ? AND collection_id = (SELECT id FROM collections WHERE alias = ?)", slug, collAlias).Scan(&id, &title, &content, &font, &lang, &isRTL, &views, &created, &ownerID) err = app.db.QueryRow("SELECT id, title, content, text_appearance, language, rtl, view_count, created, updated, owner_id FROM posts WHERE slug = ? AND collection_id = (SELECT id FROM collections WHERE alias = ?)", slug, collAlias).Scan(&id, &title, &content, &font, &lang, &isRTL, &views, &created, &updated, &ownerID)
} }
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
@ -1283,6 +1284,7 @@ func getRawCollectionPost(app *App, slug, collAlias string) *RawPost {
Content: content, Content: content,
Font: font, Font: font,
Created: created, Created: created,
Updated: updated,
IsRTL: isRTL, IsRTL: isRTL,
Language: lang, Language: lang,
OwnerID: ownerID.Int64, OwnerID: ownerID.Int64,
@ -1543,6 +1545,13 @@ func (rp *RawPost) Created8601() string {
return rp.Created.Format("2006-01-02T15:04:05Z") return rp.Created.Format("2006-01-02T15:04:05Z")
} }
func (rp *RawPost) Updated8601() string {
if rp.Updated.IsZero() {
return ""
}
return rp.Updated.Format("2006-01-02T15:04:05Z")
}
var imageURLRegex = regexp.MustCompile(`(?i)[^ ]+\.(gif|png|jpg|jpeg|image)$`) var imageURLRegex = regexp.MustCompile(`(?i)[^ ]+\.(gif|png|jpg|jpeg|image)$`)
func (p *Post) extractImages() { func (p *Post) extractImages() {

View file

@ -116,13 +116,27 @@ var H = {
save: function($el, key) { save: function($el, key) {
localStorage.setItem(key, $el.el.value); localStorage.setItem(key, $el.el.value);
}, },
load: function($el, key, onlyLoadPopulated) { load: function($el, key, onlyLoadPopulated, postUpdated) {
var val = localStorage.getItem(key); var val = localStorage.getItem(key);
if (onlyLoadPopulated && val == null) { if (onlyLoadPopulated && val == null) {
// Do nothing // Do nothing
return; return true;
} }
$el.el.value = val; $el.el.value = val;
if (postUpdated != null) {
var lastLocalPublishStr = localStorage.getItem(key+'-published');
if (lastLocalPublishStr != null && lastLocalPublishStr != '') {
try {
var lastLocalPublish = new Date(lastLocalPublishStr);
if (postUpdated > lastLocalPublish) {
return false;
}
} catch (e) {
console.error("unable to parse draft updated time");
}
}
}
return true;
}, },
set: function(key, value) { set: function(key, value) {
localStorage.setItem(key, value); localStorage.setItem(key, value);

View file

@ -17,6 +17,8 @@
{{end}}{{.Post.Content}}</textarea> {{end}}{{.Post.Content}}</textarea>
<div class="alert success hidden" id="edited-elsewhere">This post has been updated elsewhere since you last published! <a href="#" id="erase-edit">Delete draft and reload</a>.</div>
<header id="tools"> <header id="tools">
<div id="clip"> <div id="clip">
{{if not .SingleUser}}<h1>{{if .Chorus}}<a href="/" title="Home">{{else}}<a href="/me/c/" title="View blogs">{{end}}{{.SiteName}}</a></h1>{{end}} {{if not .SingleUser}}<h1>{{if .Chorus}}<a href="/" title="Home">{{else}}<a href="/me/c/" title="View blogs">{{end}}{{.SiteName}}</a></h1>{{end}}
@ -36,6 +38,7 @@
<script> <script>
var $writer = H.getEl('writer'); var $writer = H.getEl('writer');
var $btnPublish = H.getEl('publish'); var $btnPublish = H.getEl('publish');
var $btnEraseEdit = H.getEl('edited-elsewhere');
var $wc = H.getEl("wc"); var $wc = H.getEl("wc");
var updateWordCount = function() { var updateWordCount = function() {
var words = 0; var words = 0;
@ -58,7 +61,17 @@
}; };
{{if .Post.Id}}var draftDoc = 'draft{{.Post.Id}}'; {{if .Post.Id}}var draftDoc = 'draft{{.Post.Id}}';
var origDoc = '{{.Post.Content}}';{{else}}var draftDoc = 'lastDoc';{{end}} var origDoc = '{{.Post.Content}}';{{else}}var draftDoc = 'lastDoc';{{end}}
H.load($writer, draftDoc, true); var updatedStr = '{{.Post.Updated8601}}';
var updated = null;
if (updatedStr != '') {
updated = new Date(updatedStr);
}
var ok = H.load($writer, draftDoc, true, updated);
if (!ok) {
// Show "edited elsewhere" warning
$btnEraseEdit.el.classList.remove('hidden');
}
var defaultTimeSet = false;
updateWordCount(); updateWordCount();
var typingTimer; var typingTimer;
@ -130,6 +143,7 @@
data = JSON.parse(http.responseText); data = JSON.parse(http.responseText);
id = data.data.id; id = data.data.id;
nextURL = '{{if .SingleUser}}/d{{end}}/'+id; nextURL = '{{if .SingleUser}}/d{{end}}/'+id;
localStorage.setItem('draft'+id+'-published', new Date().toISOString());
{{ if not .Post.Id }} {{ if not .Post.Id }}
// Post created // Post created
@ -198,6 +212,13 @@
publish(content, selectedFont); publish(content, selectedFont);
} }
}); });
H.getEl('erase-edit').on('click', function(e) {
e.preventDefault();
H.remove(draftDoc);
H.remove(draftDoc+'-published');
justPublished = true; // Block auto-save
location.reload();
});
WebFontConfig = { WebFontConfig = {
custom: { families: [ 'Lora:400,700:latin' ], urls: [ '/css/fonts.css' ] } custom: { families: [ 'Lora:400,700:latin' ], urls: [ '/css/fonts.css' ] }
@ -207,12 +228,20 @@
var doneTyping = function() { var doneTyping = function() {
if (draftDoc == 'lastDoc' || $writer.el.value != origDoc) { if (draftDoc == 'lastDoc' || $writer.el.value != origDoc) {
H.save($writer, draftDoc); H.save($writer, draftDoc);
if (!defaultTimeSet) {
var lastLocalPublishStr = localStorage.getItem(draftDoc+'-published');
if (lastLocalPublishStr == null || lastLocalPublishStr == '') {
localStorage.setItem(draftDoc+'-published', updatedStr);
}
defaultTimeSet = true;
}
updateWordCount(); updateWordCount();
} }
}; };
window.addEventListener('beforeunload', function(e) { window.addEventListener('beforeunload', function(e) {
if (draftDoc != 'lastDoc' && $writer.el.value == origDoc) { if (draftDoc != 'lastDoc' && $writer.el.value == origDoc) {
H.remove(draftDoc); H.remove(draftDoc);
H.remove(draftDoc+'-published');
} else if (!justPublished) { } else if (!justPublished) {
doneTyping(); doneTyping();
} }

View file

@ -17,6 +17,8 @@
{{end}}{{.Post.Content}}</textarea> {{end}}{{.Post.Content}}</textarea>
<div class="alert success hidden" id="edited-elsewhere">This post has been updated elsewhere since you last published! <a href="#" id="erase-edit">Delete draft and reload</a>.</div>
<header id="tools"> <header id="tools">
<div id="clip"> <div id="clip">
{{if not .SingleUser}}<h1><a href="/me/c/" title="View blogs"><img class="ic-24dp" src="/img/ic_blogs_dark@2x.png" /></a></h1>{{end}} {{if not .SingleUser}}<h1><a href="/me/c/" title="View blogs"><img class="ic-24dp" src="/img/ic_blogs_dark@2x.png" /></a></h1>{{end}}
@ -88,6 +90,7 @@
} }
var $writer = H.getEl('writer'); var $writer = H.getEl('writer');
var $btnPublish = H.getEl('publish'); var $btnPublish = H.getEl('publish');
var $btnEraseEdit = H.getEl('edited-elsewhere');
var $wc = H.getEl("wc"); var $wc = H.getEl("wc");
var updateWordCount = function() { var updateWordCount = function() {
var words = 0; var words = 0;
@ -110,7 +113,17 @@
}; };
{{if .Post.Id}}var draftDoc = 'draft{{.Post.Id}}'; {{if .Post.Id}}var draftDoc = 'draft{{.Post.Id}}';
var origDoc = '{{.Post.Content}}';{{else}}var draftDoc = 'lastDoc';{{end}} var origDoc = '{{.Post.Content}}';{{else}}var draftDoc = 'lastDoc';{{end}}
H.load($writer, draftDoc, true); var updatedStr = '{{.Post.Updated8601}}';
var updated = null;
if (updatedStr != '') {
updated = new Date(updatedStr);
}
var ok = H.load($writer, draftDoc, true, updated);
if (!ok) {
// Show "edited elsewhere" warning
$btnEraseEdit.el.classList.remove('hidden');
}
var defaultTimeSet = false;
updateWordCount(); updateWordCount();
var typingTimer; var typingTimer;
@ -190,6 +203,7 @@
data = JSON.parse(http.responseText); data = JSON.parse(http.responseText);
id = data.data.id; id = data.data.id;
nextURL = '{{if .SingleUser}}/d{{end}}/'+id; nextURL = '{{if .SingleUser}}/d{{end}}/'+id;
localStorage.setItem('draft'+id+'-published', new Date().toISOString());
{{ if not .Post.Id }} {{ if not .Post.Id }}
// Post created // Post created
@ -258,6 +272,13 @@
publish(content, selectedFont); publish(content, selectedFont);
} }
}); });
H.getEl('erase-edit').on('click', function(e) {
e.preventDefault();
H.remove(draftDoc);
H.remove(draftDoc+'-published');
justPublished = true; // Block auto-save
location.reload();
});
H.getEl('toggle-theme').on('click', function(e) { H.getEl('toggle-theme').on('click', function(e) {
e.preventDefault(); e.preventDefault();
@ -338,12 +359,20 @@
var doneTyping = function() { var doneTyping = function() {
if (draftDoc == 'lastDoc' || $writer.el.value != origDoc) { if (draftDoc == 'lastDoc' || $writer.el.value != origDoc) {
H.save($writer, draftDoc); H.save($writer, draftDoc);
if (!defaultTimeSet) {
var lastLocalPublishStr = localStorage.getItem(draftDoc+'-published');
if (lastLocalPublishStr == null || lastLocalPublishStr == '') {
localStorage.setItem(draftDoc+'-published', updatedStr);
}
defaultTimeSet = true;
}
updateWordCount(); updateWordCount();
} }
}; };
window.addEventListener('beforeunload', function(e) { window.addEventListener('beforeunload', function(e) {
if (draftDoc != 'lastDoc' && $writer.el.value == origDoc) { if (draftDoc != 'lastDoc' && $writer.el.value == origDoc) {
H.remove(draftDoc); H.remove(draftDoc);
H.remove(draftDoc+'-published');
} else if (!justPublished) { } else if (!justPublished) {
doneTyping(); doneTyping();
} }