client/tags: add tag category editing
This commit is contained in:
parent
be0a70355d
commit
27cce56054
12 changed files with 412 additions and 20 deletions
|
@ -172,6 +172,15 @@ input[type=password]
|
||||||
&:focus
|
&:focus
|
||||||
border-color: $main-color
|
border-color: $main-color
|
||||||
|
|
||||||
|
label.color
|
||||||
|
position: relative
|
||||||
|
input[type=text]
|
||||||
|
text-align: center
|
||||||
|
pointer-events: none
|
||||||
|
input[type=color]
|
||||||
|
position: absolute
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
form.show-validation .input
|
form.show-validation .input
|
||||||
input:invalid
|
input:invalid
|
||||||
outline: none
|
outline: none
|
||||||
|
|
|
@ -19,6 +19,8 @@ a
|
||||||
color: $main-color
|
color: $main-color
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
transition: color 0.1s linear
|
transition: color 0.1s linear
|
||||||
|
&.inactive
|
||||||
|
color: $inactive-link-color
|
||||||
&.icon
|
&.icon
|
||||||
color: $inactive-link-color
|
color: $inactive-link-color
|
||||||
opacity: .5
|
opacity: .5
|
||||||
|
|
|
@ -38,3 +38,23 @@
|
||||||
.append
|
.append
|
||||||
font-size: 0.95em
|
font-size: 0.95em
|
||||||
color: $inactive-link-color
|
color: $inactive-link-color
|
||||||
|
|
||||||
|
.tag-categories
|
||||||
|
td, th
|
||||||
|
padding: .2em
|
||||||
|
&:first-child
|
||||||
|
padding-left: 0
|
||||||
|
&:last-child
|
||||||
|
padding-right: 0
|
||||||
|
&.name
|
||||||
|
width: 12em
|
||||||
|
&.color
|
||||||
|
text-align: center
|
||||||
|
width: 5em
|
||||||
|
&.usages
|
||||||
|
text-align: center
|
||||||
|
width: 5em
|
||||||
|
tfoot
|
||||||
|
display: none
|
||||||
|
.messages, form
|
||||||
|
width: auto
|
||||||
|
|
76
client/html/tag_categories.hbs
Normal file
76
client/html/tag_categories.hbs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
<div class='content-wrapper tag-categories'>
|
||||||
|
<form>
|
||||||
|
<h1>Tag categories</h1>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class='name'>Category name</th>
|
||||||
|
<th class='color'>CSS color</th>
|
||||||
|
<th class='usages'>Usages</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% _.each(tagCategories, category => { %>
|
||||||
|
<tr data-category='<%= category.name %>'>
|
||||||
|
<td class='name'>
|
||||||
|
<% if (canEditName) { %>
|
||||||
|
<%= makeTextInput({value: category.name, required: true}) %>
|
||||||
|
<% } else { %>
|
||||||
|
<%= category.name %>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
<td class='color'>
|
||||||
|
<% if (canEditColor) { %>
|
||||||
|
<%= makeColorInput({value: category.color}) %>
|
||||||
|
<% } else { %>
|
||||||
|
<%= category.color %>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
<td class='usages'>
|
||||||
|
<a href='/tags/text=category:<%= category.name %>'>
|
||||||
|
<%= category.usages %>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<% if (canDelete) { %>
|
||||||
|
<td>
|
||||||
|
<% if (category.usages) { %>
|
||||||
|
<a class='inactive remove' title="Can't delete category in use">Remove</a>
|
||||||
|
<% } else { %>
|
||||||
|
<a href='#' class='remove'>Remove</a>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
<% } %>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class='add-template'>
|
||||||
|
<td class='name'>
|
||||||
|
<%= makeTextInput({required: true}) %>
|
||||||
|
</td>
|
||||||
|
<td class='color'>
|
||||||
|
<%= makeColorInput({value: '#000000'}) %>
|
||||||
|
</td>
|
||||||
|
<td class='usages'>
|
||||||
|
0
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href='#' class='remove'>Remove</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<% if (canCreate) { %>
|
||||||
|
<p><a href='#' class='add'>Add new category</a></p>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<div class='messages'></div>
|
||||||
|
|
||||||
|
<% if (canCreate || canEditName || canEditColor || canDelete) { %>
|
||||||
|
<div class='buttons'>
|
||||||
|
<input type='submit' class='save' value='Save changes'>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -9,7 +9,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div class='buttons'>
|
<div class='buttons'>
|
||||||
<input type='submit' value='Search'/>
|
<input type='submit' value='Search'/>
|
||||||
<a class='append' href='/help/search/tags'>Syntax help</a>
|
<a class='button append' href='/help/search/tags'>Syntax help</a>
|
||||||
|
<% if (canEditTagCategories) { %>
|
||||||
|
<a class='append' href='/tag-categories'>Tag categories</a>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<td class='names'>
|
<td class='names'>
|
||||||
<ul>
|
<ul>
|
||||||
<% _.each(tag.names, name => { %>
|
<% _.each(tag.names, name => { %>
|
||||||
<li><a href='/tag/<%= name %>'><%= name %></a></li>
|
<li><%= makeTagLink(name) %></li>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
<% if (tag.implications.length) { %>
|
<% if (tag.implications.length) { %>
|
||||||
<ul>
|
<ul>
|
||||||
<% _.each(tag.implications, name => { %>
|
<% _.each(tag.implications, name => { %>
|
||||||
<li><a href='/tag/<%= name %>'><%= name %></a></li>
|
<li><%= makeTagLink(name) %></li>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</ul>
|
</ul>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<% if (tag.suggestions.length) { %>
|
<% if (tag.suggestions.length) { %>
|
||||||
<ul>
|
<ul>
|
||||||
<% _.each(tag.suggestions, name => { %>
|
<% _.each(tag.suggestions, name => { %>
|
||||||
<li><a href='/tag/<%= name %>'><%= name %></a></li>
|
<li><%= makeTagLink(name) %></li>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</ul>
|
</ul>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
|
|
|
@ -2,25 +2,77 @@
|
||||||
|
|
||||||
const page = require('page');
|
const page = require('page');
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
|
const events = require('../events.js');
|
||||||
const misc = require('../util/misc.js');
|
const misc = require('../util/misc.js');
|
||||||
const topNavController = require('../controllers/top_nav_controller.js');
|
const topNavController = require('../controllers/top_nav_controller.js');
|
||||||
const pageController = require('../controllers/page_controller.js');
|
const pageController = require('../controllers/page_controller.js');
|
||||||
const TagListHeaderView = require('../views/tag_list_header_view.js');
|
const TagListHeaderView = require('../views/tag_list_header_view.js');
|
||||||
const TagListPageView = require('../views/tag_list_page_view.js');
|
const TagListPageView = require('../views/tag_list_page_view.js');
|
||||||
|
const TagCategoriesView = require('../views/tag_categories_view.js');
|
||||||
|
|
||||||
class TagsController {
|
class TagsController {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.tagListHeaderView = new TagListHeaderView();
|
this.tagListHeaderView = new TagListHeaderView();
|
||||||
this.tagListPageView = new TagListPageView();
|
this.tagListPageView = new TagListPageView();
|
||||||
|
this.tagCategoriesView = new TagCategoriesView();
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRoutes() {
|
registerRoutes() {
|
||||||
|
page('/tag-categories', () => { this.tagCategoriesRoute(); });
|
||||||
page(
|
page(
|
||||||
'/tags/:query?',
|
'/tags/:query?',
|
||||||
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
|
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
|
||||||
(ctx, next) => { this.listTagsRoute(ctx, next); });
|
(ctx, next) => { this.listTagsRoute(ctx, next); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_saveTagCategories(addedCategories, changedCategories, removedCategories) {
|
||||||
|
let promises = [];
|
||||||
|
for (let category of addedCategories) {
|
||||||
|
promises.push(api.post('/tag-categories/', category));
|
||||||
|
}
|
||||||
|
for (let category of changedCategories) {
|
||||||
|
promises.push(
|
||||||
|
api.put('/tag-category/' + category.originalName, category));
|
||||||
|
}
|
||||||
|
for (let name of removedCategories) {
|
||||||
|
promises.push(api.delete('/tag-category/' + name));
|
||||||
|
}
|
||||||
|
Promise.all(promises).then(
|
||||||
|
() => {
|
||||||
|
events.notify(events.TagsChange);
|
||||||
|
events.notify(events.Success, 'Changes saved successfully');
|
||||||
|
},
|
||||||
|
response => {
|
||||||
|
events.notify(events.Error, response.description);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tagCategoriesRoute(ctx, next) {
|
||||||
|
topNavController.activate('tags');
|
||||||
|
api.get('/tag-categories/').then(response => {
|
||||||
|
this.tagCategoriesView.render({
|
||||||
|
tagCategories: response.results,
|
||||||
|
canEditName: api.hasPrivilege('tagCategories:edit:name'),
|
||||||
|
canEditColor: api.hasPrivilege('tagCategories:edit:color'),
|
||||||
|
canDelete: api.hasPrivilege('tagCategories:delete'),
|
||||||
|
canCreate: api.hasPrivilege('tagCategories:create'),
|
||||||
|
saveChanges: (...args) => {
|
||||||
|
return this._saveTagCategories(...args);
|
||||||
|
},
|
||||||
|
getCategories: () => {
|
||||||
|
return api.get('/tag-categories/').then(response => {
|
||||||
|
return Promise.resolve(response.results);
|
||||||
|
}, response => {
|
||||||
|
return Promise.reject(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, response => {
|
||||||
|
this.emptyView.render();
|
||||||
|
events.notify(events.Error, response.description);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
listTagsRoute(ctx, next) {
|
listTagsRoute(ctx, next) {
|
||||||
topNavController.activate('tags');
|
topNavController.activate('tags');
|
||||||
|
|
||||||
|
@ -37,6 +89,7 @@ class TagsController {
|
||||||
searchQuery: ctx.searchQuery,
|
searchQuery: ctx.searchQuery,
|
||||||
headerRenderer: this.tagListHeaderView,
|
headerRenderer: this.tagListHeaderView,
|
||||||
pageRenderer: this.tagListPageView,
|
pageRenderer: this.tagListPageView,
|
||||||
|
canEditTagCategories: api.hasPrivilege('tagCategories:edit'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ module.exports = {
|
||||||
Info: 3,
|
Info: 3,
|
||||||
Authentication: 4,
|
Authentication: 4,
|
||||||
SettingsChange: 5,
|
SettingsChange: 5,
|
||||||
|
TagsChange: 6,
|
||||||
|
|
||||||
notify: notify,
|
notify: notify,
|
||||||
listen: listen,
|
listen: listen,
|
||||||
|
|
|
@ -27,22 +27,24 @@ controllers.push(require('./controllers/settings_controller.js'));
|
||||||
|
|
||||||
controllers.push(require('./controllers/home_controller.js'));
|
controllers.push(require('./controllers/home_controller.js'));
|
||||||
|
|
||||||
|
const tags = require('./tags.js');
|
||||||
const events = require('./events.js');
|
const events = require('./events.js');
|
||||||
for (let controller of controllers) {
|
for (let controller of controllers) {
|
||||||
controller.registerRoutes();
|
controller.registerRoutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = require('./api.js');
|
const api = require('./api.js');
|
||||||
api.loginFromCookies().then(() => {
|
Promise.all([tags.refreshExport(), api.loginFromCookies()])
|
||||||
page();
|
.then(() => {
|
||||||
}).catch(errorMessage => {
|
|
||||||
if (window.location.href.indexOf('login') !== -1) {
|
|
||||||
api.forget();
|
|
||||||
page();
|
page();
|
||||||
} else {
|
}).catch(errorMessage => {
|
||||||
page('/');
|
if (window.location.href.indexOf('login') !== -1) {
|
||||||
events.notify(
|
api.forget();
|
||||||
events.Error,
|
page();
|
||||||
'An error happened while trying to log you in: ' + errorMessage);
|
} else {
|
||||||
}
|
page('/');
|
||||||
});
|
events.notify(
|
||||||
|
events.Error,
|
||||||
|
'An error happened while trying to log you in: ' + errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
70
client/js/tags.js
Normal file
70
client/js/tags.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const request = require('superagent');
|
||||||
|
const util = require('./util/misc.js');
|
||||||
|
const events = require('./events.js');
|
||||||
|
|
||||||
|
let _export = null;
|
||||||
|
let _stylesheet = null;
|
||||||
|
|
||||||
|
function _tagsToDictionary(tags)
|
||||||
|
{
|
||||||
|
let dict = {};
|
||||||
|
for (let tag of tags) {
|
||||||
|
for (let name of tag.names) {
|
||||||
|
dict[name] = tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _tagCategoriesToDictionary(categories)
|
||||||
|
{
|
||||||
|
let dict = {};
|
||||||
|
for (let category of categories) {
|
||||||
|
dict[category.name] = category;
|
||||||
|
}
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _refreshStylesheet() {
|
||||||
|
if (_stylesheet) {
|
||||||
|
document.head.removeChild(_stylesheet);
|
||||||
|
}
|
||||||
|
_stylesheet = document.createElement('style');
|
||||||
|
document.head.appendChild(_stylesheet);
|
||||||
|
for (let category of Object.values(_export.categories)) {
|
||||||
|
_stylesheet.sheet.insertRule(
|
||||||
|
'.tag-{0} { color: {1} }'.format(category.name, category.color),
|
||||||
|
_stylesheet.sheet.cssRules.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshExport() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request.get('/data/tags.json').end((error, response) => {
|
||||||
|
if (error) {
|
||||||
|
console.log('Error while fetching exported tags', error);
|
||||||
|
_export = {tags: {}, categories: {}};
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
_export = response.body;
|
||||||
|
_export.tags = _tagsToDictionary(_export.tags);
|
||||||
|
_export.categories = _tagCategoriesToDictionary(
|
||||||
|
_export.categories);
|
||||||
|
_refreshStylesheet();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExport() {
|
||||||
|
return _export || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
events.listen(events.TagsChange, refreshExport);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getExport: getExport,
|
||||||
|
refreshExport: refreshExport,
|
||||||
|
};
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
require('../util/polyfill.js');
|
require('../util/polyfill.js');
|
||||||
const underscore = require('underscore');
|
const underscore = require('underscore');
|
||||||
|
const tags = require('../tags.js');
|
||||||
const events = require('../events.js');
|
const events = require('../events.js');
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
const misc = require('./misc.js');
|
const misc = require('./misc.js');
|
||||||
|
@ -105,6 +106,37 @@ function makeEmailInput(options) {
|
||||||
return makeInput(options);
|
return makeInput(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeColorInput(options) {
|
||||||
|
const textInput = makeVoidElement(
|
||||||
|
'input', {
|
||||||
|
type: 'text',
|
||||||
|
value: options.value || '',
|
||||||
|
required: options.required,
|
||||||
|
style: 'color: ' + options.value,
|
||||||
|
disabled: true,
|
||||||
|
});
|
||||||
|
const colorInput = makeVoidElement(
|
||||||
|
'input', {
|
||||||
|
type: 'color',
|
||||||
|
value: options.value || '',
|
||||||
|
});
|
||||||
|
return makeNonVoidElement('label', {class: 'color'}, colorInput + textInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTagLink(name) {
|
||||||
|
const tagExport = tags.getExport();
|
||||||
|
let category = null;
|
||||||
|
try {
|
||||||
|
category = tagExport.tags[name].category;
|
||||||
|
} catch (e) {
|
||||||
|
category = 'unknown';
|
||||||
|
}
|
||||||
|
return makeNonVoidElement('a', {
|
||||||
|
'href': '/tag/' + name,
|
||||||
|
'class': 'tag-' + category,
|
||||||
|
}, name);
|
||||||
|
}
|
||||||
|
|
||||||
function makeFlexboxAlign(options) {
|
function makeFlexboxAlign(options) {
|
||||||
return Array.from(misc.range(20))
|
return Array.from(misc.range(20))
|
||||||
.map(() => '<li class="flexbox-dummy"></li>').join('');
|
.map(() => '<li class="flexbox-dummy"></li>').join('');
|
||||||
|
@ -201,6 +233,8 @@ function getTemplate(templatePath) {
|
||||||
makeTextInput: makeTextInput,
|
makeTextInput: makeTextInput,
|
||||||
makePasswordInput: makePasswordInput,
|
makePasswordInput: makePasswordInput,
|
||||||
makeEmailInput: makeEmailInput,
|
makeEmailInput: makeEmailInput,
|
||||||
|
makeColorInput: makeColorInput,
|
||||||
|
makeTagLink: makeTagLink,
|
||||||
makeFlexboxAlign: makeFlexboxAlign,
|
makeFlexboxAlign: makeFlexboxAlign,
|
||||||
});
|
});
|
||||||
return htmlToDom(templateFactory(ctx));
|
return htmlToDom(templateFactory(ctx));
|
||||||
|
@ -210,10 +244,15 @@ function getTemplate(templatePath) {
|
||||||
function decorateValidator(form) {
|
function decorateValidator(form) {
|
||||||
// postpone showing form fields validity until user actually tries
|
// postpone showing form fields validity until user actually tries
|
||||||
// to submit it (seeing red/green form w/o doing anything breaks POLA)
|
// to submit it (seeing red/green form w/o doing anything breaks POLA)
|
||||||
const submitButton = form.querySelector('.buttons input');
|
let submitButton = form.querySelector('.buttons input');
|
||||||
submitButton.addEventListener('click', e => {
|
if (!submitButton) {
|
||||||
form.classList.add('show-validation');
|
submitButton = form.querySelector('input[type=submit]');
|
||||||
});
|
}
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.addEventListener('click', e => {
|
||||||
|
form.classList.add('show-validation');
|
||||||
|
});
|
||||||
|
}
|
||||||
form.addEventListener('submit', e => {
|
form.addEventListener('submit', e => {
|
||||||
form.classList.remove('show-validation');
|
form.classList.remove('show-validation');
|
||||||
});
|
});
|
||||||
|
@ -259,6 +298,14 @@ function scrollToHash() {
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener('input', e => {
|
||||||
|
if (e.target.getAttribute('type').toLowerCase() === 'color') {
|
||||||
|
const textInput = e.target.parentNode.querySelector('input[type=text]');
|
||||||
|
textInput.style.color = e.target.value;
|
||||||
|
textInput.value = e.target.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
htmlToDom: htmlToDom,
|
htmlToDom: htmlToDom,
|
||||||
getTemplate: getTemplate,
|
getTemplate: getTemplate,
|
||||||
|
|
109
client/js/views/tag_categories_view.js
Normal file
109
client/js/views/tag_categories_view.js
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const misc = require('../util/misc.js');
|
||||||
|
const views = require('../util/views.js');
|
||||||
|
|
||||||
|
class TagListHeaderView {
|
||||||
|
constructor() {
|
||||||
|
this.template = views.getTemplate('tag-categories');
|
||||||
|
}
|
||||||
|
|
||||||
|
_saveButtonClickHandler(e, ctx, target) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
views.clearMessages(target);
|
||||||
|
const tableBody = target.querySelector('tbody');
|
||||||
|
|
||||||
|
ctx.getCategories().then(categories => {
|
||||||
|
let existingCategories = {};
|
||||||
|
for (let category of categories) {
|
||||||
|
existingCategories[category.name] = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
let addedCategories = [];
|
||||||
|
let removedCategories = [];
|
||||||
|
let changedCategories = [];
|
||||||
|
let allNames = [];
|
||||||
|
for (let row of tableBody.querySelectorAll('tr')) {
|
||||||
|
let name = row.getAttribute('data-category');
|
||||||
|
let category = {
|
||||||
|
originalName: name,
|
||||||
|
name: row.querySelector('.name input').value,
|
||||||
|
color: row.querySelector('.color input').value,
|
||||||
|
};
|
||||||
|
if (!name) {
|
||||||
|
if (category.name) {
|
||||||
|
addedCategories.push(category);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const existingCategory = existingCategories[name];
|
||||||
|
if (existingCategory.color !== category.color
|
||||||
|
|| existingCategory.name !== category.name) {
|
||||||
|
changedCategories.push(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allNames.push(name);
|
||||||
|
}
|
||||||
|
for (let name of Object.keys(existingCategories)) {
|
||||||
|
if (allNames.indexOf(name) === -1) {
|
||||||
|
removedCategories.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.saveChanges(
|
||||||
|
addedCategories, changedCategories, removedCategories);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeButtonClickHandler(e, row, link) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (link.classList.contains('inactive')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
row.parentNode.removeChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
_addRemoveButtonClickHandler(row) {
|
||||||
|
const link = row.querySelector('a.remove');
|
||||||
|
if (!link) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
link.addEventListener(
|
||||||
|
'click', e => this._removeButtonClickHandler(e, row, link));
|
||||||
|
}
|
||||||
|
|
||||||
|
render(ctx) {
|
||||||
|
const target = document.getElementById('content-holder');
|
||||||
|
const source = this.template(ctx);
|
||||||
|
|
||||||
|
const form = source.querySelector('form');
|
||||||
|
const newRowTemplate = source.querySelector('.add-template');
|
||||||
|
const tableBody = source.querySelector('tbody');
|
||||||
|
const addLink = source.querySelector('a.add');
|
||||||
|
const saveButton = source.querySelector('button.save');
|
||||||
|
|
||||||
|
newRowTemplate.parentNode.removeChild(newRowTemplate);
|
||||||
|
views.decorateValidator(form);
|
||||||
|
|
||||||
|
for (let row of tableBody.querySelectorAll('tr')) {
|
||||||
|
this._addRemoveButtonClickHandler(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addLink) {
|
||||||
|
addLink.addEventListener('click', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
let newRow = newRowTemplate.cloneNode(true);
|
||||||
|
tableBody.appendChild(newRow);
|
||||||
|
this._addRemoveButtonClickHandler(newRow);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', e => {
|
||||||
|
this._saveButtonClickHandler(e, ctx, target);
|
||||||
|
});
|
||||||
|
|
||||||
|
views.listenToMessages(target);
|
||||||
|
views.showView(target, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TagListHeaderView;
|
Loading…
Reference in a new issue