client/posts: add simple editing
This commit is contained in:
parent
651c3f6925
commit
7488abb332
|
@ -310,3 +310,37 @@ $safety-unsafe = #F3985F
|
|||
color: $inactive-link-color
|
||||
padding-left: 0.7em
|
||||
font-size: 90%
|
||||
|
||||
.post-view .edit-sidebar
|
||||
section
|
||||
margin-bottom: 1em
|
||||
|
||||
.safety
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
label:not(.radio)
|
||||
width: 100%
|
||||
.radio
|
||||
flex-grow: 1
|
||||
display: inline-block
|
||||
|
||||
.tags
|
||||
.tag-input
|
||||
min-height: 6.25em
|
||||
|
||||
label
|
||||
margin-bottom: 0.3em
|
||||
display: block
|
||||
|
||||
.buttons
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
margin-top: 2em
|
||||
input, button
|
||||
flex-grow: 1
|
||||
|
||||
input[type=submit],
|
||||
input[type=button],
|
||||
button
|
||||
&:focus
|
||||
border: 2px solid $text-color !important
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<article class='edit-post'>
|
||||
<% if (ctx.editMode) { %>
|
||||
<a href='/post/<%- encodeURIComponent(ctx.post.id) %>'>
|
||||
<i class='fa fa-eye'></i>
|
||||
<i class='fa fa-reply'></i>
|
||||
<span class='vim-nav-hint'>Back to view mode</span>
|
||||
</a>
|
||||
<% } else { %>
|
||||
|
|
|
@ -1 +1,69 @@
|
|||
<h1>Editing</h1>
|
||||
<div class='edit-sidebar'>
|
||||
<form autocomplete='off'>
|
||||
<div class='input'>
|
||||
<section class='safety'>
|
||||
<label>Safety</label>
|
||||
<%= ctx.makeRadio({
|
||||
name: 'safety',
|
||||
class: 'safety-safe',
|
||||
value: 'safe',
|
||||
selectedValue: ctx.post.safety,
|
||||
text: 'Safe'}) %>
|
||||
<%= ctx.makeRadio({
|
||||
name: 'safety',
|
||||
class: 'safety-sketchy',
|
||||
value: 'sketchy',
|
||||
selectedValue: ctx.post.safety,
|
||||
text: 'Sketchy'}) %>
|
||||
<%= ctx.makeRadio({
|
||||
name: 'safety',
|
||||
value: 'unsafe',
|
||||
selectedValue: ctx.post.safety,
|
||||
class: 'safety-unsafe',
|
||||
text: 'Unsafe'}) %>
|
||||
</section>
|
||||
|
||||
<section class='tags'>
|
||||
<%= ctx.makeTextInput({
|
||||
text: 'Tags',
|
||||
value: ctx.post.tags.join(' '),
|
||||
readonly: !ctx.canEditPostTags}) %>
|
||||
</section>
|
||||
|
||||
<section class='relations'>
|
||||
<%= ctx.makeTextInput({
|
||||
text: 'Relations',
|
||||
name: 'relations',
|
||||
placeholder: 'space-separated post IDs',
|
||||
pattern: '^[0-9 ]*$',
|
||||
value: ctx.post.relations.map(rel => rel.id).join(' '),
|
||||
readonly: !ctx.canEditPostRelations}) %>
|
||||
</section>
|
||||
|
||||
<% if ((ctx.editingNewPost && ctx.canCreateAnonymousPosts) || ctx.post.type === 'video') { %>
|
||||
<section class='flags'>
|
||||
<label>Miscellaneous</label>
|
||||
|
||||
<% if (ctx.editingNewPost && ctx.canCreateAnonymousPosts) { %>
|
||||
<%= ctx.makeCheckbox({
|
||||
text: 'Don\'t show me as uploader',
|
||||
name: 'anonymous'}) %>
|
||||
<% } %>
|
||||
|
||||
<% if (ctx.post.type === 'video') { %>
|
||||
<!-- TODO: bind state -->
|
||||
<%= ctx.makeCheckbox({
|
||||
text: 'Loop video',
|
||||
name: 'loop',
|
||||
readonly: !ctx.canEditPostFlags}) %>
|
||||
<% } %>
|
||||
</section>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class='messages'></div>
|
||||
|
||||
<div class='buttons'>
|
||||
<input class='encourage' type='submit' value='Submit' class='submit'/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -52,6 +52,10 @@ class PostController {
|
|||
'score', e => this._evtScorePost(e));
|
||||
this._view.sidebarControl.addEventListener(
|
||||
'fitModeChange', e => this._evtFitModeChange(e));
|
||||
this._view.sidebarControl.addEventListener(
|
||||
'change', e => this._evtPostChange(e));
|
||||
this._view.sidebarControl.addEventListener(
|
||||
'submit', e => this._evtPostEdit(e));
|
||||
}
|
||||
if (this._view.commentFormControl) {
|
||||
this._view.commentFormControl.addEventListener(
|
||||
|
@ -93,6 +97,26 @@ class PostController {
|
|||
settings.save(browsingSettings);
|
||||
}
|
||||
|
||||
_evtPostEdit(e) {
|
||||
// TODO: disable form
|
||||
const post = e.detail.post;
|
||||
post.tags = e.detail.tags;
|
||||
post.safety = e.detail.safety;
|
||||
post.relations = e.detail.relations;
|
||||
post.save()
|
||||
.then(() => {
|
||||
misc.disableExitConfirmation();
|
||||
// TODO: enable form
|
||||
}, errorMessage => {
|
||||
window.alert(errorMessage);
|
||||
// TODO: enable form
|
||||
});
|
||||
}
|
||||
|
||||
_evtPostChange(e) {
|
||||
misc.enableExitConfirmation();
|
||||
}
|
||||
|
||||
_evtCommentChange(e) {
|
||||
misc.enableExitConfirmation();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
const api = require('../api.js');
|
||||
const events = require('../events.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const views = require('../util/views.js');
|
||||
const TagInputControl = require('./tag_input_control.js');
|
||||
|
||||
const template = views.getTemplate('post-edit-sidebar');
|
||||
|
||||
|
@ -14,7 +17,61 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
|
||||
views.replaceContent(this._hostNode, template({
|
||||
post: this._post,
|
||||
canEditPostContent: api.hasPrivilege('posts:edit:content'),
|
||||
canEditPostFlags: api.hasPrivilege('posts:edit:flags'),
|
||||
canEditPostNotes: api.hasPrivilege('posts:edit:notes'),
|
||||
canEditPostRelations: api.hasPrivilege('posts:edit:relations'),
|
||||
canEditPostSafety: api.hasPrivilege('posts:edit:safety'),
|
||||
canEditPostSource: api.hasPrivilege('posts:edit:source'),
|
||||
canEditPostTags: api.hasPrivilege('posts:edit:tags'),
|
||||
canEditPostThumbnail: api.hasPrivilege('posts:edit:thumbnail'),
|
||||
canCreateAnonymousPosts: api.hasPrivilege('posts:create:anonymous'),
|
||||
canDeletePosts: api.hasPrivilege('posts:delete'),
|
||||
canFeaturePosts: api.hasPrivilege('posts:feature'),
|
||||
}));
|
||||
|
||||
if (this._formNode) {
|
||||
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
|
||||
}
|
||||
|
||||
this._tagControl = new TagInputControl(this._tagInputNode);
|
||||
}
|
||||
|
||||
_evtSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('submit', {
|
||||
detail: {
|
||||
post: this._post,
|
||||
safety:
|
||||
Array.from(this._safetyButtonNodes)
|
||||
.filter(node => node.checked)[0]
|
||||
.value.toLowerCase(),
|
||||
tags:
|
||||
misc.splitByWhitespace(this._tagInputNode.value),
|
||||
relations:
|
||||
misc.splitByWhitespace(this._relationsInputNode.value),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
get _formNode() {
|
||||
return this._hostNode.querySelector('form');
|
||||
}
|
||||
|
||||
get _submitButtonNode() {
|
||||
return this._hostNode.querySelector('.submit');
|
||||
}
|
||||
|
||||
get _safetyButtonNodes() {
|
||||
return this._formNode.querySelectorAll('.safety input');
|
||||
}
|
||||
|
||||
get _tagInputNode() {
|
||||
return this._formNode.querySelector('.tags input');
|
||||
}
|
||||
|
||||
get _relationsInputNode() {
|
||||
return this._formNode.querySelector('.relations input');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
const api = require('../api.js');
|
||||
const tags = require('../tags.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
const TagAutoCompleteControl = require('./tag_auto_complete_control.js');
|
||||
|
||||
|
@ -16,8 +17,17 @@ const KEY_RETURN = 13;
|
|||
const KEY_BACKSPACE = 8;
|
||||
const KEY_DELETE = 46;
|
||||
|
||||
class TagInputControl {
|
||||
class TagInputEvent extends Event {
|
||||
constructor(name, tagInput) {
|
||||
super(name);
|
||||
this.tagInput = tagInput;
|
||||
this.tags = tagInput.tags;
|
||||
}
|
||||
}
|
||||
|
||||
class TagInputControl extends events.EventTarget {
|
||||
constructor(sourceInputNode) {
|
||||
super();
|
||||
this.tags = [];
|
||||
this.readOnly = sourceInputNode.readOnly;
|
||||
|
||||
|
@ -90,6 +100,8 @@ class TagInputControl {
|
|||
this._hideVisualCues();
|
||||
|
||||
this.tags.push(text);
|
||||
this.dispatchEvent(new TagInputEvent('add', this));
|
||||
this.dispatchEvent(new TagInputEvent('change', this));
|
||||
this._sourceInputNode.value = this.tags.join(' ');
|
||||
|
||||
const sourceWrapperNode = this._getWrapperFromChild(sourceNode);
|
||||
|
@ -128,6 +140,8 @@ class TagInputControl {
|
|||
}
|
||||
this._hideAutoComplete();
|
||||
this.tags = this.tags.filter(t => t.toLowerCase() != tag.toLowerCase());
|
||||
this.dispatchEvent(new TagInputEvent('remove', this));
|
||||
this.dispatchEvent(new TagInputEvent('change', this));
|
||||
this._sourceInputNode.value = this.tags.join(' ');
|
||||
for (let wrapperNode of this._getAllWrapperNodes()) {
|
||||
if (this._getTagFromWrapper(wrapperNode).toLowerCase() ==
|
||||
|
|
|
@ -5,9 +5,16 @@ const tags = require('../tags.js');
|
|||
const events = require('../events.js');
|
||||
const CommentList = require('./comment_list.js');
|
||||
|
||||
function _arraysDiffer(source1, source2) {
|
||||
return [...source1].filter(value => !source2.includes(value)).length > 0
|
||||
|| [...source2].filter(value => !source1.includes(value)).length > 0;
|
||||
}
|
||||
|
||||
class Post extends events.EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
this._orig = {};
|
||||
|
||||
this._id = null;
|
||||
this._type = null;
|
||||
this._mimeType = null;
|
||||
|
@ -31,27 +38,31 @@ class Post extends events.EventTarget {
|
|||
this._ownFavorite = null;
|
||||
}
|
||||
|
||||
get id() { return this._id; }
|
||||
get type() { return this._type; }
|
||||
get mimeType() { return this._mimeType; }
|
||||
get creationTime() { return this._creationTime; }
|
||||
get user() { return this._user; }
|
||||
get safety() { return this._safety; }
|
||||
get contentUrl() { return this._contentUrl; }
|
||||
get thumbnailUrl() { return this._thumbnailUrl; }
|
||||
get canvasWidth() { return this._canvasWidth || 800; }
|
||||
get canvasHeight() { return this._canvasHeight || 450; }
|
||||
get fileSize() { return this._fileSize || 0; }
|
||||
get id() { return this._id; }
|
||||
get type() { return this._type; }
|
||||
get mimeType() { return this._mimeType; }
|
||||
get creationTime() { return this._creationTime; }
|
||||
get user() { return this._user; }
|
||||
get safety() { return this._safety; }
|
||||
get contentUrl() { return this._contentUrl; }
|
||||
get thumbnailUrl() { return this._thumbnailUrl; }
|
||||
get canvasWidth() { return this._canvasWidth || 800; }
|
||||
get canvasHeight() { return this._canvasHeight || 450; }
|
||||
get fileSize() { return this._fileSize || 0; }
|
||||
|
||||
get tags() { return this._tags; }
|
||||
get notes() { return this._notes; }
|
||||
get comments() { return this._comments; }
|
||||
get relations() { return this._relations; }
|
||||
get tags() { return this._tags; }
|
||||
get notes() { return this._notes; }
|
||||
get comments() { return this._comments; }
|
||||
get relations() { return this._relations; }
|
||||
|
||||
get score() { return this._score; }
|
||||
get favoriteCount() { return this._favoriteCount; }
|
||||
get ownFavorite() { return this._ownFavorite; }
|
||||
get ownScore() { return this._ownScore; }
|
||||
get score() { return this._score; }
|
||||
get favoriteCount() { return this._favoriteCount; }
|
||||
get ownFavorite() { return this._ownFavorite; }
|
||||
get ownScore() { return this._ownScore; }
|
||||
|
||||
set tags(value) { this._tags = value; }
|
||||
set safety(value) { this._safety = value; }
|
||||
set relations(value) { this._relations = value; }
|
||||
|
||||
static fromResponse(response) {
|
||||
const ret = new Post();
|
||||
|
@ -90,15 +101,22 @@ class Post extends events.EventTarget {
|
|||
}
|
||||
|
||||
save() {
|
||||
let promise = null;
|
||||
let data = {
|
||||
tags: this._tags,
|
||||
};
|
||||
if (this._id) {
|
||||
promise = api.put('/post/' + this._id, data);
|
||||
} else {
|
||||
promise = api.post('/posts', data);
|
||||
const detail = {};
|
||||
|
||||
// send only changed fields to avoid user privilege violation
|
||||
if (this._safety !== this._orig._safety) {
|
||||
detail.safety = this._safety;
|
||||
}
|
||||
if (_arraysDiffer(this._tags, this._orig._tags)) {
|
||||
detail.tags = this._tags;
|
||||
}
|
||||
if (_arraysDiffer(this._relations, this._orig._relations)) {
|
||||
detail.relations = this._relations;
|
||||
}
|
||||
|
||||
let promise = this._id ?
|
||||
api.put('/post/' + this._id, detail) :
|
||||
api.post('/posts', detail);
|
||||
|
||||
return promise.then(response => {
|
||||
this._updateFromResponse(response);
|
||||
|
@ -180,27 +198,32 @@ class Post extends events.EventTarget {
|
|||
}
|
||||
|
||||
_updateFromResponse(response) {
|
||||
this._id = response.id;
|
||||
this._type = response.type;
|
||||
this._mimeType = response.mimeType;
|
||||
this._creationTime = response.creationTime;
|
||||
this._user = response.user;
|
||||
this._safety = response.safety;
|
||||
this._contentUrl = response.contentUrl;
|
||||
this._thumbnailUrl = response.thumbnailUrl;
|
||||
this._canvasWidth = response.canvasWidth;
|
||||
this._canvasHeight = response.canvasHeight;
|
||||
this._fileSize = response.fileSize;
|
||||
const map = {
|
||||
_id: response.id,
|
||||
_type: response.type,
|
||||
_mimeType: response.mimeType,
|
||||
_creationTime: response.creationTime,
|
||||
_user: response.user,
|
||||
_safety: response.safety,
|
||||
_contentUrl: response.contentUrl,
|
||||
_thumbnailUrl: response.thumbnailUrl,
|
||||
_canvasWidth: response.canvasWidth,
|
||||
_canvasHeight: response.canvasHeight,
|
||||
_fileSize: response.fileSize,
|
||||
|
||||
this._tags = response.tags;
|
||||
this._notes = response.notes;
|
||||
this._comments = CommentList.fromResponse(response.comments || []);
|
||||
this._relations = response.relations;
|
||||
_tags: response.tags,
|
||||
_notes: response.notes,
|
||||
_comments: CommentList.fromResponse(response.comments || []),
|
||||
_relations: response.relations,
|
||||
|
||||
this._score = response.score;
|
||||
this._favoriteCount = response.favoriteCount;
|
||||
this._ownScore = response.ownScore;
|
||||
this._ownFavorite = response.ownFavorite;
|
||||
_score: response.score,
|
||||
_favoriteCount: response.favoriteCount,
|
||||
_ownScore: response.ownScore,
|
||||
_ownFavorite: response.ownFavorite,
|
||||
};
|
||||
|
||||
Object.assign(this, map);
|
||||
Object.assign(this._orig, map);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -163,6 +163,10 @@ function formatUrlParameters(dict) {
|
|||
return result.join(';');
|
||||
}
|
||||
|
||||
function splitByWhitespace(str) {
|
||||
return str.split(/\s+/).filter(s => s);
|
||||
}
|
||||
|
||||
function parseUrlParameters(query) {
|
||||
let result = {};
|
||||
for (let word of (query || '').split(/;/)) {
|
||||
|
@ -254,4 +258,5 @@ module.exports = {
|
|||
confirmPageExit: confirmPageExit,
|
||||
escapeHtml: escapeHtml,
|
||||
makeCssName: makeCssName,
|
||||
splitByWhitespace: splitByWhitespace,
|
||||
};
|
||||
|
|
|
@ -2,15 +2,12 @@
|
|||
|
||||
const config = require('../config.js');
|
||||
const events = require('../events.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const views = require('../util/views.js');
|
||||
const TagInputControl = require('../controls/tag_input_control.js');
|
||||
|
||||
const template = views.getTemplate('tag-edit');
|
||||
|
||||
function _split(str) {
|
||||
return str.split(/\s+/).filter(s => s);
|
||||
}
|
||||
|
||||
class TagEditView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
|
@ -58,7 +55,7 @@ class TagEditView extends events.EventTarget {
|
|||
|
||||
_evtNameInput(e) {
|
||||
const regex = new RegExp(config.tagNameRegex);
|
||||
const list = this._namesFieldNode.value.split(/\s+/).filter(t => t);
|
||||
const list = misc.splitByWhitespace(this._namesFieldNode.value);
|
||||
|
||||
if (!list.length) {
|
||||
this._namesFieldNode.setCustomValidity(
|
||||
|
@ -82,10 +79,12 @@ class TagEditView extends events.EventTarget {
|
|||
this.dispatchEvent(new CustomEvent('submit', {
|
||||
detail: {
|
||||
tag: this._tag,
|
||||
names: _split(this._namesFieldNode.value),
|
||||
names: misc.splitByWhitespace(this._namesFieldNode.value),
|
||||
category: this._categoryFieldNode.value,
|
||||
implications: _split(this._implicationsFieldNode.value),
|
||||
suggestions: _split(this._suggestionsFieldNode.value),
|
||||
implications: misc.splitByWhitespace(
|
||||
this._implicationsFieldNode.value),
|
||||
suggestions: misc.splitByWhitespace(
|
||||
this._suggestionsFieldNode.value),
|
||||
description: this._descriptionFieldNode.value,
|
||||
},
|
||||
}));
|
||||
|
|
Loading…
Reference in New Issue