client/general: replace direct API with models

This commit is contained in:
rr- 2016-06-19 19:16:40 +02:00
parent 5f4b67a2bc
commit eb09677bf8
33 changed files with 1025 additions and 572 deletions

View file

@ -10,67 +10,7 @@
</tr>
</thead>
<tbody>
<% for (let category of ctx.tagCategories) { %>
<% if (category.default) { %>
<tr data-category='<%= category.name %>' class='default'>
<% } else { %>
<tr data-category='<%= category.name %>'>
<% } %>
<td class='name'>
<% if (ctx.canEditName) { %>
<%= ctx.makeTextInput({value: category.name, required: true}) %>
<% } else { %>
<%= category.name %>
<% } %>
</td>
<td class='color'>
<% if (ctx.canEditColor) { %>
<%= ctx.makeColorInput({value: category.color}) %>
<% } else { %>
<%= category.color %>
<% } %>
</td>
<td class='usages'>
<a href='/tags/text=category:<%= category.name %>'>
<%= category.usages %>
</a>
</td>
<% if (ctx.canDelete) { %>
<td class='remove'>
<% if (category.usages) { %>
<a class='inactive' title="Can't delete category in use">Remove</a>
<% } else { %>
<a href='#'>Remove</a>
<% } %>
</td>
<% } %>
<% if (ctx.canSetDefault) { %>
<td class='set-default'>
<a href='#'>Make default</a>
</td>
<% } %>
</tr>
<% } %>
</tbody>
<tfoot>
<tr class='add-template'>
<td class='name'>
<%= ctx.makeTextInput({required: true}) %>
</td>
<td class='color'>
<%= ctx.makeColorInput({value: '#000000'}) %>
</td>
<td class='usages'>
0
</td>
<td class='remove'>
<a href='#'>Remove</a>
</td>
<td class='set-default'>
<a href='#'>Make default</a>
</td>
</tr>
</tfoot>
</table>
<% if (ctx.canCreate) { %>

View file

@ -0,0 +1,41 @@
<tr data-category='<%= ctx.tagCategory.name %>'
<% if (ctx.tagCategory.isDefault) { %> class='default' <% } %>
>
<td class='name'>
<% if (ctx.canEditName) { %>
<%= ctx.makeTextInput({value: ctx.tagCategory.name, required: true}) %>
<% } else { %>
<%= ctx.tagCategory.name %>
<% } %>
</td>
<td class='color'>
<% if (ctx.canEditColor) { %>
<%= ctx.makeColorInput({value: ctx.tagCategory.color}) %>
<% } else { %>
<%= ctx.tagCategory.color %>
<% } %>
</td>
<td class='usages'>
<% if (ctx.tagCategory.name) { %>
<a href='/tags/text=category:<%= ctx.tagCategory.name %>'>
<%= ctx.tagCategory.tagCount %>
</a>
<% } else { %>
<%= ctx.tagCategory.tagCount %>
<% } %>
</td>
<% if (ctx.canDelete) { %>
<td class='remove'>
<% if (ctx.tagCategory.tagCount) { %>
<a class='inactive' title="Can't delete category in use">Remove</a>
<% } else { %>
<a href='#'>Remove</a>
<% } %>
</td>
<% } %>
<% if (ctx.canSetDefault) { %>
<td class='set-default'>
<a href='#'>Make default</a>
</td>
<% } %>
</tr>

View file

@ -1,6 +1,6 @@
<div class='tag-delete'>
<form>
<% if (ctx.tag.usages) { %>
<% if (ctx.tag.postCount) { %>
<p>For extra <s>paranoia</s> safety, only tags that are unused can be deleted.</p>
<p>Check <a href='/posts/text=<%= ctx.tag.names[0] %>'>which posts</a> are tagged with <%= ctx.tag.names[0] %>.</p>
<% } else { %>

View file

@ -71,7 +71,7 @@
<% } %>
</td>
<td class='usages'>
<%= tag.usages %>
<%= tag.postCount %>
</td>
<td class='edit-time'>
<%= ctx.makeRelativeTime(tag.lastEditTime) %>

View file

@ -22,6 +22,15 @@ class Api extends events.EventTarget {
'administrator',
'nobody',
];
this.rankNames = new Map([
['anonymous', 'Anonymous'],
['restricted', 'Restricted user'],
['regular', 'Regular user'],
['power', 'Power user'],
['moderator', 'Moderator'],
['administrator', 'Administrator'],
['nobody', 'Nobody'],
]);
}
get(url, options) {

View file

@ -7,29 +7,18 @@ const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const CommentsPageView = require('../views/comments_page_view.js');
const fields = ['id', 'comments', 'commentCount', 'thumbnailUrl'];
class CommentsController {
constructor(ctx) {
topNavigation.activate('comments');
const proxy = PageController.createHistoryCacheProxy(
ctx, page => {
const url =
'/posts/?query=sort:comment-date+comment-count-min:1' +
`&page=${page}&pageSize=10&fields=` +
'id,comments,commentCount,thumbnailUrl';
return api.get(url);
});
this._pageController = new PageController({
searchQuery: ctx.searchQuery,
clientUrl: '/comments/' + misc.formatSearchQuery({page: '{page}'}),
requestPage: page => {
return proxy(page).then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: PostList.fromResponse(response.results)}));
});
return PostList.search(
'sort:comment-date+comment-count-min:1', page, 10, fields);
},
pageRenderer: pageCtx => {
Object.assign(pageCtx, {

View file

@ -2,6 +2,7 @@
const api = require('../api.js');
const config = require('../config.js');
const Info = require('../models/info.js');
const topNavigation = require('../models/top_navigation.js');
const HomeView = require('../views/home_view.js');
@ -16,20 +17,20 @@ class HomeController {
canListPosts: api.hasPrivilege('posts:list'),
});
api.get('/info')
.then(response => {
Info.get()
.then(info => {
this._homeView.setStats({
diskUsage: response.diskUsage,
postCount: response.postCount,
diskUsage: info.diskUsage,
postCount: info.postCount,
});
this._homeView.setFeaturedPost({
featuredPost: response.featuredPost,
featuringUser: response.featuringUser,
featuringTime: response.featuringTime,
featuredPost: info.featuredPost,
featuringUser: info.featuringUser,
featuringTime: info.featuringTime,
});
},
response => {
this._homeView.showError(response.description);
errorMessage => {
this._homeView.showError(errorMessage);
});
}

View file

@ -28,22 +28,6 @@ class PageController {
showError(message) {
this._view.showError(message);
}
static createHistoryCacheProxy(routerCtx, requestPage) {
return page => {
if (routerCtx.state.response) {
return new Promise((resolve, reject) => {
resolve(routerCtx.state.response);
});
}
const promise = requestPage(page);
promise.then(response => {
routerCtx.state.response = response;
routerCtx.save();
});
return promise;
};
}
}
module.exports = PageController;

View file

@ -5,6 +5,7 @@ const misc = require('../util/misc.js');
const settings = require('../models/settings.js');
const Comment = require('../models/comment.js');
const Post = require('../models/post.js');
const PostList = require('../models/post_list.js');
const topNavigation = require('../models/top_navigation.js');
const PostView = require('../views/post_view.js');
const EmptyView = require('../views/empty_view.js');
@ -15,8 +16,8 @@ class PostController {
Promise.all([
Post.get(id),
api.get(`/post/${id}/around?fields=id&query=` +
this._decorateSearchQuery(
PostList.getAround(
id, this._decorateSearchQuery(
searchQuery ? searchQuery.text : '')),
]).then(responses => {
const [post, aroundResponse] = responses;
@ -53,9 +54,9 @@ class PostController {
this._view.commentListControl.addEventListener(
'delete', e => this._evtDeleteComment(e));
}
}, response => {
}, errorMessage => {
this._view = new EmptyView();
this._view.showError(response.description);
this._view.showError(errorMessage);
});
}

View file

@ -3,11 +3,16 @@
const api = require('../api.js');
const settings = require('../models/settings.js');
const misc = require('../util/misc.js');
const PostList = require('../models/post_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const PostsHeaderView = require('../views/posts_header_view.js');
const PostsPageView = require('../views/posts_page_view.js');
const fields = [
'id', 'thumbnailUrl', 'type',
'score', 'favoriteCount', 'commentCount', 'tags'];
class PostListController {
constructor(ctx) {
topNavigation.activate('posts');
@ -16,16 +21,11 @@ class PostListController {
searchQuery: ctx.searchQuery,
clientUrl: '/posts/' + misc.formatSearchQuery({
text: ctx.searchQuery.text, page: '{page}'}),
requestPage: PageController.createHistoryCacheProxy(
ctx,
page => {
const text
= this._decorateSearchQuery(ctx.searchQuery.text);
return api.get(
`/posts/?query=${text}&page=${page}&pageSize=40` +
'&fields=id,type,tags,score,favoriteCount,' +
'commentCount,thumbnailUrl');
}),
requestPage: page => {
return PostList.search(
this._decorateSearchQuery(ctx.searchQuery.text),
page, 40, fields);
},
headerRenderer: headerCtx => {
return new PostsHeaderView(headerCtx);
},

View file

@ -3,6 +3,7 @@
const api = require('../api.js');
const tags = require('../tags.js');
const misc = require('../util/misc.js');
const TagCategoryList = require('../models/tag_category_list.js');
const topNavigation = require('../models/top_navigation.js');
const TagCategoriesView = require('../views/tag_categories_view.js');
const EmptyView = require('../views/empty_view.js');
@ -10,66 +11,35 @@ const EmptyView = require('../views/empty_view.js');
class TagCategoriesController {
constructor() {
topNavigation.activate('tags');
api.get('/tag-categories/').then(response => {
TagCategoryList.get().then(response => {
this._tagCategories = response.results;
this._view = new TagCategoriesView({
tagCategories: response.results,
tagCategories: this._tagCategories,
canEditName: api.hasPrivilege('tagCategories:edit:name'),
canEditColor: api.hasPrivilege('tagCategories:edit:color'),
canDelete: api.hasPrivilege('tagCategories:delete'),
canCreate: api.hasPrivilege('tagCategories:create'),
canSetDefault: api.hasPrivilege('tagCategories:setDefault'),
saveChanges: (...args) => {
return this._saveTagCategories(...args);
},
getCategories: () => {
return api.get('/tag-categories/').then(response => {
return Promise.resolve(response.results);
}, response => {
return Promise.reject(response);
});
}
});
this._view.addEventListener('submit', e => this._evtSubmit(e));
}, response => {
this._view = new EmptyView();
this._view.showError(response.description);
});
}
_saveTagCategories(
addedCategories,
changedCategories,
removedCategories,
defaultCategory) {
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(
() => {
if (!defaultCategory) {
return Promise.resolve();
}
return api.put(
'/tag-category/' + defaultCategory + '/default');
}, response => {
return Promise.reject(response);
})
.then(
() => {
tags.refreshExport();
this._view.showSuccess('Changes saved.');
},
response => {
this._view.showError(response.description);
});
_evtSubmit(e) {
this._view.clearMessages();
this._view.disableForm();
this._tagCategories.save()
.then(() => {
tags.refreshExport();
this._view.enableForm();
this._view.showSuccess('Changes saved.');
}, errorMessage => {
this._view.enableForm();
this._view.showError(errorMessage);
});
}
}

View file

@ -3,27 +3,19 @@
const router = require('../router.js');
const api = require('../api.js');
const tags = require('../tags.js');
const Tag = require('../models/tag.js');
const topNavigation = require('../models/top_navigation.js');
const TagView = require('../views/tag_view.js');
const EmptyView = require('../views/empty_view.js');
class TagController {
constructor(ctx, section) {
new Promise((resolve, reject) => {
if (ctx.state.tag) {
resolve(ctx.state.tag);
return;
}
api.get('/tag/' + ctx.params.name).then(response => {
ctx.state.tag = response;
ctx.save();
resolve(ctx.state.tag);
}, response => {
reject(response.description);
});
}).then(tag => {
Tag.get(ctx.params.name).then(tag => {
topNavigation.activate('tags');
this._name = ctx.params.name;
tag.addEventListener('change', e => this._evtSaved(e));
const categories = {};
for (let category of tags.getAllCategories()) {
categories[category.name] = category.name;
@ -50,19 +42,20 @@ class TagController {
});
}
_evtSaved(e) {
if (this._name !== e.detail.tag.names[0]) {
router.replace('/tag/' + e.detail.tag.names[0], null, false);
}
}
_evtChange(e) {
this._view.clearMessages();
this._view.disableForm();
return api.put('/tag/' + e.detail.tag.names[0], {
names: e.detail.names,
category: e.detail.category,
implications: e.detail.implications,
suggestions: e.detail.suggestions,
}).then(response => {
// TODO: update header links and text
if (e.detail.names && e.detail.names[0] !== e.detail.tag.names[0]) {
router.replace('/tag/' + e.detail.names[0], null, false);
}
e.detail.tag.names = e.detail.names;
e.detail.tag.category = e.detail.category;
e.detail.tag.implications = e.detail.implications;
e.detail.tag.suggestions = e.detail.suggestions;
e.detail.tag.save().then(() => {
this._view.showSuccess('Tag saved.');
this._view.enableForm();
}, response => {
@ -74,17 +67,11 @@ class TagController {
_evtMerge(e) {
this._view.clearMessages();
this._view.disableForm();
return api.post(
'/tag-merge/',
{remove: e.detail.tag.names[0], mergeTo: e.detail.targetTagName}
).then(response => {
// TODO: update header links and text
router.replace(
'/tag/' + e.detail.targetTagName + '/merge', null, false);
e.detail.tag.merge(e.detail.targetTagName).then(() => {
this._view.showSuccess('Tag merged.');
this._view.enableForm();
}, response => {
this._view.showError(response.description);
}, errorMessage => {
this._view.showError(errorMessage);
this._view.enableForm();
});
}
@ -92,13 +79,14 @@ class TagController {
_evtDelete(e) {
this._view.clearMessages();
this._view.disableForm();
return api.delete('/tag/' + e.detail.tag.names[0]).then(response => {
const ctx = router.show('/tags/');
ctx.controller.showSuccess('Tag deleted.');
}, response => {
this._view.showError(response.description);
this._view.enableForm();
});
e.detail.tag.delete()
.then(() => {
const ctx = router.show('/tags/');
ctx.controller.showSuccess('Tag deleted.');
}, errorMessage => {
this._view.showError(errorMessage);
this._view.enableForm();
});
}
}

View file

@ -2,11 +2,15 @@
const api = require('../api.js');
const misc = require('../util/misc.js');
const TagList = require('../models/tag_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const TagsHeaderView = require('../views/tags_header_view.js');
const TagsPageView = require('../views/tags_page_view.js');
const fields = [
'names', 'suggestions', 'implications', 'lastEditTime', 'usages'];
class TagListController {
constructor(ctx) {
topNavigation.activate('tags');
@ -15,15 +19,9 @@ class TagListController {
searchQuery: ctx.searchQuery,
clientUrl: '/tags/' + misc.formatSearchQuery({
text: ctx.searchQuery.text, page: '{page}'}),
requestPage: PageController.createHistoryCacheProxy(
ctx,
page => {
const text = ctx.searchQuery.text;
return api.get(
`/tags/?query=${text}&page=${page}&pageSize=50` +
'&fields=names,suggestions,implications,' +
'lastEditTime,usages');
}),
requestPage: page => {
return TagList.search(ctx.searchQuery.text, page, 50, fields);
},
headerRenderer: headerCtx => {
Object.assign(headerCtx, {
canEditTagCategories:

View file

@ -4,39 +4,20 @@ const router = require('../router.js');
const api = require('../api.js');
const config = require('../config.js');
const views = require('../util/views.js');
const User = require('../models/user.js');
const topNavigation = require('../models/top_navigation.js');
const UserView = require('../views/user_view.js');
const EmptyView = require('../views/empty_view.js');
const rankNames = new Map([
['anonymous', 'Anonymous'],
['restricted', 'Restricted user'],
['regular', 'Regular user'],
['power', 'Power user'],
['moderator', 'Moderator'],
['administrator', 'Administrator'],
['nobody', 'Nobody'],
]);
class UserController {
constructor(ctx, section) {
new Promise((resolve, reject) => {
if (ctx.state.user) {
resolve(ctx.state.user);
return;
}
api.get('/user/' + ctx.params.name).then(response => {
response.rankName = rankNames.get(response.rank);
ctx.state.user = response;
ctx.save();
resolve(ctx.state.user);
}, response => {
reject(response.description);
});
}).then(user => {
User.get(ctx.params.name).then(user => {
const isLoggedIn = api.isLoggedIn(user);
const infix = isLoggedIn ? 'self' : 'any';
this._name = ctx.params.name;
user.addEventListener('change', e => this._evtSaved(e));
const myRankIndex = api.user ?
api.allRanks.indexOf(api.user.rank) :
0;
@ -48,7 +29,7 @@ class UserController {
if (rankIdx > myRankIndex) {
continue;
}
ranks[rankIdentifier] = rankNames.get(rankIdentifier);
ranks[rankIdentifier] = api.rankNames.get(rankIdentifier);
}
if (isLoggedIn) {
@ -77,64 +58,64 @@ class UserController {
});
}
_evtSaved(e) {
if (this._name !== e.detail.user.name) {
router.replace(
'/user/' + e.detail.user.name + '/edit', null, false);
}
}
_evtChange(e) {
this._view.clearMessages();
this._view.disableForm();
const isLoggedIn = api.isLoggedIn(e.detail.user);
const infix = isLoggedIn ? 'self' : 'any';
const files = [];
const data = {};
if (e.detail.name) {
data.name = e.detail.name;
if (e.detail.name !== undefined) {
e.detail.user.name = e.detail.name;
}
if (e.detail.password) {
data.password = e.detail.password;
if (e.detail.email !== undefined) {
e.detail.user.email = e.detail.email;
}
if (api.hasPrivilege('users:edit:' + infix + ':email')) {
data.email = e.detail.email;
}
if (e.detail.rank) {
data.rank = e.detail.rank;
}
if (e.detail.avatarStyle &&
(e.detail.avatarStyle != e.detail.user.avatarStyle ||
e.detail.avatarContent)) {
data.avatarStyle = e.detail.avatarStyle;
}
if (e.detail.avatarContent) {
files.avatar = e.detail.avatarContent;
if (e.detail.rank !== undefined) {
e.detail.user.rank = e.detail.rank;
}
api.put('/user/' + e.detail.user.name, data, files)
.then(response => {
return isLoggedIn ?
api.login(
data.name || api.userName,
data.password || api.userPassword,
false) :
Promise.resolve();
}, response => {
return Promise.reject(response.description);
}).then(() => {
if (data.name && data.name !== e.detail.user.name) {
// TODO: update header links and text
router.replace('/user/' + data.name + '/edit', null, false);
}
this._view.showSuccess('Settings updated.');
this._view.enableForm();
}, errorMessage => {
this._view.showError(errorMessage);
this._view.enableForm();
});
if (e.detail.password !== undefined) {
e.detail.user.password = e.detail.password;
}
if (e.detail.avatarStyle !== undefined) {
e.detail.user.avatarStyle = e.detail.avatarStyle;
if (e.detail.avatarContent) {
e.detail.user.avatarContent = e.detail.avatarContent;
}
}
e.detail.user.save().then(() => {
return isLoggedIn ?
api.login(
e.detail.name || api.userName,
e.detail.password || api.userPassword,
false) :
Promise.resolve();
}, errorMessage => {
return Promise.reject(errorMessage);
}).then(() => {
this._view.showSuccess('Settings updated.');
this._view.enableForm();
}, errorMessage => {
this._view.showError(errorMessage);
this._view.enableForm();
});
}
_evtDelete(e) {
this._view.clearMessages();
this._view.disableForm();
const isLoggedIn = api.isLoggedIn(e.detail.user);
api.delete('/user/' + e.detail.user.name)
.then(response => {
e.detail.user.delete()
.then(() => {
if (isLoggedIn) {
api.forget();
api.logout();
@ -146,7 +127,7 @@ class UserController {
const ctx = router.show('/');
ctx.controller.showSuccess('Account deleted.');
}
}, response => {
}, errorMessage => {
this._view.showError(response.description);
this._view.enableForm();
});

View file

@ -2,6 +2,7 @@
const api = require('../api.js');
const misc = require('../util/misc.js');
const UserList = require('../models/user_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const UsersHeaderView = require('../views/users_header_view.js');
@ -15,13 +16,9 @@ class UserListController {
searchQuery: ctx.searchQuery,
clientUrl: '/users/' + misc.formatSearchQuery({
text: ctx.searchQuery.text, page: '{page}'}),
requestPage: PageController.createHistoryCacheProxy(
ctx,
page => {
const text = ctx.searchQuery.text;
return api.get(
`/users/?query=${text}&page=${page}&pageSize=30`);
}),
requestPage: page => {
return UserList.search(ctx.searchQuery.text, page);
},
headerRenderer: headerCtx => {
return new UsersHeaderView(headerCtx);
},

View file

@ -2,6 +2,7 @@
const router = require('../router.js');
const api = require('../api.js');
const User = require('../models/user.js');
const topNavigation = require('../models/top_navigation.js');
const RegistrationView = require('../views/registration_view.js');
@ -15,11 +16,11 @@ class UserRegistrationController {
_evtRegister(e) {
this._view.clearMessages();
this._view.disableForm();
api.post('/users/', {
name: e.detail.name,
password: e.detail.password,
email: e.detail.email
}).then(() => {
const user = new User();
user.name = e.detail.name;
user.email = e.detail.email;
user.password = e.detail.password;
user.save().then(() => {
api.forget();
return api.login(e.detail.name, e.detail.password, false);
}, response => {

View file

@ -25,7 +25,7 @@ class TagAutoCompleteControl extends AutoCompleteControl {
return Array.from(allTags.entries())
.filter(kv => match(transform(kv[0]), text))
.sort((kv1, kv2) => {
return kv2[1].usages - kv1[1].usages;
return kv2[1].postCount - kv1[1].postCount;
})
.map(kv => {
const category = kv[1].category;

View file

@ -0,0 +1,59 @@
'use strict';
const events = require('../events.js');
class AbstractList extends events.EventTarget {
constructor() {
super();
this._list = [];
}
static fromResponse(response) {
const ret = new this();
for (let item of response) {
const addedItem = this._itemClass.fromResponse(item);
addedItem.addEventListener('delete', e => {
ret.remove(addedItem);
});
ret._list.push(addedItem);
}
return ret;
}
add(item) {
item.addEventListener('delete', e => {
this.remove(item);
});
this._list.push(item);
const detail = {};
detail[this.constructor._itemName] = item;
this.dispatchEvent(new CustomEvent('add', {
detail: detail,
}));
}
remove(itemToRemove) {
for (let [index, item] of this._list.entries()) {
if (item !== itemToRemove) {
continue;
}
this._list.splice(index, 1);
const detail = {};
detail[this.constructor._itemName] = itemToRemove;
this.dispatchEvent(new CustomEvent('remove', {
detail: detail,
}));
return;
}
}
get length() {
return this._list.length;
}
[Symbol.iterator]() {
return this._list[Symbol.iterator]();
}
}
module.exports = AbstractList;

View file

@ -6,16 +6,14 @@ const events = require('../events.js');
class Comment extends events.EventTarget {
constructor() {
super();
this.commentList = null;
this._id = null;
this._postId = null;
this._text = null;
this._user = null;
this._id = null;
this._postId = null;
this._text = null;
this._user = null;
this._creationTime = null;
this._lastEditTime = null;
this._score = null;
this._ownScore = null;
this._score = null;
this._ownScore = null;
}
static create(postId) {
@ -30,16 +28,16 @@ class Comment extends events.EventTarget {
return comment;
}
get id() { return this._id; }
get postId() { return this._postId; }
get text() { return this._text; }
get user() { return this._user; }
get id() { return this._id; }
get postId() { return this._postId; }
get text() { return this._text; }
get user() { return this._user; }
get creationTime() { return this._creationTime; }
get lastEditTime() { return this._lastEditTime; }
get score() { return this._score; }
get ownScore() { return this._ownScore; }
get score() { return this._score; }
get ownScore() { return this._ownScore; }
set text(value) { this._text = value; }
set text(value) { this._text = value; }
save() {
let promise = null;
@ -61,7 +59,7 @@ class Comment extends events.EventTarget {
return promise.then(response => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
details: {
detail: {
comment: this,
},
}));
@ -74,11 +72,8 @@ class Comment extends events.EventTarget {
delete() {
return api.delete('/comment/' + this._id)
.then(response => {
if (this.commentList) {
this.commentList.remove(this);
}
this.dispatchEvent(new CustomEvent('delete', {
details: {
detail: {
comment: this,
},
}));
@ -93,7 +88,7 @@ class Comment extends events.EventTarget {
.then(response => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('changeScore', {
details: {
detail: {
comment: this,
},
}));
@ -104,14 +99,14 @@ class Comment extends events.EventTarget {
}
_updateFromResponse(response) {
this._id = response.id;
this._postId = response.postId;
this._text = response.text;
this._user = response.user;
this._id = response.id;
this._postId = response.postId;
this._text = response.text;
this._user = response.user;
this._creationTime = response.creationTime;
this._lastEditTime = response.lastEditTime;
this._score = parseInt(response.score);
this._ownScore = parseInt(response.ownScore);
this._score = parseInt(response.score);
this._ownScore = parseInt(response.ownScore);
}
}

View file

@ -1,59 +1,12 @@
'use strict';
const events = require('../events.js');
const AbstractList = require('./abstract_list.js');
const Comment = require('./comment.js');
class CommentList extends events.EventTarget {
constructor(comments) {
super();
this._list = [];
}
static fromResponse(commentsResponse) {
const commentList = new CommentList();
for (let commentResponse of commentsResponse) {
const comment = Comment.fromResponse(commentResponse);
comment.commentList = commentList;
commentList._list.push(comment);
}
return commentList;
}
get comments() {
return [...this._list];
}
add(comment) {
comment.commentList = this;
this._list.push(comment);
this.dispatchEvent(new CustomEvent('add', {
detail: {
comment: comment,
},
}));
}
remove(commentToRemove) {
for (let [index, comment] of this._list.entries()) {
if (comment.id === commentToRemove.id) {
this._list.splice(index, 1);
break;
}
}
this.dispatchEvent(new CustomEvent('remove', {
detail: {
comment: commentToRemove,
},
}));
}
get length() {
return this._list.length;
}
[Symbol.iterator]() {
return this._list[Symbol.iterator]();
}
class CommentList extends AbstractList {
}
CommentList._itemClass = Comment;
CommentList._itemName = 'comment';
module.exports = CommentList;

16
client/js/models/info.js Normal file
View file

@ -0,0 +1,16 @@
'use strict';
const api = require('../api.js');
class Info {
static get() {
return api.get('/info')
.then(response => {
return Promise.resolve(response);
}, response => {
return Promise.reject(response.errorMessage);
});
}
}
module.exports = Info;

View file

@ -7,67 +7,66 @@ const CommentList = require('./comment_list.js');
class Post extends events.EventTarget {
constructor() {
super();
this._id = null;
this._type = null;
this._mimeType = null;
this._creationTime = null;
this._user = null;
this._safety = null;
this._contentUrl = null;
this._thumbnailUrl = null;
this._canvasWidth = null;
this._canvasHeight = null;
this._fileSize = null;
this._id = null;
this._type = null;
this._mimeType = null;
this._creationTime = null;
this._user = null;
this._safety = null;
this._contentUrl = null;
this._thumbnailUrl = null;
this._canvasWidth = null;
this._canvasHeight = null;
this._fileSize = null;
this._tags = [];
this._notes = [];
this._comments = [];
this._relations = [];
this._tags = [];
this._notes = [];
this._comments = [];
this._relations = [];
this._score = null;
this._score = null;
this._favoriteCount = null;
this._ownScore = null;
this._ownFavorite = null;
this._ownScore = null;
this._ownFavorite = null;
}
get id() { return this._id; }
get type() { return this._type; }
get mimeType() { return this._mimeType; }
get creationTime() { return this._creationTime; }
get user() { return this._user; }
get safety() { return this._safety; }
get contentUrl() { return this._contentUrl; }
get thumbnailUrl() { return this._thumbnailUrl; }
get canvasWidth() { return this._canvasWidth || 800; }
get canvasHeight() { return this._canvasHeight || 450; }
get fileSize() { return this._fileSize || 0; }
get tags() { return this._tags; }
get notes() { return this._notes; }
get comments() { return this._comments; }
get relations() { return this._relations; }
get score() { return this._score; }
get favoriteCount() { return this._favoriteCount; }
get ownFavorite() { return this._ownFavorite; }
get ownScore() { return this._ownScore; }
static fromResponse(response) {
const post = new Post();
post._updateFromResponse(response);
return post;
const ret = new Post();
ret._updateFromResponse(response);
return ret;
}
static get(id) {
return api.get('/post/' + id)
.then(response => {
const post = Post.fromResponse(response);
return Promise.resolve(post);
return Promise.resolve(Post.fromResponse(response));
}, response => {
return Promise.reject(response);
return Promise.reject(response.description);
});
}
get id() { return this._id; }
get type() { return this._type; }
get mimeType() { return this._mimeType; }
get creationTime() { return this._creationTime; }
get user() { return this._user; }
get safety() { return this._safety; }
get contentUrl() { return this._contentUrl; }
get thumbnailUrl() { return this._thumbnailUrl; }
get canvasWidth() { return this._canvasWidth || 800; }
get canvasHeight() { return this._canvasHeight || 450; }
get fileSize() { return this._fileSize || 0; }
get tags() { return this._tags; }
get notes() { return this._notes; }
get comments() { return this._comments; }
get relations() { return this._relations; }
get score() { return this._score; }
get favoriteCount() { return this._favoriteCount; }
get ownFavorite() { return this._ownFavorite; }
get ownScore() { return this._ownScore; }
setScore(score) {
return api.put('/post/' + this._id + '/score', {score: score})
.then(response => {
@ -75,13 +74,13 @@ class Post extends events.EventTarget {
this._updateFromResponse(response);
if (this._ownFavorite !== prevFavorite) {
this.dispatchEvent(new CustomEvent('changeFavorite', {
details: {
detail: {
post: this,
},
}));
}
this.dispatchEvent(new CustomEvent('changeScore', {
details: {
detail: {
post: this,
},
}));
@ -98,13 +97,13 @@ class Post extends events.EventTarget {
this._updateFromResponse(response);
if (this._ownScore !== prevScore) {
this.dispatchEvent(new CustomEvent('changeScore', {
details: {
detail: {
post: this,
},
}));
}
this.dispatchEvent(new CustomEvent('changeFavorite', {
details: {
detail: {
post: this,
},
}));
@ -121,13 +120,13 @@ class Post extends events.EventTarget {
this._updateFromResponse(response);
if (this._ownScore !== prevScore) {
this.dispatchEvent(new CustomEvent('changeScore', {
details: {
detail: {
post: this,
},
}));
}
this.dispatchEvent(new CustomEvent('changeFavorite', {
details: {
detail: {
post: this,
},
}));
@ -138,27 +137,27 @@ class Post extends events.EventTarget {
}
_updateFromResponse(response) {
this._id = response.id;
this._type = response.type;
this._mimeType = response.mimeType;
this._creationTime = response.creationTime;
this._user = response.user;
this._safety = response.safety;
this._contentUrl = response.contentUrl;
this._thumbnailUrl = response.thumbnailUrl;
this._canvasWidth = response.canvasWidth;
this._canvasHeight = response.canvasHeight;
this._fileSize = response.fileSize;
this._id = response.id;
this._type = response.type;
this._mimeType = response.mimeType;
this._creationTime = response.creationTime;
this._user = response.user;
this._safety = response.safety;
this._contentUrl = response.contentUrl;
this._thumbnailUrl = response.thumbnailUrl;
this._canvasWidth = response.canvasWidth;
this._canvasHeight = response.canvasHeight;
this._fileSize = response.fileSize;
this._tags = response.tags;
this._notes = response.notes;
this._comments = CommentList.fromResponse(response.comments);
this._relations = response.relations;
this._tags = response.tags;
this._notes = response.notes;
this._comments = CommentList.fromResponse(response.comments || []);
this._relations = response.relations;
this._score = response.score;
this._score = response.score;
this._favoriteCount = response.favoriteCount;
this._ownScore = response.ownScore;
this._ownFavorite = response.ownFavorite;
this._ownScore = response.ownScore;
this._ownFavorite = response.ownFavorite;
}
};

View file

@ -1,33 +1,35 @@
'use strict';
const events = require('../events.js');
const api = require('../api.js');
const AbstractList = require('./abstract_list.js');
const Post = require('./post.js');
class PostList extends events.EventTarget {
constructor(posts) {
super();
this._list = [];
class PostList extends AbstractList {
static getAround(id, searchQuery) {
return api.get(`/post/${id}/around?fields=id&query=${searchQuery}`)
.then(response => {
return Promise.resolve(response);
}).catch(response => {
return Promise.reject(response.description);
});
}
static fromResponse(postsResponse) {
const postList = new PostList();
for (let postResponse of postsResponse) {
postList._list.push(Post.fromResponse(postResponse));
}
return postList;
}
get posts() {
return [...this._list];
}
get length() {
return this._list.length;
}
[Symbol.iterator]() {
return this._list[Symbol.iterator]();
static search(text, page, pageSize, fields) {
const url =
`/posts/?query=${text}` +
`&page=${page}` +
`&pageSize=${pageSize}` +
`&fields=${fields.join(',')}`;
return api.get(url).then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: PostList.fromResponse(response.results)}));
});
}
}
PostList._itemClass = Post;
PostList._itemName = 'post';
module.exports = PostList;

114
client/js/models/tag.js Normal file
View file

@ -0,0 +1,114 @@
'use strict';
const api = require('../api.js');
const events = require('../events.js');
class Tag extends events.EventTarget {
constructor() {
super();
this._origName = null;
this._names = null;
this._category = null;
this._suggestions = null;
this._implications = null;
this._postCount = null;
this._creationTime = null;
this._lastEditTime = null;
}
get names() { return this._names; }
get category() { return this._category; }
get suggestions() { return this._suggestions; }
get implications() { return this._implications; }
get postCount() { return this._postCount; }
get creationTime() { return this._creationTime; }
get lastEditTime() { return this._lastEditTime; }
set names(value) { this._names = value; }
set category(value) { this._category = value; }
set implications(value) { this._implications = value; }
set suggestions(value) { this._suggestions = value; }
static fromResponse(response) {
const ret = new Tag();
ret._updateFromResponse(response);
return ret;
}
static get(id) {
return api.get('/tag/' + id)
.then(response => {
return Promise.resolve(Tag.fromResponse(response));
}, response => {
return Promise.reject(response.description);
});
}
save() {
const detail = {
names: this.names,
category: this.category,
implications: this.implications,
suggestions: this.suggestions,
};
let promise = this._origName ?
api.put('/tag/' + this._origName, detail) :
api.post('/tags', detail);
return promise
.then(response => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
detail: {
tag: this,
},
}));
return Promise.resolve();
}, response => {
return Promise.reject(response.description);
});
}
merge(targetName) {
return api.post('/tag-merge/', {
remove: this._origName,
mergeTo: targetName,
}).then(response => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
detail: {
tag: this,
},
}));
return Promise.resolve();
}, response => {
return Promise.reject(response.description);
});
}
delete() {
return api.delete('/tag/' + this._origName)
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
tag: this,
},
}));
return Promise.resolve();
}, response => {
return Promise.reject(response.description);
});
}
_updateFromResponse(response) {
this._origName = response.names ? response.names[0] : null;
this._names = response.names;
this._category = response.category;
this._implications = response.implications;
this._suggestions = response.suggestions;
this._creationTime = response.creationTime;
this._lastEditTime = response.lastEditTime;
this._postCount = response.usages;
}
};
module.exports = Tag;

View file

@ -0,0 +1,87 @@
'use strict';
const api = require('../api.js');
const events = require('../events.js');
class TagCategory extends events.EventTarget {
constructor() {
super();
this._name = '';
this._color = '#000000';
this._tagCount = 0;
this._isDefault = false;
this._origName = null;
this._origColor = null;
}
get name() { return this._name; }
get color() { return this._color; }
get tagCount() { return this._tagCount; }
get isDefault() { return this._isDefault; }
get isTransient() { return !this._origName; }
set name(value) { this._name = value; }
set color(value) { this._color = value; }
static fromResponse(response) {
const ret = new TagCategory();
ret._updateFromResponse(response);
return ret;
}
save() {
const data = {};
if (this.name !== this._origName) {
data.name = this.name;
}
if (this.color !== this._origColor) {
data.color = this.color;
}
if (!Object.keys(data).length) {
return Promise.resolve();
}
let promise = this._origName ?
api.put('/tag-category/' + this._origName, data) :
api.post('/tag-categories', data);
return promise
.then(response => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
detail: {
tagCategory: this,
},
}));
return Promise.resolve();
}, response => {
return Promise.reject(response.description);
});
}
delete() {
return api.delete('/tag-category/' + this._origName)
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
tagCategory: this,
},
}));
return Promise.resolve();
}, response => {
return Promise.reject(response.description);
});
}
_updateFromResponse(response) {
this._name = response.name;
this._color = response.color;
this._isDefault = response.default;
this._tagCount = response.usages;
this._origName = this.name;
this._origColor = this.color;
}
}
module.exports = TagCategory;

View file

@ -0,0 +1,80 @@
'use strict';
const api = require('../api.js');
const AbstractList = require('./abstract_list.js');
const TagCategory = require('./tag_category.js');
class TagCategoryList extends AbstractList {
constructor() {
super();
this._defaultCategory = null;
this._origDefaultCategory = null;
this._deletedCategories = [];
this.addEventListener('remove', e => this._evtCategoryDeleted(e));
}
static fromResponse(response) {
const ret = super.fromResponse(response);
ret._defaultCategory = null;
for (let tagCategory of ret) {
if (tagCategory.isDefault) {
ret._defaultCategory = tagCategory;
}
}
ret._origDefaultCategory = ret._defaultCategory;
return ret;
}
static get() {
return api.get('/tag-categories/').then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: TagCategoryList.fromResponse(response.results)}));
});
}
get defaultCategory() {
return this._defaultCategory;
}
set defaultCategory(tagCategory) {
this._defaultCategory = tagCategory;
}
save() {
let promises = [];
for (let tagCategory of this) {
promises.push(tagCategory.save());
}
for (let tagCategory of this._deletedCategories) {
promises.push(tagCategory.delete());
}
if (this._defaultCategory !== this._origDefaultCategory) {
promises.push(
api.put(
`/tag-category/${this._defaultCategory.name}/default`));
}
return Promise.all(promises)
.then(response => {
this._deletedCategories = [];
return Promise.resolve();
}, errorMessage => {
return Promise.reject(
errorMessage.description || errorMessage);
});
}
_evtCategoryDeleted(e) {
if (!e.detail.tagCategory.isTransient) {
this._deletedCategories.push(e.detail.tagCategory);
}
}
}
TagCategoryList._itemClass = TagCategory;
TagCategoryList._itemName = 'tagCategory';
module.exports = TagCategoryList;

View file

@ -0,0 +1,24 @@
'use strict';
const api = require('../api.js');
const AbstractList = require('./abstract_list.js');
const Tag = require('./tag.js');
class TagList extends AbstractList {
static search(text, page, pageSize, fields) {
const url =
`/tags/?query=${text}` +
`&page=${page}` +
`&pageSize=${pageSize}` +
`&fields=${fields.join(',')}`;
return api.get(url).then(response => {
response.results = TagList.fromResponse(response.results);
return Promise.resolve(response);
});
}
}
TagList._itemClass = Tag;
TagList._itemName = 'tag';
module.exports = TagList;

149
client/js/models/user.js Normal file
View file

@ -0,0 +1,149 @@
'use strict';
const api = require('../api.js');
const events = require('../events.js');
class User extends events.EventTarget {
constructor() {
super();
this._name = null;
this._rank = null;
this._email = null;
this._avatarStyle = null;
this._avatarUrl = null;
this._creationTime = null;
this._lastLoginTime = null;
this._commentCount = null;
this._favoritePostCount = null;
this._uploadedPostCount = null;
this._likedPostCount = null;
this._dislikedPostCount = null;
this._origName = null;
this._origEmail = null;
this._origRank = null;
this._origAvatarStyle = null;
this._password = null;
this._avatarContent = null;
}
get name() { return this._name; }
get rank() { return this._rank; }
get email() { return this._email; }
get avatarStyle() { return this._avatarStyle; }
get avatarUrl() { return this._avatarUrl; }
get creationTime() { return this._creationTime; }
get lastLoginTime() { return this._lastLoginTime; }
get commentCount() { return this._commentCount; }
get favoritePostCount() { return this._favoritePostCount; }
get uploadedPostCount() { return this._uploadedPostCount; }
get likedPostCount() { return this._likedPostCount; }
get dislikedPostCount() { return this._dislikedPostCount; }
get rankName() { return api.rankNames.get(this.rank); }
get avatarContent() { throw 'Invalid operation'; }
get password() { throw 'Invalid operation'; }
set name(value) { this._name = value; }
set rank(value) { this._rank = value; }
set email(value) { this._email = value || null; }
set avatarStyle(value) { this._avatarStyle = value; }
set avatarContent(value) { this._avatarContent = value; }
set password(value) { this._password = value; }
static fromResponse(response) {
const ret = new User();
ret._updateFromResponse(response);
return ret;
}
static get(name) {
return api.get('/user/' + name)
.then(response => {
return Promise.resolve(User.fromResponse(response));
}, response => {
return Promise.reject(response.description);
});
}
save() {
const files = [];
const data = {};
if (this.name !== this._origName) {
data.name = this.name;
}
if (this._password) {
data.password = this._password;
}
if (this.email !== this._origEmail) {
data.email = this.email;
}
if (this.rank !== this._origRank) {
data.rank = this.rank;
}
if (this.avatarStyle !== this._origAvatarStyle) {
data.avatarStyle = this.avatarStyle;
}
if (this._avatarContent) {
files.avatar = this._avatarContent;
}
let promise = this._origName ?
api.put('/user/' + this._origName, data, files) :
api.post('/users', data, files);
return promise
.then(response => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
detail: {
user: this,
},
}));
return Promise.resolve();
}, response => {
return Promise.reject(response.description);
});
}
delete() {
return api.delete('/user/' + this._origName)
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
user: this,
},
}));
return Promise.resolve();
}, response => {
return Promise.reject(response.description);
});
}
_updateFromResponse(response) {
this._name = response.name;
this._rank = response.rank;
this._email = response.email;
this._avatarStyle = response.avatarStyle;
this._avatarUrl = response.avatarUrl;
this._creationTime = response.creationTime;
this._lastLoginTime = response.lastLoginTime;
this._commentCount = response.commentCount;
this._favoritePostCount = response.favoritePostCount;
this._uploadedPostCount = response.uploadedPostCount;
this._likedPostCount = response.likedPostCount;
this._dislikedPostCount = response.dislikedPostCount;
this._origName = this.name;
this._origRank = this.rank;
this._origEmail = this.email;
this._origAvatarStyle = this.avatarStyle;
this._password = null;
this._avatarContent = null;
}
};
module.exports = User;

View file

@ -0,0 +1,22 @@
'use strict';
const api = require('../api.js');
const AbstractList = require('./abstract_list.js');
const User = require('./user.js');
class UserList extends AbstractList {
static search(text, page) {
const url = `/users/?query=${text}&page=${page}&pageSize=30`;
return api.get(url).then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: UserList.fromResponse(response.results)}));
});
}
}
UserList._itemClass = User;
UserList._itemName = 'user';
module.exports = UserList;

View file

@ -1,40 +1,59 @@
'use strict';
const events = require('../events.js');
const views = require('../util/views.js');
const TagCategory = require('../models/tag_category.js');
const template = views.getTemplate('tag-categories');
const rowTemplate = views.getTemplate('tag-category-row');
class TagCategoriesView {
class TagCategoriesView extends events.EventTarget {
constructor(ctx) {
super();
this._ctx = ctx;
this._hostNode = document.getElementById('content-holder');
const sourceNode = template(ctx);
const formNode = sourceNode.querySelector('form');
const newRowTemplate = sourceNode.querySelector('.add-template');
const tableBodyNode = sourceNode.querySelector('tbody');
const addLinkNode = sourceNode.querySelector('a.add');
views.replaceContent(this._hostNode, template(ctx));
views.decorateValidator(this._formNode);
newRowTemplate.parentNode.removeChild(newRowTemplate);
views.decorateValidator(formNode);
for (let row of tableBodyNode.querySelectorAll('tr')) {
this._addRowHandlers(row);
}
if (addLinkNode) {
addLinkNode.addEventListener('click', e => {
e.preventDefault();
let newRow = newRowTemplate.cloneNode(true);
tableBody.appendChild(newRow);
this._addRowHandlers(row);
});
}
formNode.addEventListener('submit', e => {
this._evtSaveButtonClick(e, ctx);
const categoriesToAdd = Array.from(ctx.tagCategories);
categoriesToAdd.sort((a, b) => {
if (b.isDefault) {
return 1;
} else if (a.isDefault) {
return -1;
}
return a.name.localeCompare(b.name);
});
for (let tagCategory of categoriesToAdd) {
this._addTagCategoryRowNode(tagCategory);
}
views.replaceContent(this._hostNode, sourceNode);
if (this._addLinkNode) {
this._addLinkNode.addEventListener(
'click', e => this._evtAddButtonClick(e));
}
ctx.tagCategories.addEventListener(
'add', e => this._evtTagCategoryAdded(e));
ctx.tagCategories.addEventListener(
'remove', e => this._evtTagCategoryDeleted(e));
this._formNode.addEventListener(
'submit', e => this._evtSaveButtonClick(e, ctx));
}
enableForm() {
views.enableForm(this._formNode);
}
disableForm() {
views.disableForm(this._formNode);
}
clearMessages() {
views.clearMessages(this._hostNode);
}
showSuccess(message) {
@ -45,88 +64,100 @@ class TagCategoriesView {
views.showError(this._hostNode, message);
}
_evtSaveButtonClick(e, ctx) {
e.preventDefault();
views.clearMessages(this._hostNode);
const tableBodyNode = this._hostNode.querySelector('tbody');
ctx.getCategories().then(categories => {
let existingCategories = {};
for (let category of categories) {
existingCategories[category.name] = category;
}
let defaultCategory = null;
let addedCategories = [];
let removedCategories = [];
let changedCategories = [];
let allNames = [];
for (let row of tableBodyNode.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 (row.classList.contains('default')) {
defaultCategory = category.name;
}
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,
defaultCategory);
});
get _formNode() {
return this._hostNode.querySelector('form');
}
_evtRemoveButtonClick(e, row, link) {
get _tableBodyNode() {
return this._hostNode.querySelector('tbody');
}
get _addLinkNode() {
return this._hostNode.querySelector('a.add');
}
_addTagCategoryRowNode(tagCategory) {
const rowNode = rowTemplate(
Object.assign(
{}, this._ctx, {tagCategory: tagCategory}));
const nameInput = rowNode.querySelector('.name input');
if (nameInput) {
nameInput.addEventListener(
'change', e => this._evtNameChange(e, rowNode));
}
const colorInput = rowNode.querySelector('.color input');
if (colorInput) {
colorInput.addEventListener(
'change', e => this._evtColorChange(e, rowNode));
}
const removeLinkNode = rowNode.querySelector('.remove a');
if (removeLinkNode) {
removeLinkNode.addEventListener(
'click', e => this._evtDeleteButtonClick(e, rowNode));
}
const defaultLinkNode = rowNode.querySelector('.set-default a');
if (defaultLinkNode) {
defaultLinkNode.addEventListener(
'click', e => this._evtSetDefaultButtonClick(e, rowNode));
}
this._tableBodyNode.appendChild(rowNode);
rowNode._tagCategory = tagCategory;
tagCategory._rowNode = rowNode;
}
_removeTagCategoryRowNode(tagCategory) {
const rowNode = tagCategory._rowNode;
rowNode.parentNode.removeChild(rowNode);
}
_evtTagCategoryAdded(e) {
this._addTagCategoryRowNode(e.detail.tagCategory);
}
_evtTagCategoryDeleted(e) {
this._removeTagCategoryRowNode(e.detail.tagCategory);
}
_evtAddButtonClick(e) {
e.preventDefault();
if (link.classList.contains('inactive')) {
this._ctx.tagCategories.add(new TagCategory());
}
_evtNameChange(e, rowNode) {
rowNode._tagCategory.name = e.target.value;
}
_evtColorChange(e, rowNode) {
rowNode._tagCategory.color = e.target.value;
}
_evtDeleteButtonClick(e, rowNode, link) {
e.preventDefault();
if (e.target.classList.contains('inactive')) {
return;
}
row.parentNode.removeChild(row);
this._ctx.tagCategories.remove(rowNode._tagCategory);
}
_evtSetDefaultButtonClick(e, row) {
_evtSetDefaultButtonClick(e, rowNode) {
e.preventDefault();
const oldRowNode = row.parentNode.querySelector('tr.default');
this._ctx.tagCategories.defaultCategory = rowNode._tagCategory;
const oldRowNode = rowNode.parentNode.querySelector('tr.default');
if (oldRowNode) {
oldRowNode.classList.remove('default');
}
row.classList.add('default');
rowNode.classList.add('default');
}
_addRowHandlers(row) {
const removeLink = row.querySelector('.remove a');
if (removeLink) {
removeLink.addEventListener(
'click', e => this._evtRemoveButtonClick(e, row, removeLink));
}
const defaultLink = row.querySelector('.set-default a');
if (defaultLink) {
defaultLink.addEventListener(
'click', e => this._evtSetDefaultButtonClick(e, row));
}
_evtSaveButtonClick(e, ctx) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit'));
}
}

View file

@ -12,16 +12,21 @@ class TagView extends events.EventTarget {
constructor(ctx) {
super();
this._hostNode = document.getElementById('content-holder');
this._ctx = ctx;
ctx.tag.addEventListener('change', e => this._evtChange(e));
ctx.section = ctx.section || 'summary';
this._hostNode = document.getElementById('content-holder');
this._install();
}
_install() {
const ctx = this._ctx;
views.replaceContent(this._hostNode, template(ctx));
for (let item of this._hostNode.querySelectorAll('[data-name]')) {
if (item.getAttribute('data-name') === ctx.section) {
item.className = 'active';
} else {
item.className = '';
}
item.classList.toggle(
'active', item.getAttribute('data-name') === ctx.section);
}
ctx.hostNode = this._hostNode.querySelector('.tag-content-holder');
@ -65,6 +70,11 @@ class TagView extends events.EventTarget {
showError(message) {
this._view.showError(message);
}
_evtChange(e) {
this._ctx.tag = e.detail.tag;
this._install(this._ctx);
}
}
module.exports = TagView;

View file

@ -60,12 +60,12 @@ class UserEditView extends events.EventTarget {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
user: this._user,
name: this._userNameFieldNode.value,
password: this._passwordFieldNode.value,
email: this._emailFieldNode.value,
rank: this._rankFieldNode.value,
avatarStyle: this._avatarStyleFieldNode.value,
user: this._user,
name: (this._userNameFieldNode || {}).value,
email: (this._emailFieldNode || {}).value,
rank: (this._rankFieldNode || {}).value,
avatarStyle: (this._avatarStyleFieldNode || {}).value,
password: (this._passwordFieldNode || {}).value,
avatarContent: this._avatarContent,
},
}));

View file

@ -12,10 +12,17 @@ class UserView extends events.EventTarget {
constructor(ctx) {
super();
this._hostNode = document.getElementById('content-holder');
this._ctx = ctx;
ctx.user.addEventListener('change', e => this._evtChange(e));
ctx.section = ctx.section || 'summary';
views.replaceContent(this._hostNode, template(ctx));
this._hostNode = document.getElementById('content-holder');
this._install();
}
_install() {
const ctx = this._ctx;
views.replaceContent(this._hostNode, template(ctx));
for (let item of this._hostNode.querySelectorAll('[data-name]')) {
if (item.getAttribute('data-name') === ctx.section) {
item.className = 'active';
@ -61,6 +68,11 @@ class UserView extends events.EventTarget {
disableForm() {
this._view.disableForm();
}
_evtChange(e) {
this._ctx.user = e.detail.user;
this._install(this._ctx);
}
}
module.exports = UserView;