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
|
||||
visibility: hidden
|
||||
|
||||
a.append
|
||||
a.append, span.append
|
||||
margin-left: 1em
|
||||
form .fa-question-circle-o
|
||||
font-size: 110%
|
||||
|
|
|
@ -67,7 +67,7 @@ $safety-unsafe = #F3985F
|
|||
min-height: 7.5em
|
||||
height: 9vw
|
||||
|
||||
a
|
||||
.thumbnail-wrapper
|
||||
display: inline-block
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
@ -99,6 +99,35 @@ $safety-unsafe = #F3985F
|
|||
.icon:not(:first-of-type)
|
||||
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
|
||||
background-position: 50% 30%
|
||||
width: 100%
|
||||
|
@ -122,12 +151,23 @@ $safety-unsafe = #F3985F
|
|||
text-align: left
|
||||
form
|
||||
width: auto
|
||||
*
|
||||
vertical-align: top
|
||||
input[name=search-text]
|
||||
width: 25em
|
||||
max-width: 90vw
|
||||
input[name=masstag]
|
||||
width: 15em
|
||||
margin-left: 1em
|
||||
.append
|
||||
font-size: 0.95em
|
||||
color: $inactive-link-color
|
||||
.masstag
|
||||
&:not(.active)
|
||||
[type=text],
|
||||
.start-tagging,
|
||||
.stop-tagging
|
||||
display: none
|
||||
|
||||
.safety
|
||||
&.safety-safe
|
||||
|
@ -148,7 +188,7 @@ $safety-unsafe = #F3985F
|
|||
|
||||
.post-container
|
||||
.post-content.transparency-grid img
|
||||
background: url('/img/transparency_grid.png');
|
||||
background: url('/img/transparency_grid.png')
|
||||
|
||||
text-align: center
|
||||
.post-content
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
<div class='post-list-header'>
|
||||
<form class='horizontal'>
|
||||
<div class='input'>
|
||||
<ul>
|
||||
<li>
|
||||
<%= 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 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=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>
|
||||
</div>
|
||||
<form class='horizontal search'>
|
||||
<%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.searchQuery.text}) %>
|
||||
<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=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' %>'/>
|
||||
<a class='mousetrap button append' href='/help/search/posts'>Syntax help</a>
|
||||
</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>
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
<li>
|
||||
<% if (ctx.canViewPosts) { %>
|
||||
<% 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 { %>
|
||||
<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 { %>
|
||||
<a>
|
||||
<a class='thumbnail-wrapper'>
|
||||
<% } %>
|
||||
<%= ctx.makeThumbnail(post.thumbnailUrl) %>
|
||||
<span class='type' data-type='<%- post.type %>'>
|
||||
|
@ -39,6 +39,10 @@
|
|||
</span>
|
||||
<% } %>
|
||||
</a>
|
||||
<% if (ctx.searchQuery && ctx.searchQuery.tag) { %>
|
||||
<a data-post-id='<%= post.id %>' class='masstag'>
|
||||
</a>
|
||||
<% } %>
|
||||
</li>
|
||||
<% } %>
|
||||
<%= ctx.makeFlexboxAlign() %>
|
||||
|
|
|
@ -15,7 +15,11 @@ class CommentsController {
|
|||
|
||||
this._pageController = new PageController({
|
||||
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 => {
|
||||
return PostList.search(
|
||||
'sort:comment-date+comment-count-min:1', page, 10, fields);
|
||||
|
|
|
@ -7,7 +7,7 @@ const ManualPageView = require('../views/manual_page_view.js');
|
|||
class PageController {
|
||||
constructor(ctx) {
|
||||
const extendedContext = {
|
||||
clientUrl: ctx.clientUrl,
|
||||
getClientUrlForPage: ctx.getClientUrlForPage,
|
||||
searchQuery: ctx.searchQuery,
|
||||
};
|
||||
|
||||
|
|
|
@ -17,27 +17,63 @@ class PostListController {
|
|||
constructor(ctx) {
|
||||
topNavigation.activate('posts');
|
||||
|
||||
this._ctx = ctx;
|
||||
this._pageController = new PageController({
|
||||
searchQuery: ctx.searchQuery,
|
||||
clientUrl: '/posts/' + misc.formatSearchQuery({
|
||||
text: ctx.searchQuery.text, page: '{page}'}),
|
||||
getClientUrlForPage: page => {
|
||||
const searchQuery = Object.assign(
|
||||
{}, ctx.searchQuery, {page: page});
|
||||
return '/posts/' + misc.formatSearchQuery(searchQuery);
|
||||
},
|
||||
requestPage: page => {
|
||||
return PostList.search(
|
||||
this._decorateSearchQuery(ctx.searchQuery.text),
|
||||
page, 40, fields);
|
||||
},
|
||||
headerRenderer: headerCtx => {
|
||||
Object.assign(headerCtx, {
|
||||
canMassTag: api.hasPrivilege('tags:masstag'),
|
||||
massTagTags: this._massTagTags,
|
||||
});
|
||||
return new PostsHeaderView(headerCtx);
|
||||
},
|
||||
pageRenderer: pageCtx => {
|
||||
Object.assign(pageCtx, {
|
||||
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) {
|
||||
const browsingSettings = settings.get();
|
||||
let disabledSafety = [];
|
||||
|
|
|
@ -17,8 +17,11 @@ class TagListController {
|
|||
|
||||
this._pageController = new PageController({
|
||||
searchQuery: ctx.searchQuery,
|
||||
clientUrl: '/tags/' + misc.formatSearchQuery({
|
||||
text: ctx.searchQuery.text, page: '{page}'}),
|
||||
getClientUrlForPage: page => {
|
||||
const searchQuery = Object.assign(
|
||||
{}, ctx.searchQuery, {page: page});
|
||||
return '/tags/' + misc.formatSearchQuery(searchQuery);
|
||||
},
|
||||
requestPage: page => {
|
||||
return TagList.search(ctx.searchQuery.text, page, 50, fields);
|
||||
},
|
||||
|
|
|
@ -14,8 +14,11 @@ class UserListController {
|
|||
|
||||
this._pageController = new PageController({
|
||||
searchQuery: ctx.searchQuery,
|
||||
clientUrl: '/users/' + misc.formatSearchQuery({
|
||||
text: ctx.searchQuery.text, page: '{page}'}),
|
||||
getClientUrlForPage: page => {
|
||||
const searchQuery = Object.assign(
|
||||
{}, ctx.searchQuery, {page: page});
|
||||
return '/users/' + misc.formatSearchQuery(searchQuery);
|
||||
},
|
||||
requestPage: page => {
|
||||
return UserList.search(ctx.searchQuery.text, page);
|
||||
},
|
||||
|
|
|
@ -105,18 +105,16 @@ class TagInputControl {
|
|||
this._editAreaNode.insertBefore(targetWrapperNode, sourceWrapperNode);
|
||||
this._editAreaNode.insertBefore(this._createSpace(), sourceWrapperNode);
|
||||
|
||||
const actualTag = tags.getTagByName(text) || {};
|
||||
|
||||
// XXX: perhaps we should aggregate suggestions from all implications
|
||||
// for call to the _suggestRelations
|
||||
if (addImplications) {
|
||||
for (let otherTag of (actualTag.implications || [])) {
|
||||
for (let otherTag of tags.getAllImplications(text)) {
|
||||
this.addTag(otherTag, sourceNode, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestRelations) {
|
||||
this._suggestRelations([], actualTag.suggestions || []);
|
||||
this._suggestRelations([], tags.getSuggestions(text) || []);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const api = require('../api.js');
|
||||
const tags = require('../tags.js');
|
||||
const events = require('../events.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) {
|
||||
return api.put('/post/' + this._id + '/score', {score: score})
|
||||
.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 = {
|
||||
getAllCategories: getAllCategories,
|
||||
getAllTags: getAllTags,
|
||||
|
@ -97,4 +108,6 @@ module.exports = {
|
|||
getNameToTagMap: getNameToTagMap,
|
||||
getOriginalTagName: getOriginalTagName,
|
||||
refreshExport: refreshExport,
|
||||
getAllImplications: getAllImplications,
|
||||
getSuggestions: getSuggestions,
|
||||
};
|
||||
|
|
|
@ -6,10 +6,6 @@ const views = require('../util/views.js');
|
|||
const holderTemplate = views.getTemplate('endless-pager');
|
||||
const pageTemplate = views.getTemplate('endless-pager-page');
|
||||
|
||||
function _formatUrl(url, page) {
|
||||
return url.replace('{page}', page);
|
||||
}
|
||||
|
||||
class EndlessPageView {
|
||||
constructor(ctx) {
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
|
@ -68,7 +64,7 @@ class EndlessPageView {
|
|||
let topPageNumber = parseInt(topPageNode.getAttribute('data-page'));
|
||||
if (topPageNumber !== this.currentPage) {
|
||||
router.replace(
|
||||
_formatUrl(ctx.clientUrl, topPageNumber),
|
||||
ctx.getClientUrlForPage(topPageNumber),
|
||||
{},
|
||||
false);
|
||||
this.currentPage = topPageNumber;
|
||||
|
|
|
@ -8,10 +8,6 @@ const views = require('../util/views.js');
|
|||
const holderTemplate = views.getTemplate('manual-pager');
|
||||
const navTemplate = views.getTemplate('manual-pager-nav');
|
||||
|
||||
function _formatUrl(url, page) {
|
||||
return url.replace('{page}', page);
|
||||
}
|
||||
|
||||
function _removeConsecutiveDuplicates(a) {
|
||||
return a.filter((item, pos, ary) => {
|
||||
return !pos || item != ary[pos - 1];
|
||||
|
@ -40,7 +36,7 @@ function _getVisiblePageNumbers(currentPage, totalPages) {
|
|||
return pagesVisible;
|
||||
}
|
||||
|
||||
function _getPages(currentPage, pageNumbers, clientUrl) {
|
||||
function _getPages(currentPage, pageNumbers, ctx) {
|
||||
const pages = [];
|
||||
let lastPage = 0;
|
||||
for (let page of pageNumbers) {
|
||||
|
@ -49,7 +45,7 @@ function _getPages(currentPage, pageNumbers, clientUrl) {
|
|||
}
|
||||
pages.push({
|
||||
number: page,
|
||||
link: _formatUrl(clientUrl, page),
|
||||
link: ctx.getClientUrlForPage(page),
|
||||
active: currentPage === page,
|
||||
});
|
||||
lastPage = page;
|
||||
|
@ -83,16 +79,16 @@ class ManualPageView {
|
|||
|
||||
const totalPages = Math.ceil(response.total / response.pageSize);
|
||||
const pageNumbers = _getVisiblePageNumbers(currentPage, totalPages);
|
||||
const pages = _getPages(currentPage, pageNumbers, ctx.clientUrl);
|
||||
const pages = _getPages(currentPage, pageNumbers, ctx);
|
||||
|
||||
keyboard.bind(['a', 'left'], () => {
|
||||
if (currentPage > 1) {
|
||||
router.show(_formatUrl(ctx.clientUrl, currentPage - 1));
|
||||
router.show(ctx.getClientUrlForPage(currentPage - 1));
|
||||
}
|
||||
});
|
||||
keyboard.bind(['d', 'right'], () => {
|
||||
if (currentPage < totalPages) {
|
||||
router.show(_formatUrl(ctx.clientUrl, currentPage + 1));
|
||||
router.show(ctx.getClientUrlForPage(currentPage + 1));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -100,8 +96,8 @@ class ManualPageView {
|
|||
views.replaceContent(
|
||||
pageNavNode,
|
||||
navTemplate({
|
||||
prevLink: _formatUrl(ctx.clientUrl, currentPage - 1),
|
||||
nextLink: _formatUrl(ctx.clientUrl, currentPage + 1),
|
||||
prevLink: ctx.getClientUrlForPage(currentPage - 1),
|
||||
nextLink: ctx.getClientUrlForPage(currentPage + 1),
|
||||
prevLinkActive: currentPage > 1,
|
||||
nextLinkActive: currentPage < totalPages,
|
||||
pages: pages,
|
||||
|
|
|
@ -13,15 +13,20 @@ const template = views.getTemplate('posts-header');
|
|||
class PostsHeaderView {
|
||||
constructor(ctx) {
|
||||
ctx.settings = settings.get();
|
||||
this._ctx = ctx;
|
||||
this._hostNode = ctx.hostNode;
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
|
||||
if (this._queryInputNode) {
|
||||
new TagAutoCompleteControl(this._queryInputNode, {addSpace: true});
|
||||
}
|
||||
if (this._massTagInputNode) {
|
||||
new TagAutoCompleteControl(
|
||||
this._massTagInputNode, {addSpace: false});
|
||||
}
|
||||
|
||||
keyboard.bind('q', () => {
|
||||
this._formNode.querySelector('input').focus();
|
||||
this._searchFormNode.querySelector('input').focus();
|
||||
});
|
||||
|
||||
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(
|
||||
'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() {
|
||||
return this._hostNode.querySelector('form');
|
||||
_toggleMassTagVisibility(state) {
|
||||
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() {
|
||||
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) {
|
||||
|
@ -56,15 +110,27 @@ class PostsHeaderView {
|
|||
browsingSettings.listPosts[safety] =
|
||||
!browsingSettings.listPosts[safety];
|
||||
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();
|
||||
const text = queryInputNode.value;
|
||||
queryInputNode.blur();
|
||||
const text = this._queryInputNode.value;
|
||||
this._queryInputNode.blur();
|
||||
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;
|
||||
|
|
|
@ -1,12 +1,64 @@
|
|||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
const template = views.getTemplate('posts-page');
|
||||
|
||||
class PostsPageView {
|
||||
class PostsPageView extends events.EventTarget {
|
||||
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