client/posts: add mass tag
This commit is contained in:
parent
99011b02d7
commit
fccedc090f
16 changed files with 322 additions and 64 deletions
|
@ -51,7 +51,7 @@ a
|
||||||
position: absolute
|
position: absolute
|
||||||
visibility: hidden
|
visibility: hidden
|
||||||
|
|
||||||
a.append
|
a.append, span.append
|
||||||
margin-left: 1em
|
margin-left: 1em
|
||||||
form .fa-question-circle-o
|
form .fa-question-circle-o
|
||||||
font-size: 110%
|
font-size: 110%
|
||||||
|
|
|
@ -67,7 +67,7 @@ $safety-unsafe = #F3985F
|
||||||
min-height: 7.5em
|
min-height: 7.5em
|
||||||
height: 9vw
|
height: 9vw
|
||||||
|
|
||||||
a
|
.thumbnail-wrapper
|
||||||
display: inline-block
|
display: inline-block
|
||||||
width: 100%
|
width: 100%
|
||||||
height: 100%
|
height: 100%
|
||||||
|
@ -99,6 +99,35 @@ $safety-unsafe = #F3985F
|
||||||
.icon:not(:first-of-type)
|
.icon:not(:first-of-type)
|
||||||
margin-left: 1em
|
margin-left: 1em
|
||||||
|
|
||||||
|
.masstag
|
||||||
|
position: absolute
|
||||||
|
top: 0.5em
|
||||||
|
left: 0.5em
|
||||||
|
display: inline-block
|
||||||
|
padding: 0.5em
|
||||||
|
box-sizing: border-box
|
||||||
|
border: 0
|
||||||
|
cursor: pointer
|
||||||
|
&:after
|
||||||
|
display: inline-block
|
||||||
|
width: 1em
|
||||||
|
height: 1em
|
||||||
|
text-align: center
|
||||||
|
line-height: 1em
|
||||||
|
font-size: 20pt
|
||||||
|
&.tagged
|
||||||
|
background: rgba(0, 230, 0, 0.7)
|
||||||
|
&:after
|
||||||
|
color: white
|
||||||
|
content: '-'
|
||||||
|
&:not(.tagged)
|
||||||
|
background: rgba(255, 0, 0, 0.7)
|
||||||
|
&:after
|
||||||
|
color: white
|
||||||
|
content: '+'
|
||||||
|
&[data-disabled]
|
||||||
|
background: rgba(200, 200, 200, 0.7)
|
||||||
|
|
||||||
.thumbnail
|
.thumbnail
|
||||||
background-position: 50% 30%
|
background-position: 50% 30%
|
||||||
width: 100%
|
width: 100%
|
||||||
|
@ -122,12 +151,23 @@ $safety-unsafe = #F3985F
|
||||||
text-align: left
|
text-align: left
|
||||||
form
|
form
|
||||||
width: auto
|
width: auto
|
||||||
|
*
|
||||||
|
vertical-align: top
|
||||||
input[name=search-text]
|
input[name=search-text]
|
||||||
width: 25em
|
width: 25em
|
||||||
max-width: 90vw
|
max-width: 90vw
|
||||||
|
input[name=masstag]
|
||||||
|
width: 15em
|
||||||
|
margin-left: 1em
|
||||||
.append
|
.append
|
||||||
font-size: 0.95em
|
font-size: 0.95em
|
||||||
color: $inactive-link-color
|
color: $inactive-link-color
|
||||||
|
.masstag
|
||||||
|
&:not(.active)
|
||||||
|
[type=text],
|
||||||
|
.start-tagging,
|
||||||
|
.stop-tagging
|
||||||
|
display: none
|
||||||
|
|
||||||
.safety
|
.safety
|
||||||
&.safety-safe
|
&.safety-safe
|
||||||
|
@ -148,7 +188,7 @@ $safety-unsafe = #F3985F
|
||||||
|
|
||||||
.post-container
|
.post-container
|
||||||
.post-content.transparency-grid img
|
.post-content.transparency-grid img
|
||||||
background: url('/img/transparency_grid.png');
|
background: url('/img/transparency_grid.png')
|
||||||
|
|
||||||
text-align: center
|
text-align: center
|
||||||
.post-content
|
.post-content
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
<div class='post-list-header'>
|
<div class='post-list-header'>
|
||||||
<form class='horizontal'>
|
<form class='horizontal search'>
|
||||||
<div class='input'>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.searchQuery.text}) %>
|
<%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.searchQuery.text}) %>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class='buttons'>
|
|
||||||
<input class='mousetrap' type='submit' value='Search'/>
|
<input class='mousetrap' type='submit' value='Search'/>
|
||||||
<input data-safety=safe type='button' class='mousetrap safety safety-safe <%- ctx.settings.listPosts.safe ? '' : 'disabled' %>'/>
|
<input data-safety=safe type='button' class='mousetrap safety safety-safe <%- ctx.settings.listPosts.safe ? '' : 'disabled' %>'/>
|
||||||
<input data-safety=sketchy type='button' class='mousetrap safety safety-sketchy <%- ctx.settings.listPosts.sketchy ? '' : 'disabled' %>'/>
|
<input data-safety=sketchy type='button' class='mousetrap safety safety-sketchy <%- ctx.settings.listPosts.sketchy ? '' : 'disabled' %>'/>
|
||||||
<input data-safety=unsafe type='button' class='mousetrap safety safety-unsafe <%- ctx.settings.listPosts.unsafe ? '' : 'disabled' %>'/>
|
<input data-safety=unsafe type='button' class='mousetrap safety safety-unsafe <%- ctx.settings.listPosts.unsafe ? '' : 'disabled' %>'/>
|
||||||
<a class='mousetrap button append' href='/help/search/posts'>Syntax help</a>
|
<a class='mousetrap button append' href='/help/search/posts'>Syntax help</a>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
<% if (ctx.canMassTag) { %>
|
||||||
|
<form class='masstag horizontal'>
|
||||||
|
<% if (ctx.searchQuery.tag) { %>
|
||||||
|
<span class='append'>Tagging with:</span>
|
||||||
|
<% } else { %>
|
||||||
|
<a class='mousetrap button append open-masstag' href='#'>Mass tag</a>
|
||||||
|
<% } %>
|
||||||
|
<%= ctx.makeTextInput({name: 'masstag', value: ctx.searchQuery.tag}) %>
|
||||||
|
<input class='mousetrap start-tagging' type='submit' value='Start tagging'/>
|
||||||
|
<a class='mousetrap button append stop-tagging' href='#'>Stop tagging</a>
|
||||||
|
</form>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
<li>
|
<li>
|
||||||
<% if (ctx.canViewPosts) { %>
|
<% if (ctx.canViewPosts) { %>
|
||||||
<% if (ctx.searchQuery && ctx.searchQuery.text) { %>
|
<% if (ctx.searchQuery && ctx.searchQuery.text) { %>
|
||||||
<a href='/post/<%- encodeURIComponent(post.id) %>/text=<%- encodeURIComponent(ctx.searchQuery.text) %>' title='@<%- post.id %> (<%- post.type %>) Tags: <%- post.tags.map(tag => '#' + tag).join(' ') %>'>
|
<a class='thumbnail-wrapper' href='/post/<%- encodeURIComponent(post.id) %>/text=<%- encodeURIComponent(ctx.searchQuery.text) %>' title='@<%- post.id %> (<%- post.type %>) Tags: <%- post.tags.map(tag => '#' + tag).join(' ') %>'>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a href='/post/<%- encodeURIComponent(post.id) %>' title='@<%- post.id %> (<%- post.type %>) Tags: <%- post.tags.map(tag => '#' + tag).join(' ') %>'>
|
<a class='thumbnail-wrapper' href='/post/<%- encodeURIComponent(post.id) %>' title='@<%- post.id %> (<%- post.type %>) Tags: <%- post.tags.map(tag => '#' + tag).join(' ') %>'>
|
||||||
<% } %>
|
<% } %>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a>
|
<a class='thumbnail-wrapper'>
|
||||||
<% } %>
|
<% } %>
|
||||||
<%= ctx.makeThumbnail(post.thumbnailUrl) %>
|
<%= ctx.makeThumbnail(post.thumbnailUrl) %>
|
||||||
<span class='type' data-type='<%- post.type %>'>
|
<span class='type' data-type='<%- post.type %>'>
|
||||||
|
@ -39,6 +39,10 @@
|
||||||
</span>
|
</span>
|
||||||
<% } %>
|
<% } %>
|
||||||
</a>
|
</a>
|
||||||
|
<% if (ctx.searchQuery && ctx.searchQuery.tag) { %>
|
||||||
|
<a data-post-id='<%= post.id %>' class='masstag'>
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
</li>
|
</li>
|
||||||
<% } %>
|
<% } %>
|
||||||
<%= ctx.makeFlexboxAlign() %>
|
<%= ctx.makeFlexboxAlign() %>
|
||||||
|
|
|
@ -15,7 +15,11 @@ class CommentsController {
|
||||||
|
|
||||||
this._pageController = new PageController({
|
this._pageController = new PageController({
|
||||||
searchQuery: ctx.searchQuery,
|
searchQuery: ctx.searchQuery,
|
||||||
clientUrl: '/comments/' + misc.formatSearchQuery({page: '{page}'}),
|
getClientUrlForPage: page => {
|
||||||
|
const searchQuery = Object.assign(
|
||||||
|
{}, ctx.searchQuery, {page: page});
|
||||||
|
return '/comments/' + misc.formatSearchQuery(searchQuery);
|
||||||
|
},
|
||||||
requestPage: page => {
|
requestPage: page => {
|
||||||
return PostList.search(
|
return PostList.search(
|
||||||
'sort:comment-date+comment-count-min:1', page, 10, fields);
|
'sort:comment-date+comment-count-min:1', page, 10, fields);
|
||||||
|
|
|
@ -7,7 +7,7 @@ const ManualPageView = require('../views/manual_page_view.js');
|
||||||
class PageController {
|
class PageController {
|
||||||
constructor(ctx) {
|
constructor(ctx) {
|
||||||
const extendedContext = {
|
const extendedContext = {
|
||||||
clientUrl: ctx.clientUrl,
|
getClientUrlForPage: ctx.getClientUrlForPage,
|
||||||
searchQuery: ctx.searchQuery,
|
searchQuery: ctx.searchQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,27 +17,63 @@ class PostListController {
|
||||||
constructor(ctx) {
|
constructor(ctx) {
|
||||||
topNavigation.activate('posts');
|
topNavigation.activate('posts');
|
||||||
|
|
||||||
|
this._ctx = ctx;
|
||||||
this._pageController = new PageController({
|
this._pageController = new PageController({
|
||||||
searchQuery: ctx.searchQuery,
|
searchQuery: ctx.searchQuery,
|
||||||
clientUrl: '/posts/' + misc.formatSearchQuery({
|
getClientUrlForPage: page => {
|
||||||
text: ctx.searchQuery.text, page: '{page}'}),
|
const searchQuery = Object.assign(
|
||||||
|
{}, ctx.searchQuery, {page: page});
|
||||||
|
return '/posts/' + misc.formatSearchQuery(searchQuery);
|
||||||
|
},
|
||||||
requestPage: page => {
|
requestPage: page => {
|
||||||
return PostList.search(
|
return PostList.search(
|
||||||
this._decorateSearchQuery(ctx.searchQuery.text),
|
this._decorateSearchQuery(ctx.searchQuery.text),
|
||||||
page, 40, fields);
|
page, 40, fields);
|
||||||
},
|
},
|
||||||
headerRenderer: headerCtx => {
|
headerRenderer: headerCtx => {
|
||||||
|
Object.assign(headerCtx, {
|
||||||
|
canMassTag: api.hasPrivilege('tags:masstag'),
|
||||||
|
massTagTags: this._massTagTags,
|
||||||
|
});
|
||||||
return new PostsHeaderView(headerCtx);
|
return new PostsHeaderView(headerCtx);
|
||||||
},
|
},
|
||||||
pageRenderer: pageCtx => {
|
pageRenderer: pageCtx => {
|
||||||
Object.assign(pageCtx, {
|
Object.assign(pageCtx, {
|
||||||
canViewPosts: api.hasPrivilege('posts:view'),
|
canViewPosts: api.hasPrivilege('posts:view'),
|
||||||
|
massTagTags: this._massTagTags,
|
||||||
});
|
});
|
||||||
return new PostsPageView(pageCtx);
|
const view = new PostsPageView(pageCtx);
|
||||||
|
view.addEventListener('tag', e => this._evtTag(e));
|
||||||
|
view.addEventListener('untag', e => this._evtUntag(e));
|
||||||
|
return view;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get _massTagTags() {
|
||||||
|
return (this._ctx.searchQuery.tag || '').split(/\s+/).filter(s => s);
|
||||||
|
}
|
||||||
|
|
||||||
|
_evtTag(e) {
|
||||||
|
for (let tag of this._massTagTags) {
|
||||||
|
e.detail.post.addTag(tag);
|
||||||
|
}
|
||||||
|
e.detail.post.save()
|
||||||
|
.catch(errorMessage => {
|
||||||
|
window.alert(errorMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_evtUntag(e) {
|
||||||
|
for (let tag of this._massTagTags) {
|
||||||
|
e.detail.post.removeTag(tag);
|
||||||
|
}
|
||||||
|
e.detail.post.save()
|
||||||
|
.catch(errorMessage => {
|
||||||
|
window.alert(errorMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_decorateSearchQuery(text) {
|
_decorateSearchQuery(text) {
|
||||||
const browsingSettings = settings.get();
|
const browsingSettings = settings.get();
|
||||||
let disabledSafety = [];
|
let disabledSafety = [];
|
||||||
|
|
|
@ -17,8 +17,11 @@ class TagListController {
|
||||||
|
|
||||||
this._pageController = new PageController({
|
this._pageController = new PageController({
|
||||||
searchQuery: ctx.searchQuery,
|
searchQuery: ctx.searchQuery,
|
||||||
clientUrl: '/tags/' + misc.formatSearchQuery({
|
getClientUrlForPage: page => {
|
||||||
text: ctx.searchQuery.text, page: '{page}'}),
|
const searchQuery = Object.assign(
|
||||||
|
{}, ctx.searchQuery, {page: page});
|
||||||
|
return '/tags/' + misc.formatSearchQuery(searchQuery);
|
||||||
|
},
|
||||||
requestPage: page => {
|
requestPage: page => {
|
||||||
return TagList.search(ctx.searchQuery.text, page, 50, fields);
|
return TagList.search(ctx.searchQuery.text, page, 50, fields);
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,8 +14,11 @@ class UserListController {
|
||||||
|
|
||||||
this._pageController = new PageController({
|
this._pageController = new PageController({
|
||||||
searchQuery: ctx.searchQuery,
|
searchQuery: ctx.searchQuery,
|
||||||
clientUrl: '/users/' + misc.formatSearchQuery({
|
getClientUrlForPage: page => {
|
||||||
text: ctx.searchQuery.text, page: '{page}'}),
|
const searchQuery = Object.assign(
|
||||||
|
{}, ctx.searchQuery, {page: page});
|
||||||
|
return '/users/' + misc.formatSearchQuery(searchQuery);
|
||||||
|
},
|
||||||
requestPage: page => {
|
requestPage: page => {
|
||||||
return UserList.search(ctx.searchQuery.text, page);
|
return UserList.search(ctx.searchQuery.text, page);
|
||||||
},
|
},
|
||||||
|
|
|
@ -105,18 +105,16 @@ class TagInputControl {
|
||||||
this._editAreaNode.insertBefore(targetWrapperNode, sourceWrapperNode);
|
this._editAreaNode.insertBefore(targetWrapperNode, sourceWrapperNode);
|
||||||
this._editAreaNode.insertBefore(this._createSpace(), sourceWrapperNode);
|
this._editAreaNode.insertBefore(this._createSpace(), sourceWrapperNode);
|
||||||
|
|
||||||
const actualTag = tags.getTagByName(text) || {};
|
|
||||||
|
|
||||||
// XXX: perhaps we should aggregate suggestions from all implications
|
// XXX: perhaps we should aggregate suggestions from all implications
|
||||||
// for call to the _suggestRelations
|
// for call to the _suggestRelations
|
||||||
if (addImplications) {
|
if (addImplications) {
|
||||||
for (let otherTag of (actualTag.implications || [])) {
|
for (let otherTag of tags.getAllImplications(text)) {
|
||||||
this.addTag(otherTag, sourceNode, true, false);
|
this.addTag(otherTag, sourceNode, true, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (suggestRelations) {
|
if (suggestRelations) {
|
||||||
this._suggestRelations([], actualTag.suggestions || []);
|
this._suggestRelations([], tags.getSuggestions(text) || []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
|
const tags = require('../tags.js');
|
||||||
const events = require('../events.js');
|
const events = require('../events.js');
|
||||||
const CommentList = require('./comment_list.js');
|
const CommentList = require('./comment_list.js');
|
||||||
|
|
||||||
|
@ -67,6 +68,48 @@ class Post extends events.EventTarget {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isTaggedWith(tagName) {
|
||||||
|
return this._tags.map(s => s.toLowerCase()).includes(tagName);
|
||||||
|
}
|
||||||
|
|
||||||
|
addTag(tagName, addImplications) {
|
||||||
|
if (this.isTaggedWith(tagName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._tags.push(tagName);
|
||||||
|
if (addImplications !== false) {
|
||||||
|
for (let otherTag of tags.getAllImplications(tagName)) {
|
||||||
|
this.addTag(otherTag, addImplications);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTag(tagName) {
|
||||||
|
this._tags = this._tags.filter(
|
||||||
|
s => s.toLowerCase() != tagName.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then(response => {
|
||||||
|
this._updateFromResponse(response);
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('change', {detail: {post: this}}));
|
||||||
|
return Promise.resolve();
|
||||||
|
}, response => {
|
||||||
|
return Promise.reject(response.description);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setScore(score) {
|
setScore(score) {
|
||||||
return api.put('/post/' + this._id + '/score', {score: score})
|
return api.put('/post/' + this._id + '/score', {score: score})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
|
|
@ -89,6 +89,17 @@ function refreshExport() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAllImplications(tagName) {
|
||||||
|
const actualTag = getTagByName(tagName) || {};
|
||||||
|
// TODO: recursive
|
||||||
|
return actualTag.implications || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSuggestions(tagName) {
|
||||||
|
const actualTag = getTagByName(tagName) || {};
|
||||||
|
return actualTag.suggestions || [];
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getAllCategories: getAllCategories,
|
getAllCategories: getAllCategories,
|
||||||
getAllTags: getAllTags,
|
getAllTags: getAllTags,
|
||||||
|
@ -97,4 +108,6 @@ module.exports = {
|
||||||
getNameToTagMap: getNameToTagMap,
|
getNameToTagMap: getNameToTagMap,
|
||||||
getOriginalTagName: getOriginalTagName,
|
getOriginalTagName: getOriginalTagName,
|
||||||
refreshExport: refreshExport,
|
refreshExport: refreshExport,
|
||||||
|
getAllImplications: getAllImplications,
|
||||||
|
getSuggestions: getSuggestions,
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,10 +6,6 @@ const views = require('../util/views.js');
|
||||||
const holderTemplate = views.getTemplate('endless-pager');
|
const holderTemplate = views.getTemplate('endless-pager');
|
||||||
const pageTemplate = views.getTemplate('endless-pager-page');
|
const pageTemplate = views.getTemplate('endless-pager-page');
|
||||||
|
|
||||||
function _formatUrl(url, page) {
|
|
||||||
return url.replace('{page}', page);
|
|
||||||
}
|
|
||||||
|
|
||||||
class EndlessPageView {
|
class EndlessPageView {
|
||||||
constructor(ctx) {
|
constructor(ctx) {
|
||||||
this._hostNode = document.getElementById('content-holder');
|
this._hostNode = document.getElementById('content-holder');
|
||||||
|
@ -68,7 +64,7 @@ class EndlessPageView {
|
||||||
let topPageNumber = parseInt(topPageNode.getAttribute('data-page'));
|
let topPageNumber = parseInt(topPageNode.getAttribute('data-page'));
|
||||||
if (topPageNumber !== this.currentPage) {
|
if (topPageNumber !== this.currentPage) {
|
||||||
router.replace(
|
router.replace(
|
||||||
_formatUrl(ctx.clientUrl, topPageNumber),
|
ctx.getClientUrlForPage(topPageNumber),
|
||||||
{},
|
{},
|
||||||
false);
|
false);
|
||||||
this.currentPage = topPageNumber;
|
this.currentPage = topPageNumber;
|
||||||
|
|
|
@ -8,10 +8,6 @@ const views = require('../util/views.js');
|
||||||
const holderTemplate = views.getTemplate('manual-pager');
|
const holderTemplate = views.getTemplate('manual-pager');
|
||||||
const navTemplate = views.getTemplate('manual-pager-nav');
|
const navTemplate = views.getTemplate('manual-pager-nav');
|
||||||
|
|
||||||
function _formatUrl(url, page) {
|
|
||||||
return url.replace('{page}', page);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _removeConsecutiveDuplicates(a) {
|
function _removeConsecutiveDuplicates(a) {
|
||||||
return a.filter((item, pos, ary) => {
|
return a.filter((item, pos, ary) => {
|
||||||
return !pos || item != ary[pos - 1];
|
return !pos || item != ary[pos - 1];
|
||||||
|
@ -40,7 +36,7 @@ function _getVisiblePageNumbers(currentPage, totalPages) {
|
||||||
return pagesVisible;
|
return pagesVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getPages(currentPage, pageNumbers, clientUrl) {
|
function _getPages(currentPage, pageNumbers, ctx) {
|
||||||
const pages = [];
|
const pages = [];
|
||||||
let lastPage = 0;
|
let lastPage = 0;
|
||||||
for (let page of pageNumbers) {
|
for (let page of pageNumbers) {
|
||||||
|
@ -49,7 +45,7 @@ function _getPages(currentPage, pageNumbers, clientUrl) {
|
||||||
}
|
}
|
||||||
pages.push({
|
pages.push({
|
||||||
number: page,
|
number: page,
|
||||||
link: _formatUrl(clientUrl, page),
|
link: ctx.getClientUrlForPage(page),
|
||||||
active: currentPage === page,
|
active: currentPage === page,
|
||||||
});
|
});
|
||||||
lastPage = page;
|
lastPage = page;
|
||||||
|
@ -83,16 +79,16 @@ class ManualPageView {
|
||||||
|
|
||||||
const totalPages = Math.ceil(response.total / response.pageSize);
|
const totalPages = Math.ceil(response.total / response.pageSize);
|
||||||
const pageNumbers = _getVisiblePageNumbers(currentPage, totalPages);
|
const pageNumbers = _getVisiblePageNumbers(currentPage, totalPages);
|
||||||
const pages = _getPages(currentPage, pageNumbers, ctx.clientUrl);
|
const pages = _getPages(currentPage, pageNumbers, ctx);
|
||||||
|
|
||||||
keyboard.bind(['a', 'left'], () => {
|
keyboard.bind(['a', 'left'], () => {
|
||||||
if (currentPage > 1) {
|
if (currentPage > 1) {
|
||||||
router.show(_formatUrl(ctx.clientUrl, currentPage - 1));
|
router.show(ctx.getClientUrlForPage(currentPage - 1));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
keyboard.bind(['d', 'right'], () => {
|
keyboard.bind(['d', 'right'], () => {
|
||||||
if (currentPage < totalPages) {
|
if (currentPage < totalPages) {
|
||||||
router.show(_formatUrl(ctx.clientUrl, currentPage + 1));
|
router.show(ctx.getClientUrlForPage(currentPage + 1));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -100,8 +96,8 @@ class ManualPageView {
|
||||||
views.replaceContent(
|
views.replaceContent(
|
||||||
pageNavNode,
|
pageNavNode,
|
||||||
navTemplate({
|
navTemplate({
|
||||||
prevLink: _formatUrl(ctx.clientUrl, currentPage - 1),
|
prevLink: ctx.getClientUrlForPage(currentPage - 1),
|
||||||
nextLink: _formatUrl(ctx.clientUrl, currentPage + 1),
|
nextLink: ctx.getClientUrlForPage(currentPage + 1),
|
||||||
prevLinkActive: currentPage > 1,
|
prevLinkActive: currentPage > 1,
|
||||||
nextLinkActive: currentPage < totalPages,
|
nextLinkActive: currentPage < totalPages,
|
||||||
pages: pages,
|
pages: pages,
|
||||||
|
|
|
@ -13,15 +13,20 @@ const template = views.getTemplate('posts-header');
|
||||||
class PostsHeaderView {
|
class PostsHeaderView {
|
||||||
constructor(ctx) {
|
constructor(ctx) {
|
||||||
ctx.settings = settings.get();
|
ctx.settings = settings.get();
|
||||||
|
this._ctx = ctx;
|
||||||
this._hostNode = ctx.hostNode;
|
this._hostNode = ctx.hostNode;
|
||||||
views.replaceContent(this._hostNode, template(ctx));
|
views.replaceContent(this._hostNode, template(ctx));
|
||||||
|
|
||||||
if (this._queryInputNode) {
|
if (this._queryInputNode) {
|
||||||
new TagAutoCompleteControl(this._queryInputNode, {addSpace: true});
|
new TagAutoCompleteControl(this._queryInputNode, {addSpace: true});
|
||||||
}
|
}
|
||||||
|
if (this._massTagInputNode) {
|
||||||
|
new TagAutoCompleteControl(
|
||||||
|
this._massTagInputNode, {addSpace: false});
|
||||||
|
}
|
||||||
|
|
||||||
keyboard.bind('q', () => {
|
keyboard.bind('q', () => {
|
||||||
this._formNode.querySelector('input').focus();
|
this._searchFormNode.querySelector('input').focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
keyboard.bind('p', () => {
|
keyboard.bind('p', () => {
|
||||||
|
@ -32,20 +37,69 @@ class PostsHeaderView {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let safetyButton of this._formNode.querySelectorAll('.safety')) {
|
for (let safetyButton of this._safetyButtonNodes) {
|
||||||
safetyButton.addEventListener(
|
safetyButton.addEventListener(
|
||||||
'click', e => this._evtSafetyButtonClick(e, ctx.clientUrl));
|
'click', e => this._evtSafetyButtonClick(e));
|
||||||
|
}
|
||||||
|
this._searchFormNode.addEventListener(
|
||||||
|
'submit', e => this._evtSearchFormSubmit(e));
|
||||||
|
|
||||||
|
if (this._massTagFormNode) {
|
||||||
|
if (this._openMassTagLinkNode) {
|
||||||
|
this._openMassTagLinkNode.addEventListener(
|
||||||
|
'click', e => this._evtMassTagClick(e));
|
||||||
|
}
|
||||||
|
this._stopMassTagLinkNode.addEventListener(
|
||||||
|
'click', e => this._evtStopTaggingClick(e));
|
||||||
|
this._massTagFormNode.addEventListener(
|
||||||
|
'submit', e => this._evtMassTagFormSubmit(e));
|
||||||
|
this._toggleMassTagVisibility(!!ctx.searchQuery.tag);
|
||||||
}
|
}
|
||||||
this._formNode.addEventListener(
|
|
||||||
'submit', e => this._evtFormSubmit(e, this._queryInputNode));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get _formNode() {
|
_toggleMassTagVisibility(state) {
|
||||||
return this._hostNode.querySelector('form');
|
this._massTagFormNode.classList.toggle('active', state);
|
||||||
|
}
|
||||||
|
|
||||||
|
get _searchFormNode() {
|
||||||
|
return this._hostNode.querySelector('form.search');
|
||||||
|
}
|
||||||
|
|
||||||
|
get _massTagFormNode() {
|
||||||
|
return this._hostNode.querySelector('form.masstag');
|
||||||
|
}
|
||||||
|
|
||||||
|
get _safetyButtonNodes() {
|
||||||
|
return this._searchFormNode.querySelectorAll('.safety');
|
||||||
}
|
}
|
||||||
|
|
||||||
get _queryInputNode() {
|
get _queryInputNode() {
|
||||||
return this._formNode.querySelector('[name=search-text]');
|
return this._searchFormNode.querySelector('[name=search-text]');
|
||||||
|
}
|
||||||
|
|
||||||
|
get _massTagInputNode() {
|
||||||
|
return this._massTagFormNode.querySelector('[type=text]');
|
||||||
|
}
|
||||||
|
|
||||||
|
get _openMassTagLinkNode() {
|
||||||
|
return this._massTagFormNode.querySelector('.open-masstag');
|
||||||
|
}
|
||||||
|
|
||||||
|
get _stopMassTagLinkNode() {
|
||||||
|
return this._massTagFormNode.querySelector('.stop-tagging');
|
||||||
|
}
|
||||||
|
|
||||||
|
_evtMassTagClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this._toggleMassTagVisibility(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_evtStopTaggingClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
router.show('/posts/' + misc.formatSearchQuery({
|
||||||
|
text: this._ctx.searchQuery.text,
|
||||||
|
page: this._ctx.searchQuery.page,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
_evtSafetyButtonClick(e, url) {
|
_evtSafetyButtonClick(e, url) {
|
||||||
|
@ -56,15 +110,27 @@ class PostsHeaderView {
|
||||||
browsingSettings.listPosts[safety] =
|
browsingSettings.listPosts[safety] =
|
||||||
!browsingSettings.listPosts[safety];
|
!browsingSettings.listPosts[safety];
|
||||||
settings.save(browsingSettings, true);
|
settings.save(browsingSettings, true);
|
||||||
router.show(url.replace(/{page}/, 1));
|
router.show(location.pathname + location.search + location.hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
_evtFormSubmit(e, queryInputNode) {
|
_evtSearchFormSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const text = queryInputNode.value;
|
const text = this._queryInputNode.value;
|
||||||
queryInputNode.blur();
|
this._queryInputNode.blur();
|
||||||
router.show('/posts/' + misc.formatSearchQuery({text: text}));
|
router.show('/posts/' + misc.formatSearchQuery({text: text}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_evtMassTagFormSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const text = this._queryInputNode.value;
|
||||||
|
const tag = this._massTagInputNode.value;
|
||||||
|
this._massTagInputNode.blur();
|
||||||
|
router.show('/posts/' + misc.formatSearchQuery({
|
||||||
|
text: text,
|
||||||
|
tag: tag,
|
||||||
|
page: this._ctx.searchQuery.page,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = PostsHeaderView;
|
module.exports = PostsHeaderView;
|
||||||
|
|
|
@ -1,12 +1,64 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const events = require('../events.js');
|
||||||
const views = require('../util/views.js');
|
const views = require('../util/views.js');
|
||||||
|
|
||||||
const template = views.getTemplate('posts-page');
|
const template = views.getTemplate('posts-page');
|
||||||
|
|
||||||
class PostsPageView {
|
class PostsPageView extends events.EventTarget {
|
||||||
constructor(ctx) {
|
constructor(ctx) {
|
||||||
views.replaceContent(ctx.hostNode, template(ctx));
|
super();
|
||||||
|
this._ctx = ctx;
|
||||||
|
this._hostNode = ctx.hostNode;
|
||||||
|
views.replaceContent(this._hostNode, template(ctx));
|
||||||
|
|
||||||
|
this._postIdToPost = {};
|
||||||
|
for (let post of ctx.results) {
|
||||||
|
this._postIdToPost[post.id] = post;
|
||||||
|
post.addEventListener('change', e => this._evtPostChange(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
this._postIdToLinkNode = {};
|
||||||
|
for (let linkNode of this._hostNode.querySelectorAll('.masstag')) {
|
||||||
|
const postId = linkNode.getAttribute('data-post-id');
|
||||||
|
const post = this._postIdToPost[postId];
|
||||||
|
this._postIdToLinkNode[postId] = linkNode;
|
||||||
|
linkNode.addEventListener(
|
||||||
|
'click', e => this._evtMassTagClick(e, post));
|
||||||
|
}
|
||||||
|
|
||||||
|
this._syncMassTagHighlights();
|
||||||
|
}
|
||||||
|
|
||||||
|
_evtPostChange(e) {
|
||||||
|
const linkNode = this._postIdToLinkNode[e.detail.post.id];
|
||||||
|
linkNode.removeAttribute('data-disabled');
|
||||||
|
this._syncMassTagHighlights();
|
||||||
|
}
|
||||||
|
|
||||||
|
_syncMassTagHighlights() {
|
||||||
|
for (let linkNode of this._hostNode.querySelectorAll('.masstag')) {
|
||||||
|
const postId = linkNode.getAttribute('data-post-id');
|
||||||
|
const post = this._postIdToPost[postId];
|
||||||
|
let tagged = true;
|
||||||
|
for (let tag of this._ctx.massTagTags) {
|
||||||
|
tagged = tagged & post.isTaggedWith(tag);
|
||||||
|
}
|
||||||
|
linkNode.classList.toggle('tagged', tagged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_evtMassTagClick(e, post) {
|
||||||
|
e.preventDefault();
|
||||||
|
const linkNode = e.target;
|
||||||
|
if (linkNode.getAttribute('data-disabled')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
linkNode.setAttribute('data-disabled', true);
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(
|
||||||
|
linkNode.classList.contains('tagged') ? 'untag' : 'tag',
|
||||||
|
{detail: {post: post}}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue