diff --git a/client/css/post-list-view.styl b/client/css/post-list-view.styl
index ed57fe97..10ebb6df 100644
--- a/client/css/post-list-view.styl
+++ b/client/css/post-list-view.styl
@@ -84,6 +84,37 @@
&[data-disabled]
background: rgba(200, 200, 200, 0.7)
+ .safety-flipper a
+ display: inline-block
+ margin: 0.1em
+ box-sizing: border-box
+ border: 0
+ display: inline-block
+ width: 1.2em
+ height: 1.2em
+ text-align: center
+ line-height: 1em
+ font-size: 20pt
+ border: 3px solid
+ &.safety-safe
+ background-color: darken($safety-safe, 5%)
+ border-color: @background-color
+ &:not(.active)
+ background-color: alpha(@background-color, 0.3)
+ &.safety-sketchy
+ background-color: $safety-sketchy
+ border-color: @background-color
+ &:not(.active)
+ background-color: alpha(@background-color, 0.3)
+ &.safety-unsafe
+ background-color: $safety-unsafe
+ border-color: @background-color
+ &:not(.active)
+ background-color: alpha(@background-color, 0.3)
+ &[data-disabled]
+ background: rgba(200, 200, 200, 0.7)
+
+
.thumbnail
background-position: 50% 30%
width: 100%
@@ -122,20 +153,25 @@
.append
font-size: 0.95em
color: $inactive-link-color
- .bulk-edit-tags
+ .bulk-edit
&:not(.opened)
- [type=text],
- .start,
.close
display: none
- .hint
- display: none
&.opened
.open
display: none
+ &.hidden
+ display: none
+ .bulk-edit-tags
+ &:not(.opened)
+ [type=text],
+ .start
+ display: none
+ .hint
+ display: none
input[name=tag]
width: 12em
- .hint, .open
+ .hint
margin-right: 1em
.safety
diff --git a/client/html/posts_header.tpl b/client/html/posts_header.tpl
index b85b784a..29efd520 100644
--- a/client/html/posts_header.tpl
+++ b/client/html/posts_header.tpl
@@ -11,7 +11,7 @@
%>'>Syntax help<%
%><%
%><% if (ctx.canBulkEditTags) { %><%
- %>
<%
%><% } %><%
+ %><% if (ctx.canBulkEditSafety) { %><%
+ %><%
+ %><% } %><%
%>
diff --git a/client/html/posts_page.tpl b/client/html/posts_page.tpl
index a6c6cc55..895de559 100644
--- a/client/html/posts_page.tpl
+++ b/client/html/posts_page.tpl
@@ -2,7 +2,7 @@
<% if (ctx.response.results.length) { %>
<% for (let post of ctx.response.results) { %>
- -
+
-
'
href='<%= ctx.canViewPosts ? ctx.getPostUrl(post.id, ctx.parameters) : '' %>'>
@@ -35,9 +35,17 @@
<% if (ctx.canBulkEditTags && ctx.parameters && ctx.parameters.tag) { %>
-
+
<% } %>
+ <% if (ctx.canBulkEditSafety && ctx.parameters && ctx.parameters.safety) { %>
+
+ <% for (let safety of ['safe', 'sketchy', 'unsafe']) { %>
+ '>
+
+ <% } %>
+
+ <% } %>
<% } %>
diff --git a/client/js/controllers/post_list_controller.js b/client/js/controllers/post_list_controller.js
index a0973e19..12b354f9 100644
--- a/client/js/controllers/post_list_controller.js
+++ b/client/js/controllers/post_list_controller.js
@@ -11,7 +11,7 @@ const PostsPageView = require('../views/posts_page_view.js');
const EmptyView = require('../views/empty_view.js');
const fields = [
- 'id', 'thumbnailUrl', 'type',
+ 'id', 'thumbnailUrl', 'type', 'safety',
'score', 'favoriteCount', 'commentCount', 'tags', 'version'];
class PostListController {
@@ -32,6 +32,7 @@ class PostListController {
hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters,
canBulkEditTags: api.hasPrivilege('posts:bulkEdit:tags'),
+ canBulkEditSafety: api.hasPrivilege('posts:bulkEdit:safety'),
bulkEdit: {
tags: this._bulkEditTags
},
@@ -73,6 +74,11 @@ class PostListController {
e.detail.post.save().catch(error => window.alert(error.message));
}
+ _evtChangeSafety(e) {
+ e.detail.post.safety = e.detail.safety;
+ e.detail.post.save().catch(error => window.alert(error.message));
+ }
+
_decorateSearchQuery(text) {
const browsingSettings = settings.get();
let disabledSafety = [];
@@ -106,6 +112,8 @@ class PostListController {
Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege('posts:view'),
canBulkEditTags: api.hasPrivilege('posts:bulkEdit:tags'),
+ canBulkEditSafety:
+ api.hasPrivilege('posts:bulkEdit:safety'),
bulkEdit: {
tags: this._bulkEditTags,
},
@@ -113,6 +121,8 @@ class PostListController {
const view = new PostsPageView(pageCtx);
view.addEventListener('tag', e => this._evtTag(e));
view.addEventListener('untag', e => this._evtUntag(e));
+ view.addEventListener(
+ 'changeSafety', e => this._evtChangeSafety(e));
return view;
},
});
diff --git a/client/js/views/posts_header_view.js b/client/js/views/posts_header_view.js
index 0bc1dce9..efa3d02c 100644
--- a/client/js/views/posts_header_view.js
+++ b/client/js/views/posts_header_view.js
@@ -11,26 +11,19 @@ const TagAutoCompleteControl =
const template = views.getTemplate('posts-header');
-class BulkTagEditor extends events.EventTarget {
+class BulkEditor extends events.EventTarget {
constructor(hostNode) {
super();
this._hostNode = hostNode;
-
- this._autoCompleteControl = new TagAutoCompleteControl(
- this._inputNode, {addSpace: false});
this._openLinkNode.addEventListener(
'click', e => this._evtOpenLinkClick(e));
this._closeLinkNode.addEventListener(
'click', e => this._evtCloseLinkClick(e));
- this._hostNode.addEventListener('submit', e => this._evtFormSubmit(e));
- }
-
- get value() {
- return this._inputNode.value;
}
get opened() {
- return this._hostNode.classList.contains('opened');
+ return this._hostNode.classList.contains('opened') &&
+ !this._hostNode.classList.contains('hidden');
}
get _openLinkNode() {
@@ -41,6 +34,53 @@ class BulkTagEditor extends events.EventTarget {
return this._hostNode.querySelector('.close');
}
+ toggleOpen(state) {
+ this._hostNode.classList.toggle('opened', state);
+ }
+
+ toggleHide(state) {
+ this._hostNode.classList.toggle('hidden', state);
+ }
+
+ _evtOpenLinkClick(e) {
+ throw new Error('Not implemented');
+ }
+
+ _evtCloseLinkClick(e) {
+ throw new Error('Not implemented');
+ }
+}
+
+class BulkSafetyEditor extends BulkEditor {
+ constructor(hostNode) {
+ super(hostNode);
+ }
+
+ _evtOpenLinkClick(e) {
+ e.preventDefault();
+ this.toggleOpen(true);
+ this.dispatchEvent(new CustomEvent('open', {detail: {}}));
+ }
+
+ _evtCloseLinkClick(e) {
+ e.preventDefault();
+ this.toggleOpen(false);
+ this.dispatchEvent(new CustomEvent('close', {detail: {}}));
+ }
+}
+
+class BulkTagEditor extends BulkEditor {
+ constructor(hostNode) {
+ super(hostNode);
+ this._autoCompleteControl = new TagAutoCompleteControl(
+ this._inputNode, {addSpace: false});
+ this._hostNode.addEventListener('submit', e => this._evtFormSubmit(e));
+ }
+
+ get value() {
+ return this._inputNode.value;
+ }
+
get _inputNode() {
return this._hostNode.querySelector('input[name=tag]');
}
@@ -54,10 +94,6 @@ class BulkTagEditor extends events.EventTarget {
this._inputNode.blur();
}
- toggleOpen(state) {
- this._hostNode.classList.toggle('opened', state);
- }
-
_evtFormSubmit(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {detail: {}}));
@@ -99,18 +135,38 @@ class PostsHeaderView extends events.EventTarget {
safetyButtonNode.addEventListener(
'click', e => this._evtSafetyButtonClick(e));
}
- this._formNode.addEventListener(
- 'submit', e => this._evtFormSubmit(e));
+ this._formNode.addEventListener('submit', e => this._evtFormSubmit(e));
+ this._bulkEditors = [];
if (this._bulkEditTagsNode) {
this._bulkTagEditor = new BulkTagEditor(this._bulkEditTagsNode);
- this._bulkTagEditor.toggleOpen(!!ctx.parameters.tag);
+ this._bulkEditors.push(this._bulkTagEditor);
+ }
+
+ if (this._bulkEditSafetyNode) {
+ this._bulkSafetyEditor = new BulkSafetyEditor(
+ this._bulkEditSafetyNode);
+ this._bulkEditors.push(this._bulkSafetyEditor);
+ }
+
+ for (let editor of this._bulkEditors) {
this._bulkTagEditor.addEventListener('submit', e => {
this._navigate();
});
- this._bulkTagEditor.addEventListener('close', e => {
+ editor.addEventListener('open', e => {
+ this._hideBulkEditorsExcept(editor);
this._navigate();
});
+ editor.addEventListener('close', e => {
+ this._closeAndShowAllBulkEditors();
+ this._navigate();
+ });
+ }
+
+ if (ctx.parameters.tag && this._bulkTagEditor) {
+ this._openBulkEditor(this._bulkTagEditor);
+ } else if (ctx.parameters.safety && this._bulkSafetyEditor) {
+ this._openBulkEditor(this._bulkSafetyEditor);
}
}
@@ -130,6 +186,31 @@ class PostsHeaderView extends events.EventTarget {
return this._hostNode.querySelector('.bulk-edit-tags');
}
+ get _bulkEditSafetyNode() {
+ return this._hostNode.querySelector('.bulk-edit-safety');
+ }
+
+ _openBulkEditor(editor) {
+ editor.toggleOpen(true);
+ this._hideBulkEditorsExcept(editor);
+ }
+
+ _hideBulkEditorsExcept(editor) {
+ for (let otherEditor of this._bulkEditors) {
+ if (otherEditor !== editor) {
+ otherEditor.toggleOpen(false);
+ otherEditor.toggleHide(true);
+ }
+ }
+ }
+
+ _closeAndShowAllBulkEditors() {
+ for (let otherEditor of this._bulkEditors) {
+ otherEditor.toggleOpen(false);
+ otherEditor.toggleHide(false);
+ }
+ }
+
_evtSafetyButtonClick(e, url) {
e.preventDefault();
e.target.classList.toggle('disabled');
@@ -164,6 +245,9 @@ class PostsHeaderView extends events.EventTarget {
} else {
parameters.tag = null;
}
+ parameters.safety = (
+ this._bulkSafetyEditor &&
+ this._bulkSafetyEditor.opened ? '1' : null);
this.dispatchEvent(
new CustomEvent('navigate', {detail: {parameters: parameters}}));
}
diff --git a/client/js/views/posts_page_view.js b/client/js/views/posts_page_view.js
index c2dbe904..26103251 100644
--- a/client/js/views/posts_page_view.js
+++ b/client/js/views/posts_page_view.js
@@ -18,26 +18,48 @@ class PostsPageView extends events.EventTarget {
post.addEventListener('change', e => this._evtPostChange(e));
}
- this._postIdToLinkNode = {};
- for (let linkNode of this._tagFlipperNodes) {
- const postId = linkNode.getAttribute('data-post-id');
+ this._postIdToListItemNode = {};
+ for (let listItemNode of this._listItemNodes) {
+ const postId = listItemNode.getAttribute('data-post-id');
const post = this._postIdToPost[postId];
- this._postIdToLinkNode[postId] = linkNode;
- linkNode.addEventListener(
- 'click', e => this._evtBulkEditTagsClick(e, post));
+ this._postIdToListItemNode[postId] = listItemNode;
+
+ const tagFlipperNode = this._getTagFlipperNode(listItemNode);
+ if (tagFlipperNode) {
+ tagFlipperNode.addEventListener(
+ 'click', e => this._evtBulkEditTagsClick(e, post));
+ }
+
+ const safetyFlipperNode = this._getSafetyFlipperNode(listItemNode);
+ if (safetyFlipperNode) {
+ for (let linkNode of safetyFlipperNode.querySelectorAll('a')) {
+ linkNode.addEventListener(
+ 'click', e => this._evtBulkEditSafetyClick(e, post));
+ }
+ }
}
- this._syncTagFlippersHighlights();
+ this._syncBulkEditorsHighlights();
}
- get _tagFlipperNodes() {
- return this._hostNode.querySelectorAll('.tag-flipper');
+ get _listItemNodes() {
+ return this._hostNode.querySelectorAll('li');
+ }
+
+ _getTagFlipperNode(listItemNode) {
+ return listItemNode.querySelector('.tag-flipper');
+ }
+
+ _getSafetyFlipperNode(listItemNode) {
+ return listItemNode.querySelector('.safety-flipper');
}
_evtPostChange(e) {
- const linkNode = this._postIdToLinkNode[e.detail.post.id];
- linkNode.removeAttribute('data-disabled');
- this._syncTagFlippersHighlights();
+ const listItemNode = this._postIdToListItemNode[e.detail.post.id];
+ for (let node of listItemNode.querySelectorAll('[data-disabled]')) {
+ node.removeAttribute('data-disabled');
+ }
+ this._syncBulkEditorsHighlights();
}
_evtBulkEditTagsClick(e, post) {
@@ -53,15 +75,43 @@ class PostsPageView extends events.EventTarget {
{detail: {post: post}}));
}
- _syncTagFlippersHighlights() {
- for (let linkNode of this._tagFlipperNodes) {
- const postId = linkNode.getAttribute('data-post-id');
+ _evtBulkEditSafetyClick(e, post) {
+ e.preventDefault();
+ const linkNode = e.target;
+ if (linkNode.getAttribute('data-disabled')) {
+ return;
+ }
+ const newSafety = linkNode.getAttribute('data-safety');
+ if (post.safety === newSafety) {
+ return;
+ }
+ linkNode.setAttribute('data-disabled', true);
+ this.dispatchEvent(
+ new CustomEvent(
+ 'changeSafety', {detail: {post: post, safety: newSafety}}));
+ }
+
+ _syncBulkEditorsHighlights() {
+ for (let listItemNode of this._listItemNodes) {
+ const postId = listItemNode.getAttribute('data-post-id');
const post = this._postIdToPost[postId];
- let tagged = true;
- for (let tag of this._ctx.bulkEdit.tags) {
- tagged = tagged & post.isTaggedWith(tag);
+
+ const tagFlipperNode = this._getTagFlipperNode(listItemNode);
+ if (tagFlipperNode) {
+ let tagged = true;
+ for (let tag of this._ctx.bulkEdit.tags) {
+ tagged = tagged & post.isTaggedWith(tag);
+ }
+ tagFlipperNode.classList.toggle('tagged', tagged);
+ }
+
+ const safetyFlipperNode = this._getSafetyFlipperNode(listItemNode);
+ if (safetyFlipperNode) {
+ for (let linkNode of safetyFlipperNode.querySelectorAll('a')) {
+ const safety = linkNode.getAttribute('data-safety');
+ linkNode.classList.toggle('active', post.safety == safety);
+ }
}
- linkNode.classList.toggle('tagged', tagged);
}
}
}
diff --git a/config.yaml.dist b/config.yaml.dist
index 2b03c0c8..9d50e50c 100644
--- a/config.yaml.dist
+++ b/config.yaml.dist
@@ -90,6 +90,7 @@ privileges:
'posts:merge': moderator
'posts:favorite': regular
'posts:bulk-edit:tags': power
+ 'posts:bulk-edit:safety': power
'tags:create': regular
'tags:edit:names': power