client/posts: add mass tag

This commit is contained in:
rr- 2016-07-05 21:20:28 +02:00
parent 99011b02d7
commit fccedc090f
16 changed files with 322 additions and 64 deletions

View file

@ -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%

View file

@ -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

View file

@ -1,18 +1,22 @@
<div class='post-list-header'> <div class='post-list-header'>
<form class='horizontal'> <form class='horizontal search'>
<div class='input'> <%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.searchQuery.text}) %>
<ul> <input class='mousetrap' type='submit' value='Search'/>
<li> <input data-safety=safe type='button' class='mousetrap safety safety-safe <%- ctx.settings.listPosts.safe ? '' : 'disabled' %>'/>
<%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.searchQuery.text}) %> <input data-safety=sketchy type='button' class='mousetrap safety safety-sketchy <%- ctx.settings.listPosts.sketchy ? '' : 'disabled' %>'/>
</li> <input data-safety=unsafe type='button' class='mousetrap safety safety-unsafe <%- ctx.settings.listPosts.unsafe ? '' : 'disabled' %>'/>
</ul> <a class='mousetrap button append' href='/help/search/posts'>Syntax help</a>
</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> </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>

View file

@ -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 %>)&#10;&#10;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 %>)&#10;&#10;Tags: <%- post.tags.map(tag => '#' + tag).join(' ') %>'>
<% } else { %> <% } else { %>
<a href='/post/<%- encodeURIComponent(post.id) %>' title='@<%- post.id %> (<%- post.type %>)&#10;&#10;Tags: <%- post.tags.map(tag => '#' + tag).join(' ') %>'> <a class='thumbnail-wrapper' href='/post/<%- encodeURIComponent(post.id) %>' title='@<%- post.id %> (<%- post.type %>)&#10;&#10;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() %>

View file

@ -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);

View file

@ -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,
}; };

View file

@ -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 = [];

View file

@ -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);
}, },

View file

@ -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);
}, },

View file

@ -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) || []);
} }
} }

View file

@ -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 => {

View file

@ -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,
}; };

View file

@ -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;

View file

@ -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,

View file

@ -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;

View file

@ -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}}));
} }
} }