client/general: refactor control flow

- Controller lifetime is bound to route lifetime
- View lifetime is bound to controller lifetime
- Control lifetime is bound to view lifetime
- Enhanced event dispatching
- Enhanced responsiveness in some places
- Views communicate user input to controllers via new event system
This commit is contained in:
rr- 2016-06-14 10:31:48 +02:00
parent c74f06da35
commit 54e3099c56
68 changed files with 1755 additions and 1561 deletions

View file

@ -2,9 +2,7 @@
<div class='messages'></div> <div class='messages'></div>
<header> <header>
<h1><%= ctx.name %></h1> <h1><%= ctx.name %></h1>
<aside> <aside class='stats-container'></aside>
Serving <%= ctx.postCount %> posts (<%= ctx.makeFileSize(ctx.diskUsage) %>)
</aside>
</header> </header>
<% if (ctx.canListPosts) { %> <% if (ctx.canListPosts) { %>
<form class='horizontal'> <form class='horizontal'>

View file

@ -0,0 +1 @@
Serving <%= ctx.postCount %> posts (<%= ctx.makeFileSize(ctx.diskUsage) %>)

View file

@ -6,8 +6,9 @@ const request = require('superagent');
const config = require('./config.js'); const config = require('./config.js');
const events = require('./events.js'); const events = require('./events.js');
class Api { class Api extends events.EventTarget {
constructor() { constructor() {
super();
this.user = null; this.user = null;
this.userName = null; this.userName = null;
this.userPassword = null; this.userPassword = null;
@ -136,11 +137,10 @@ class Api {
options); options);
this.user = response; this.user = response;
resolve(); resolve();
events.notify(events.Authentication); this.dispatchEvent(new CustomEvent('login'));
}).catch(response => { }).catch(response => {
reject(response.description); reject(response.description);
this.logout(); this.logout();
events.notify(events.Authentication);
}); });
}); });
} }
@ -149,7 +149,7 @@ class Api {
this.user = null; this.user = null;
this.userName = null; this.userName = null;
this.userPassword = null; this.userPassword = null;
events.notify(events.Authentication); this.dispatchEvent(new CustomEvent('logout'));
} }
forget() { forget() {

View file

@ -2,105 +2,47 @@
const router = require('../router.js'); const router = require('../router.js');
const api = require('../api.js'); const api = require('../api.js');
const events = require('../events.js'); const topNavigation = require('../models/top_navigation.js');
const TopNavigation = require('../models/top_navigation.js');
const LoginView = require('../views/login_view.js'); const LoginView = require('../views/login_view.js');
const PasswordResetView = require('../views/password_reset_view.js');
class AuthController { class LoginController {
constructor() { constructor() {
api.forget();
topNavigation.activate('login');
this._loginView = new LoginView(); this._loginView = new LoginView();
this._passwordResetView = new PasswordResetView(); this._loginView.addEventListener('submit', e => this._evtLogin(e));
} }
registerRoutes() { _evtLogin(e) {
router.enter( this._loginView.clearMessages();
/\/password-reset\/([^:]+):([^:]+)$/, this._loginView.disableForm();
(ctx, next) => {
this._passwordResetFinishRoute(ctx.params[0], ctx.params[1]);
});
router.enter(
'/password-reset',
(ctx, next) => { this._passwordResetRoute(); });
router.enter(
'/login',
(ctx, next) => { this._loginRoute(); });
router.enter(
'/logout',
(ctx, next) => { this._logoutRoute(); });
}
_loginRoute() {
api.forget(); api.forget();
TopNavigation.activate('login'); api.login(e.detail.name, e.detail.password, e.detail.remember)
this._loginView.render({ .then(() => {
login: (name, password, doRemember) => { const ctx = router.show('/');
return new Promise((resolve, reject) => { ctx.controller.showSuccess('Logged in');
api.forget();
api.login(name, password, doRemember)
.then(() => {
resolve();
router.show('/');
events.notify(events.Success, 'Logged in');
}, errorMessage => {
reject(errorMessage);
events.notify(events.Error, errorMessage);
});
});
}});
}
_logoutRoute() {
api.forget();
api.logout();
router.show('/');
events.notify(events.Success, 'Logged out');
}
_passwordResetRoute() {
TopNavigation.activate('login');
this._passwordResetView.render({
proceed: (...args) => {
return this._passwordReset(...args);
}});
}
_passwordResetFinishRoute(name, token) {
api.forget();
api.logout();
let password = null;
api.post('/password-reset/' + name, {token: token})
.then(response => {
password = response.password;
return api.login(name, password, false);
}, response => {
return Promise.reject(response.description);
}).then(() => {
router.show('/');
events.notify(events.Success, 'New password: ' + password);
}, errorMessage => { }, errorMessage => {
router.show('/'); this._loginView.showError(errorMessage);
events.notify(events.Error, errorMessage); this._loginView.enableForm();
}); });
} }
_passwordReset(nameOrEmail) {
api.forget();
api.logout();
return new Promise((resolve, reject) => {
api.get('/password-reset/' + nameOrEmail)
.then(() => {
resolve();
events.notify(
events.Success,
'E-mail has been sent. To finish the procedure, ' +
'please click the link it contains.');
}, response => {
reject();
events.notify(events.Error, response.description);
});
});
}
} }
module.exports = new AuthController(); class LogoutController {
constructor() {
api.forget();
api.logout();
const ctx = router.show('/');
ctx.controller.showSuccess('Logged out');
}
}
module.exports = router => {
router.enter('/login', (ctx, next) => {
ctx.controller = new LoginController();
});
router.enter('/logout', (ctx, next) => {
ctx.controller = new LogoutController();
});
};

View file

@ -1,29 +1,19 @@
'use strict'; 'use strict';
const api = require('../api.js'); const api = require('../api.js');
const router = require('../router.js');
const misc = require('../util/misc.js'); const misc = require('../util/misc.js');
const pageController = require('../controllers/page_controller.js'); const topNavigation = require('../models/top_navigation.js');
const TopNavigation = require('../models/top_navigation.js'); const PageController = require('../controllers/page_controller.js');
const CommentsPageView = require('../views/comments_page_view.js'); const CommentsPageView = require('../views/comments_page_view.js');
const EmptyView = require('../views/empty_view.js');
class CommentsController { class CommentsController {
registerRoutes() { constructor(ctx) {
router.enter('/comments/:query?', topNavigation.activate('comments');
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
(ctx, next) => { this._listCommentsRoute(ctx); });
this._commentsPageView = new CommentsPageView();
this._emptyView = new EmptyView();
}
_listCommentsRoute(ctx) { this._pageController = new PageController({
TopNavigation.activate('comments');
pageController.run({
searchQuery: ctx.searchQuery, searchQuery: ctx.searchQuery,
clientUrl: '/comments/' + misc.formatSearchQuery({page: '{page}'}), clientUrl: '/comments/' + misc.formatSearchQuery({page: '{page}'}),
requestPage: pageController.createHistoryCacheProxy( requestPage: PageController.createHistoryCacheProxy(
ctx, ctx,
page => { page => {
return api.get( return api.get(
@ -31,12 +21,18 @@ class CommentsController {
`&page=${page}&pageSize=10&fields=` + `&page=${page}&pageSize=10&fields=` +
'id,comments,commentCount,thumbnailUrl'); 'id,comments,commentCount,thumbnailUrl');
}), }),
pageRenderer: this._commentsPageView, pageRenderer: pageCtx => {
pageContext: { Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege('posts:view'), canViewPosts: api.hasPrivilege('posts:view'),
} });
return new CommentsPageView(pageCtx);
},
}); });
} }
} };
module.exports = new CommentsController(); module.exports = router => {
router.enter('/comments/:query?',
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
(ctx, next) => { new CommentsController(ctx); });
};

View file

@ -1,35 +1,23 @@
'use strict'; 'use strict';
const router = require('../router.js'); const topNavigation = require('../models/top_navigation.js');
const TopNavigation = require('../models/top_navigation.js');
const HelpView = require('../views/help_view.js'); const HelpView = require('../views/help_view.js');
class HelpController { class HelpController {
constructor() { constructor(section, subsection) {
this._helpView = new HelpView(); topNavigation.activate('help');
} this._helpView = new HelpView(section, subsection);
registerRoutes() {
router.enter(
'/help',
(ctx, next) => { this._showHelpRoute(); });
router.enter(
'/help/:section',
(ctx, next) => { this._showHelpRoute(ctx.params.section); });
router.enter(
'/help/:section/:subsection',
(ctx, next) => {
this._showHelpRoute(ctx.params.section, ctx.params.subsection);
});
}
_showHelpRoute(section, subsection) {
TopNavigation.activate('help');
this._helpView.render({
section: section,
subsection: subsection,
});
} }
} }
module.exports = new HelpController(); module.exports = router => {
router.enter('/help', (ctx, next) => {
new HelpController();
});
router.enter('/help/:section', (ctx, next) => {
new HelpController(ctx.params.section);
});
router.enter('/help/:section/:subsection', (ctx, next) => {
new HelpController(ctx.params.section, ctx.params.subsection);
});
};

View file

@ -1,18 +1,15 @@
'use strict'; 'use strict';
const router = require('../router.js'); const topNavigation = require('../models/top_navigation.js');
const TopNavigation = require('../models/top_navigation.js');
class HistoryController { class HistoryController {
registerRoutes() { constructor() {
router.enter( topNavigation.activate('');
'/history',
(ctx, next) => { this._listHistoryRoute(); });
}
_listHistoryRoute() {
TopNavigation.activate('');
} }
} }
module.exports = new HistoryController(); module.exports = router => {
router.enter('/history', (ctx, next) => {
ctx.controller = new HistoryController();
});
};

View file

@ -1,53 +1,49 @@
'use strict'; 'use strict';
const router = require('../router.js');
const api = require('../api.js'); const api = require('../api.js');
const events = require('../events.js'); const config = require('../config.js');
const TopNavigation = require('../models/top_navigation.js'); const topNavigation = require('../models/top_navigation.js');
const HomeView = require('../views/home_view.js'); const HomeView = require('../views/home_view.js');
const NotFoundView = require('../views/not_found_view.js');
class HomeController { class HomeController {
constructor() { constructor() {
this._homeView = new HomeView(); topNavigation.activate('home');
this._notFoundView = new NotFoundView();
}
registerRoutes() { this._homeView = new HomeView({
router.enter( name: config.name,
'/', version: config.meta.version,
(ctx, next) => { this._indexRoute(); }); buildDate: config.meta.buildDate,
router.enter( canListPosts: api.hasPrivilege('posts:list'),
'*', });
(ctx, next) => { this._notFoundRoute(ctx); });
}
_indexRoute() {
TopNavigation.activate('home');
api.get('/info') api.get('/info')
.then(response => { .then(response => {
this._homeView.render({ this._homeView.setStats({
canListPosts: api.hasPrivilege('posts:list'),
diskUsage: response.diskUsage, diskUsage: response.diskUsage,
postCount: response.postCount, postCount: response.postCount,
});
this._homeView.setFeaturedPost({
featuredPost: response.featuredPost, featuredPost: response.featuredPost,
featuringUser: response.featuringUser, featuringUser: response.featuringUser,
featuringTime: response.featuringTime, featuringTime: response.featuringTime,
}); });
}, },
response => { response => {
this._homeView.render({ this._homeView.showError(response.description);
canListPosts: api.hasPrivilege('posts:list'),
});
events.notify(events.Error, response.description);
}); });
} }
_notFoundRoute(ctx) { showSuccess(message) {
TopNavigation.activate(''); this._homeView.showSuccess(message);
this._notFoundView.render({path: ctx.canonicalPath});
} }
}
module.exports = new HomeController(); showError(message) {
this._homeView.showError(message);
}
};
module.exports = router => {
router.enter('/', (ctx, next) => {
ctx.controller = new HomeController();
});
};

View file

@ -0,0 +1,17 @@
'use strict';
const topNavigation = require('../models/top_navigation.js');
const NotFoundView = require('../views/not_found_view.js');
class NotFoundController {
constructor(path) {
topNavigation.activate('');
this._notFoundView = new NotFoundView(path);
}
};
module.exports = router => {
router.enter('*', (ctx, next) => {
ctx.controller = new NotFoundController(ctx.canonicalPath);
});
};

View file

@ -1,43 +1,35 @@
'use strict'; 'use strict';
const events = require('../events.js'); const settings = require('../models/settings.js');
const settings = require('../settings.js');
const EndlessPageView = require('../views/endless_page_view.js'); const EndlessPageView = require('../views/endless_page_view.js');
const ManualPageView = require('../views/manual_page_view.js'); const ManualPageView = require('../views/manual_page_view.js');
class PageController { class PageController {
constructor() { constructor(ctx) {
events.listen(events.SettingsChange, () => {
this._update();
return true;
});
this._update();
}
_update() {
if (settings.getSettings().endlessScroll) {
this._pageView = new EndlessPageView();
} else {
this._pageView = new ManualPageView();
}
}
run(ctx) {
this._pageView.unrender();
const extendedContext = { const extendedContext = {
clientUrl: ctx.clientUrl, clientUrl: ctx.clientUrl,
searchQuery: ctx.searchQuery, searchQuery: ctx.searchQuery,
}; };
ctx.headerContext = ctx.headerContext || {}; ctx.headerContext = Object.assign({}, extendedContext);
ctx.pageContext = ctx.pageContext || {}; ctx.pageContext = Object.assign({}, extendedContext);
Object.assign(ctx.headerContext, extendedContext);
Object.assign(ctx.pageContext, extendedContext); if (settings.get().endlessScroll) {
this._pageView.render(ctx); this._view = new EndlessPageView(ctx);
} else {
this._view = new ManualPageView(ctx);
}
} }
createHistoryCacheProxy(routerCtx, requestPage) { showSuccess(message) {
this._view.showSuccess(message);
}
showError(message) {
this._view.showError(message);
}
static createHistoryCacheProxy(routerCtx, requestPage) {
return page => { return page => {
if (routerCtx.state.response) { if (routerCtx.state.response) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -52,10 +44,6 @@ class PageController {
return promise; return promise;
}; };
} }
stop() {
this._pageView.unrender();
}
} }
module.exports = new PageController(); module.exports = PageController;

View file

@ -0,0 +1,63 @@
'use strict';
const router = require('../router.js');
const api = require('../api.js');
const topNavigation = require('../models/top_navigation.js');
const PasswordResetView = require('../views/password_reset_view.js');
class PasswordResetController {
constructor() {
topNavigation.activate('login');
this._passwordResetView = new PasswordResetView();
this._passwordResetView.addEventListener(
'submit', e => this._evtReset(e));
}
_evtReset(e) {
this._passwordResetView.clearMessages();
this._passwordResetView.disableForm();
api.forget();
api.logout();
api.get('/password-reset/' + e.detail.userNameOrEmail)
.then(() => {
this._passwordResetView.showSuccess(
'E-mail has been sent. To finish the procedure, ' +
'please click the link it contains.');
}, response => {
this._passwordResetView.showError(response.description);
this._passwordResetView.enableForm();
});
}
}
class PasswordResetFinishController {
constructor(name, token) {
api.forget();
api.logout();
let password = null;
api.post('/password-reset/' + name, {token: token})
.then(response => {
password = response.password;
return api.login(name, password, false);
}, response => {
return Promise.reject(response.description);
}).then(() => {
const ctx = router.show('/');
ctx.controller.showSuccess('New password: ' + password);
}, errorMessage => {
const ctx = router.show('/');
ctx.controller.showError(errorMessage);
});
}
}
module.exports = router => {
router.enter('/password-reset', (ctx, next) => {
ctx.controller = new PasswordResetController();
});
router.enter(/\/password-reset\/([^:]+):([^:]+)$/, (ctx, next) => {
ctx.controller = new PasswordResetFinishController(
ctx.params[0], ctx.params[1]);
});
};

View file

@ -1,38 +1,23 @@
'use strict'; 'use strict';
const router = require('../router.js');
const api = require('../api.js'); const api = require('../api.js');
const events = require('../events.js'); const settings = require('../models/settings.js');
const settings = require('../settings.js');
const Post = require('../models/post.js'); const Post = require('../models/post.js');
const TopNavigation = require('../models/top_navigation.js'); const topNavigation = require('../models/top_navigation.js');
const PostView = require('../views/post_view.js'); const PostView = require('../views/post_view.js');
const EmptyView = require('../views/empty_view.js'); const EmptyView = require('../views/empty_view.js');
class PostController { class PostController {
constructor() { constructor(id, editMode) {
this._postView = new PostView(); topNavigation.activate('posts');
this._emptyView = new EmptyView();
}
registerRoutes() {
router.enter(
'/post/:id',
(ctx, next) => { this._showPostRoute(ctx.params.id, false); });
router.enter(
'/post/:id/edit',
(ctx, next) => { this._showPostRoute(ctx.params.id, true); });
}
_showPostRoute(id, editMode) {
TopNavigation.activate('posts');
Promise.all([ Promise.all([
Post.get(id), Post.get(id),
api.get(`/post/${id}/around?fields=id&query=` + api.get(`/post/${id}/around?fields=id&query=` +
this._decorateSearchQuery('')), this._decorateSearchQuery('')),
]).then(responses => { ]).then(responses => {
const [post, aroundResponse] = responses; const [post, aroundResponse] = responses;
this._postView.render({ this._view = new PostView({
post: post, post: post,
editMode: editMode, editMode: editMode,
nextPostId: aroundResponse.next ? aroundResponse.next.id : null, nextPostId: aroundResponse.next ? aroundResponse.next.id : null,
@ -42,13 +27,13 @@ class PostController {
canCreateComments: api.hasPrivilege('comments:create'), canCreateComments: api.hasPrivilege('comments:create'),
}); });
}, response => { }, response => {
this._emptyView.render(); this._view = new EmptyView();
events.notify(events.Error, response.description); this._view.showError(response.description);
}); });
} }
_decorateSearchQuery(text) { _decorateSearchQuery(text) {
const browsingSettings = settings.getSettings(); const browsingSettings = settings.get();
let disabledSafety = []; let disabledSafety = [];
for (let key of Object.keys(browsingSettings.listPosts)) { for (let key of Object.keys(browsingSettings.listPosts)) {
if (browsingSettings.listPosts[key] === false) { if (browsingSettings.listPosts[key] === false) {
@ -62,4 +47,11 @@ class PostController {
} }
} }
module.exports = new PostController(); module.exports = router => {
router.enter('/post/:id', (ctx, next) => {
ctx.controller = new PostController(ctx.params.id, false);
});
router.enter('/post/:id/edit', (ctx, next) => {
ctx.controller = new PostController(ctx.params.id, true);
});
};

View file

@ -1,35 +1,22 @@
'use strict'; 'use strict';
const router = require('../router.js');
const api = require('../api.js'); const api = require('../api.js');
const settings = require('../settings.js'); const settings = require('../models/settings.js');
const misc = require('../util/misc.js'); const misc = require('../util/misc.js');
const pageController = require('../controllers/page_controller.js'); const topNavigation = require('../models/top_navigation.js');
const TopNavigation = require('../models/top_navigation.js'); const PageController = require('../controllers/page_controller.js');
const PostsHeaderView = require('../views/posts_header_view.js'); const PostsHeaderView = require('../views/posts_header_view.js');
const PostsPageView = require('../views/posts_page_view.js'); const PostsPageView = require('../views/posts_page_view.js');
class PostListController { class PostListController {
constructor() { constructor(ctx) {
this._postsHeaderView = new PostsHeaderView(); topNavigation.activate('posts');
this._postsPageView = new PostsPageView();
}
registerRoutes() { this._pageController = new PageController({
router.enter(
'/posts/:query?',
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
(ctx, next) => { this._listPostsRoute(ctx); });
}
_listPostsRoute(ctx) {
TopNavigation.activate('posts');
pageController.run({
searchQuery: ctx.searchQuery, searchQuery: ctx.searchQuery,
clientUrl: '/posts/' + misc.formatSearchQuery({ clientUrl: '/posts/' + misc.formatSearchQuery({
text: ctx.searchQuery.text, page: '{page}'}), text: ctx.searchQuery.text, page: '{page}'}),
requestPage: pageController.createHistoryCacheProxy( requestPage: PageController.createHistoryCacheProxy(
ctx, ctx,
page => { page => {
const text const text
@ -39,16 +26,20 @@ class PostListController {
'&fields=id,type,tags,score,favoriteCount,' + '&fields=id,type,tags,score,favoriteCount,' +
'commentCount,thumbnailUrl'); 'commentCount,thumbnailUrl');
}), }),
headerRenderer: this._postsHeaderView, headerRenderer: headerCtx => {
pageRenderer: this._postsPageView, return new PostsHeaderView(headerCtx);
pageContext: { },
canViewPosts: api.hasPrivilege('posts:view'), pageRenderer: pageCtx => {
} Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege('posts:view'),
});
return new PostsPageView(pageCtx);
},
}); });
} }
_decorateSearchQuery(text) { _decorateSearchQuery(text) {
const browsingSettings = settings.getSettings(); const browsingSettings = settings.get();
let disabledSafety = []; let disabledSafety = [];
for (let key of Object.keys(browsingSettings.listPosts)) { for (let key of Object.keys(browsingSettings.listPosts)) {
if (browsingSettings.listPosts[key] === false) { if (browsingSettings.listPosts[key] === false) {
@ -62,4 +53,9 @@ class PostListController {
} }
} }
module.exports = new PostListController(); module.exports = router => {
router.enter(
'/posts/:query?',
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
(ctx, next) => { ctx.controller = new PostListController(ctx); });
};

View file

@ -1,24 +1,17 @@
'use strict'; 'use strict';
const router = require('../router.js'); const topNavigation = require('../models/top_navigation.js');
const TopNavigation = require('../models/top_navigation.js');
const EmptyView = require('../views/empty_view.js'); const EmptyView = require('../views/empty_view.js');
class PostUploadController { class PostUploadController {
constructor() { constructor() {
topNavigation.activate('upload');
this._emptyView = new EmptyView(); this._emptyView = new EmptyView();
} }
registerRoutes() {
router.enter(
'/upload',
(ctx, next) => { this._uploadPostsRoute(); });
}
_uploadPostsRoute() {
TopNavigation.activate('upload');
this._emptyView.render();
}
} }
module.exports = new PostUploadController(); module.exports = router => {
router.enter('/upload', (ctx, next) => {
ctx.controller = new PostUploadController();
});
};

View file

@ -1,26 +1,27 @@
'use strict'; 'use strict';
const router = require('../router.js'); const settings = require('../models/settings.js');
const settings = require('../settings.js'); const topNavigation = require('../models/top_navigation.js');
const TopNavigation = require('../models/top_navigation.js');
const SettingsView = require('../views/settings_view.js'); const SettingsView = require('../views/settings_view.js');
class SettingsController { class SettingsController {
constructor() { constructor() {
this._settingsView = new SettingsView(); topNavigation.activate('settings');
} this._view = new SettingsView({
settings: settings.get(),
registerRoutes() {
router.enter('/settings', (ctx, next) => { this._settingsRoute(); });
}
_settingsRoute() {
TopNavigation.activate('settings');
this._settingsView.render({
getSettings: () => settings.getSettings(),
saveSettings: newSettings => settings.saveSettings(newSettings),
}); });
this._view.addEventListener('change', e => this._evtChange(e));
}
_evtChange(e) {
this._view.clearMessages();
settings.save(e.detail.settings);
this._view.showSuccess('Settings saved.');
} }
}; };
module.exports = new SettingsController(); module.exports = router => {
router.enter('/settings', (ctx, next) => {
ctx.controller = new SettingsController();
});
};

View file

@ -0,0 +1,80 @@
'use strict';
const api = require('../api.js');
const tags = require('../tags.js');
const misc = require('../util/misc.js');
const topNavigation = require('../models/top_navigation.js');
const TagCategoriesView = require('../views/tag_categories_view.js');
const EmptyView = require('../views/empty_view.js');
class TagCategoriesController {
constructor() {
topNavigation.activate('tags');
api.get('/tag-categories/').then(response => {
this._view = new TagCategoriesView({
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'),
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);
});
}
});
}, 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);
});
}
}
module.exports = router => {
router.enter('/tag-categories', (ctx, next) => {
ctx.controller = new TagCategoriesController(ctx, next);
});
};

View file

@ -0,0 +1,115 @@
'use strict';
const router = require('../router.js');
const api = require('../api.js');
const tags = require('../tags.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 => {
topNavigation.activate('tags');
const categories = {};
for (let category of tags.getAllCategories()) {
categories[category.name] = category.name;
}
this._view = new TagView({
tag: tag,
section: section,
canEditNames: api.hasPrivilege('tags:edit:names'),
canEditCategory: api.hasPrivilege('tags:edit:category'),
canEditImplications: api.hasPrivilege('tags:edit:implications'),
canEditSuggestions: api.hasPrivilege('tags:edit:suggestions'),
canMerge: api.hasPrivilege('tags:delete'),
canDelete: api.hasPrivilege('tags:merge'),
categories: categories,
});
this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('merge', e => this._evtMerge(e));
this._view.addEventListener('delete', e => this._evtDelete(e));
}, errorMessage => {
this._view = new EmptyView();
this._view.showError(errorMessage);
});
}
_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);
}
this._view.showSuccess('Tag saved.');
this._view.enableForm();
}, response => {
this._view.showError(response.description);
this._view.enableForm();
});
}
_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);
this._view.showSuccess('Tag merged.');
this._view.enableForm();
}, response => {
this._view.showError(response.description);
this._view.enableForm();
});
}
_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();
});
}
}
module.exports = router => {
router.enter('/tag/:name', (ctx, next) => {
ctx.controller = new TagController(ctx, 'summary');
});
router.enter('/tag/:name/merge', (ctx, next) => {
ctx.controller = new TagController(ctx, 'merge');
});
router.enter('/tag/:name/delete', (ctx, next) => {
ctx.controller = new TagController(ctx, 'delete');
});
};

View file

@ -0,0 +1,54 @@
'use strict';
const api = require('../api.js');
const misc = require('../util/misc.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');
class TagListController {
constructor(ctx) {
topNavigation.activate('tags');
this._pageController = new PageController({
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');
}),
headerRenderer: headerCtx => {
Object.assign(headerCtx, {
canEditTagCategories:
api.hasPrivilege('tagCategories:edit'),
});
return new TagsHeaderView(headerCtx);
},
pageRenderer: pageCtx => {
return new TagsPageView(pageCtx);
},
});
}
showSuccess(message) {
this._pageController.showSuccess(message);
}
showError(message) {
this._pageController.showError(message);
}
}
module.exports = router => {
router.enter(
'/tags/:query?',
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
(ctx, next) => { ctx.controller = new TagListController(ctx); });
};

View file

@ -1,228 +0,0 @@
'use strict';
const router = require('../router.js');
const api = require('../api.js');
const tags = require('../tags.js');
const events = require('../events.js');
const misc = require('../util/misc.js');
const pageController = require('../controllers/page_controller.js');
const TopNavigation = require('../models/top_navigation.js');
const TagView = require('../views/tag_view.js');
const TagsHeaderView = require('../views/tags_header_view.js');
const TagsPageView = require('../views/tags_page_view.js');
const TagCategoriesView = require('../views/tag_categories_view.js');
const EmptyView = require('../views/empty_view.js');
class TagsController {
constructor() {
this._tagView = new TagView();
this._tagsHeaderView = new TagsHeaderView();
this._tagsPageView = new TagsPageView();
this._tagCategoriesView = new TagCategoriesView();
this._emptyView = new EmptyView();
}
registerRoutes() {
router.enter(
'/tag-categories',
(ctx, next) => { this._tagCategoriesRoute(ctx, next); });
router.enter(
'/tag/:name',
(ctx, next) => { this._loadTagRoute(ctx, next); },
(ctx, next) => { this._showTagRoute(ctx, next); });
router.enter(
'/tag/:name/merge',
(ctx, next) => { this._loadTagRoute(ctx, next); },
(ctx, next) => { this._mergeTagRoute(ctx, next); });
router.enter(
'/tag/:name/delete',
(ctx, next) => { this._loadTagRoute(ctx, next); },
(ctx, next) => { this._deleteTagRoute(ctx, next); });
router.enter(
'/tags/:query?',
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
(ctx, next) => { this._listTagsRoute(ctx, next); });
}
_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(
() => {
events.notify(events.TagsChange);
events.notify(events.Success, 'Changes saved.');
},
response => {
events.notify(events.Error, response.description);
});
}
_loadTagRoute(ctx, next) {
if (ctx.state.tag) {
next();
} else if (this._cachedTag &&
this._cachedTag.names == ctx.params.names) {
ctx.state.tag = this._cachedTag;
next();
} else {
api.get('/tag/' + ctx.params.name).then(response => {
ctx.state.tag = response;
ctx.save();
this._cachedTag = response;
next();
}, response => {
this._emptyView.render();
events.notify(events.Error, response.description);
});
}
}
_showTagRoute(ctx, next) {
this._show(ctx.state.tag, 'summary');
}
_mergeTagRoute(ctx, next) {
this._show(ctx.state.tag, 'merge');
}
_deleteTagRoute(ctx, next) {
this._show(ctx.state.tag, 'delete');
}
_show(tag, section) {
TopNavigation.activate('tags');
const categories = {};
for (let category of tags.getAllCategories()) {
categories[category.name] = category.name;
}
this._tagView.render({
tag: tag,
section: section,
canEditNames: api.hasPrivilege('tags:edit:names'),
canEditCategory: api.hasPrivilege('tags:edit:category'),
canEditImplications: api.hasPrivilege('tags:edit:implications'),
canEditSuggestions: api.hasPrivilege('tags:edit:suggestions'),
canMerge: api.hasPrivilege('tags:delete'),
canDelete: api.hasPrivilege('tags:merge'),
categories: categories,
save: (...args) => { return this._saveTag(tag, ...args); },
mergeTo: (...args) => { return this._mergeTag(tag, ...args); },
delete: (...args) => { return this._deleteTag(tag, ...args); },
});
}
_saveTag(tag, input) {
return api.put('/tag/' + tag.names[0], input).then(response => {
if (input.names && input.names[0] !== tag.names[0]) {
router.show('/tag/' + input.names[0]);
}
events.notify(events.Success, 'Tag saved.');
return Promise.resolve();
}, response => {
events.notify(events.Error, response.description);
return Promise.reject();
});
}
_mergeTag(tag, targetTagName) {
return api.post(
'/tag-merge/',
{remove: tag.names[0], mergeTo: targetTagName}
).then(response => {
router.show('/tag/' + targetTagName + '/merge');
events.notify(events.Success, 'Tag merged.');
return Promise.resolve();
}, response => {
events.notify(events.Error, response.description);
return Promise.reject();
});
}
_deleteTag(tag) {
return api.delete('/tag/' + tag.names[0]).then(response => {
router.show('/tags/');
events.notify(events.Success, 'Tag deleted.');
return Promise.resolve();
}, response => {
events.notify(events.Error, response.description);
return Promise.reject();
});
}
_tagCategoriesRoute(ctx, next) {
TopNavigation.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'),
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);
});
}
});
}, response => {
this._emptyView.render();
events.notify(events.Error, response.description);
});
}
_listTagsRoute(ctx, next) {
TopNavigation.activate('tags');
pageController.run({
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');
}),
headerRenderer: this._tagsHeaderView,
pageRenderer: this._tagsPageView,
headerContext: {
canEditTagCategories: api.hasPrivilege('tagCategories:edit'),
},
});
}
}
module.exports = new TagsController();

View file

@ -1,70 +1,68 @@
'use strict'; 'use strict';
const api = require('../api.js'); const api = require('../api.js');
const events = require('../events.js'); const topNavigation = require('../models/top_navigation.js');
const TopNavigationView = require('../views/top_navigation_view.js'); const TopNavigationView = require('../views/top_navigation_view.js');
const TopNavigation = require('../models/top_navigation.js');
class TopNavigationController { class TopNavigationController {
constructor() { constructor() {
this._topNavigationView = new TopNavigationView(); this._topNavigationView = new TopNavigationView();
TopNavigation.addEventListener( topNavigation.addEventListener(
'activate', e => this._evtActivate(e)); 'activate', e => this._evtActivate(e));
events.listen( api.addEventListener('login', e => this._evtAuthChange(e));
events.Authentication, api.addEventListener('logout', e => this._evtAuthChange(e));
() => {
this._render();
return true;
});
this._render(); this._render();
} }
_evtAuthChange(e) {
this._render();
}
_evtActivate(e) { _evtActivate(e) {
this._topNavigationView.activate(e.key); this._topNavigationView.activate(e.key);
} }
_updateNavigationFromPrivileges() { _updateNavigationFromPrivileges() {
TopNavigation.get('account').url = '/user/' + api.userName; topNavigation.get('account').url = '/user/' + api.userName;
TopNavigation.get('account').imageUrl = topNavigation.get('account').imageUrl =
api.user ? api.user.avatarUrl : null; api.user ? api.user.avatarUrl : null;
TopNavigation.showAll(); topNavigation.showAll();
if (!api.hasPrivilege('posts:list')) { if (!api.hasPrivilege('posts:list')) {
TopNavigation.hide('posts'); topNavigation.hide('posts');
} }
if (!api.hasPrivilege('posts:create')) { if (!api.hasPrivilege('posts:create')) {
TopNavigation.hide('upload'); topNavigation.hide('upload');
} }
if (!api.hasPrivilege('comments:list')) { if (!api.hasPrivilege('comments:list')) {
TopNavigation.hide('comments'); topNavigation.hide('comments');
} }
if (!api.hasPrivilege('tags:list')) { if (!api.hasPrivilege('tags:list')) {
TopNavigation.hide('tags'); topNavigation.hide('tags');
} }
if (!api.hasPrivilege('users:list')) { if (!api.hasPrivilege('users:list')) {
TopNavigation.hide('users'); topNavigation.hide('users');
} }
if (api.isLoggedIn()) { if (api.isLoggedIn()) {
TopNavigation.hide('register'); topNavigation.hide('register');
TopNavigation.hide('login'); topNavigation.hide('login');
} else { } else {
TopNavigation.hide('account'); topNavigation.hide('account');
TopNavigation.hide('logout'); topNavigation.hide('logout');
} }
} }
_render() { _render() {
this._updateNavigationFromPrivileges(); this._updateNavigationFromPrivileges();
console.log(TopNavigation.getAll());
this._topNavigationView.render({ this._topNavigationView.render({
items: TopNavigation.getAll(), items: topNavigation.getAll(),
}); });
this._topNavigationView.activate( this._topNavigationView.activate(
TopNavigation.activeItem ? TopNavigation.activeItem.key : ''); topNavigation.activeItem ? topNavigation.activeItem.key : '');
}; }
} }
module.exports = new TopNavigationController(); module.exports = new TopNavigationController();

View file

@ -0,0 +1,166 @@
'use strict';
const router = require('../router.js');
const api = require('../api.js');
const config = require('../config.js');
const views = require('../util/views.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 => {
const isLoggedIn = api.isLoggedIn(user);
const infix = isLoggedIn ? 'self' : 'any';
const myRankIndex = api.user ?
api.allRanks.indexOf(api.user.rank) :
0;
let ranks = {};
for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) {
if (rankIdentifier === 'anonymous') {
continue;
}
if (rankIdx > myRankIndex) {
continue;
}
ranks[rankIdentifier] = rankNames.get(rankIdentifier);
}
if (isLoggedIn) {
topNavigation.activate('account');
} else {
topNavigation.activate('users');
}
this._view = new UserView({
user: user,
section: section,
isLoggedIn: isLoggedIn,
canEditName: api.hasPrivilege(`users:edit:${infix}:name`),
canEditPassword: api.hasPrivilege(`users:edit:${infix}:pass`),
canEditEmail: api.hasPrivilege(`users:edit:${infix}:email`),
canEditRank: api.hasPrivilege(`users:edit:${infix}:rank`),
canEditAvatar: api.hasPrivilege(`users:edit:${infix}:avatar`),
canEditAnything: api.hasPrivilege(`users:edit:${infix}`),
canDelete: api.hasPrivilege(`users:delete:${infix}`),
ranks: ranks,
});
this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('delete', e => this._evtDelete(e));
}, errorMessage => {
this._view = new EmptyView();
this._view.showError(errorMessage);
});
}
_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.password) {
data.password = e.detail.password;
}
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;
}
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();
});
}
_evtDelete(e) {
this._view.clearMessages();
this._view.disableForm();
const isLoggedIn = api.isLoggedIn(e.detail.user);
api.delete('/user/' + e.detail.user.name)
.then(response => {
if (isLoggedIn) {
api.forget();
api.logout();
}
if (api.hasPrivilege('users:list')) {
const ctx = router.show('/users');
ctx.controller.showSuccess('Account deleted.');
} else {
const ctx = router.show('/');
ctx.controller.showSuccess('Account deleted.');
}
}, response => {
this._view.showError(response.description);
this._view.enableForm();
});
}
}
module.exports = router => {
router.enter('/user/:name', (ctx, next) => {
ctx.controller = new UserController(ctx, 'summary');
});
router.enter('/user/:name/edit', (ctx, next) => {
ctx.controller = new UserController(ctx, 'edit');
});
router.enter('/user/:name/delete', (ctx, next) => {
ctx.controller = new UserController(ctx, 'delete');
});
};

View file

@ -0,0 +1,47 @@
'use strict';
const api = require('../api.js');
const misc = require('../util/misc.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const UsersHeaderView = require('../views/users_header_view.js');
const UsersPageView = require('../views/users_page_view.js');
class UserListController {
constructor(ctx) {
topNavigation.activate('users');
this._pageController = new PageController({
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`);
}),
headerRenderer: headerCtx => {
return new UsersHeaderView(headerCtx);
},
pageRenderer: pageCtx => {
Object.assign(pageCtx, {
canViewUsers: api.hasPrivilege('users:view'),
});
return new UsersPageView(pageCtx);
},
});
}
showSuccess(message) {
this._pageController.showSuccess(message);
}
}
module.exports = router => {
router.enter(
'/users/:query?',
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
(ctx, next) => { ctx.controller = new UserListController(ctx); });
};

View file

@ -0,0 +1,41 @@
'use strict';
const router = require('../router.js');
const api = require('../api.js');
const topNavigation = require('../models/top_navigation.js');
const RegistrationView = require('../views/registration_view.js');
class UserRegistrationController {
constructor() {
topNavigation.activate('register');
this._view = new RegistrationView();
this._view.addEventListener('submit', e => this._evtRegister(e));
}
_evtRegister(e) {
this._view.clearMessages();
this._view.disableForm();
api.post('/users/', {
name: e.detail.name,
password: e.detail.password,
email: e.detail.email
}).then(() => {
api.forget();
return api.login(e.detail.name, e.detail.password, false);
}, response => {
return Promise.reject(response.description);
}).then(() => {
const ctx = router.show('/');
ctx.controller.showSuccess('Welcome aboard!');
}, errorMessage => {
this._view.showError(errorMessage);
this._view.enableForm();
});
}
}
module.exports = router => {
router.enter('/register', (ctx, next) => {
new UserRegistrationController();
});
};

View file

@ -1,262 +0,0 @@
'use strict';
const router = require('../router.js');
const api = require('../api.js');
const config = require('../config.js');
const events = require('../events.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
const pageController = require('../controllers/page_controller.js');
const TopNavigation = require('../models/top_navigation.js');
const RegistrationView = require('../views/registration_view.js');
const UserView = require('../views/user_view.js');
const UsersHeaderView = require('../views/users_header_view.js');
const UsersPageView = require('../views/users_page_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 UsersController {
constructor() {
this._registrationView = new RegistrationView();
this._userView = new UserView();
this._usersHeaderView = new UsersHeaderView();
this._usersPageView = new UsersPageView();
this._emptyView = new EmptyView();
}
registerRoutes() {
router.enter(
'/register',
(ctx, next) => { this._createUserRoute(ctx, next); });
router.enter(
'/users/:query?',
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
(ctx, next) => { this._listUsersRoute(ctx, next); });
router.enter(
'/user/:name',
(ctx, next) => { this._loadUserRoute(ctx, next); },
(ctx, next) => { this._showUserRoute(ctx, next); });
router.enter(
'/user/:name/edit',
(ctx, next) => { this._loadUserRoute(ctx, next); },
(ctx, next) => { this._editUserRoute(ctx, next); });
router.enter(
'/user/:name/delete',
(ctx, next) => { this._loadUserRoute(ctx, next); },
(ctx, next) => { this._deleteUserRoute(ctx, next); });
router.exit(
/\/users\/.*/, (ctx, next) => {
pageController.stop();
next();
});
router.exit(/\/user\/.*/, (ctx, next) => {
this._cachedUser = null;
next();
});
}
_listUsersRoute(ctx, next) {
TopNavigation.activate('users');
pageController.run({
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`);
}),
headerRenderer: this._usersHeaderView,
pageRenderer: this._usersPageView,
pageContext: {
canViewUsers: api.hasPrivilege('users:view'),
},
});
}
_createUserRoute(ctx, next) {
TopNavigation.activate('register');
this._registrationView.render({
register: (...args) => {
return this._register(...args);
}});
}
_loadUserRoute(ctx, next) {
if (ctx.state.user) {
next();
} else if (this._cachedUser && this._cachedUser == ctx.params.name) {
ctx.state.user = this._cachedUser;
next();
} else {
api.get('/user/' + ctx.params.name).then(response => {
response.rankName = rankNames.get(response.rank);
ctx.state.user = response;
ctx.save();
this._cachedUser = response;
next();
}, response => {
this._emptyView.render();
events.notify(events.Error, response.description);
});
}
}
_showUserRoute(ctx, next) {
this._show(ctx.state.user, 'summary');
}
_editUserRoute(ctx, next) {
this._show(ctx.state.user, 'edit');
}
_deleteUserRoute(ctx, next) {
this._show(ctx.state.user, 'delete');
}
_register(name, password, email) {
const data = {
name: name,
password: password,
email: email
};
return new Promise((resolve, reject) => {
api.post('/users/', data).then(() => {
api.forget();
return api.login(name, password, false);
}, response => {
return Promise.reject(response.description);
}).then(() => {
resolve();
router.show('/');
events.notify(events.Success, 'Welcome aboard!');
}, errorMessage => {
reject();
events.notify(events.Error, errorMessage);
});
});
}
_edit(user, data) {
const isLoggedIn = api.isLoggedIn(user);
const infix = isLoggedIn ? 'self' : 'any';
let files = [];
if (!data.name) {
delete data.name;
}
if (!data.password) {
delete data.password;
}
if (!api.hasPrivilege('users:edit:' + infix + ':email')) {
delete data.email;
}
if (!data.rank) {
delete data.rank;
}
if (!data.avatarStyle ||
(data.avatarStyle == user.avatarStyle && !data.avatarContent)) {
delete data.avatarStyle;
}
if (data.avatarContent) {
files.avatar = data.avatarContent;
}
return new Promise((resolve, reject) => {
api.put('/user/' + user.name, data, files)
.then(response => {
this._cachedUser = response;
return isLoggedIn ?
api.login(
data.name || api.userName,
data.password || api.userPassword,
false) :
Promise.resolve();
}, response => {
return Promise.reject(response.description);
}).then(() => {
resolve();
if (data.name && data.name !== user.name) {
router.show('/user/' + data.name + '/edit');
}
events.notify(events.Success, 'Settings updated.');
}, errorMessage => {
reject();
events.notify(events.Error, errorMessage);
});
});
}
_delete(user) {
const isLoggedIn = api.isLoggedIn(user);
return api.delete('/user/' + user.name)
.then(response => {
if (isLoggedIn) {
api.forget();
api.logout();
}
if (api.hasPrivilege('users:list')) {
router.show('/users');
} else {
router.show('/');
}
events.notify(events.Success, 'Account deleted.');
return Promise.resolve();
}, response => {
events.notify(events.Error, response.description);
return Promise.reject();
});
}
_show(user, section) {
const isLoggedIn = api.isLoggedIn(user);
const infix = isLoggedIn ? 'self' : 'any';
const myRankIdx = api.user ? api.allRanks.indexOf(api.user.rank) : 0;
let ranks = {};
for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) {
if (rankIdentifier === 'anonymous') {
continue;
}
if (rankIdx > myRankIdx) {
continue;
}
ranks[rankIdentifier] = rankNames.get(rankIdentifier);
}
if (isLoggedIn) {
TopNavigation.activate('account');
} else {
TopNavigation.activate('users');
}
this._userView.render({
user: user,
section: section,
isLoggedIn: isLoggedIn,
canEditName: api.hasPrivilege('users:edit:' + infix + ':name'),
canEditPassword: api.hasPrivilege('users:edit:' + infix + ':pass'),
canEditEmail: api.hasPrivilege('users:edit:' + infix + ':email'),
canEditRank: api.hasPrivilege('users:edit:' + infix + ':rank'),
canEditAvatar: api.hasPrivilege('users:edit:' + infix + ':avatar'),
canEditAnything: api.hasPrivilege('users:edit:' + infix),
canDelete: api.hasPrivilege('users:delete:' + infix),
ranks: ranks,
edit: (...args) => { return this._edit(user, ...args); },
delete: (...args) => { return this._delete(user, ...args); },
});
}
}
module.exports = new UsersController();

View file

@ -25,7 +25,7 @@ class CommentControl {
canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`), canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`),
}); });
views.showView( views.replaceContent(
sourceNode.querySelector('.score-container'), sourceNode.querySelector('.score-container'),
this._scoreTemplate({ this._scoreTemplate({
score: this._comment.score, score: this._comment.score,
@ -77,7 +77,7 @@ class CommentControl {
canCancel: true canCancel: true
}); });
views.showView(this._hostNode, sourceNode); views.replaceContent(this._hostNode, sourceNode);
} }
_evtScoreClick(e, scoreGetter) { _evtScoreClick(e, scoreGetter) {

View file

@ -47,7 +47,7 @@ class CommentFormControl {
this._growTextArea(); this._growTextArea();
}); });
views.showView(this._hostNode, sourceNode); views.replaceContent(this._hostNode, sourceNode);
} }
enterEditMode() { enterEditMode() {

View file

@ -19,7 +19,7 @@ class CommentListControl {
canListComments: api.hasPrivilege('comments:list'), canListComments: api.hasPrivilege('comments:list'),
}); });
views.showView(this._hostNode, sourceNode); views.replaceContent(this._hostNode, sourceNode);
this._renderComments(); this._renderComments();
} }
@ -42,7 +42,7 @@ class CommentListControl {
}); });
commentList.appendChild(commentListItemNode); commentList.appendChild(commentListItemNode);
} }
views.showView(this._hostNode.querySelector('ul'), commentList); views.replaceContent(this._hostNode.querySelector('ul'), commentList);
} }
}; };

View file

@ -28,7 +28,7 @@ class FileDropperControl {
this._fileInputNode.addEventListener( this._fileInputNode.addEventListener(
'change', e => this._evtFileChange(e)); 'change', e => this._evtFileChange(e));
views.showView(target, source); views.replaceContent(target, source);
} }
_resolve(files) { _resolve(files) {

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const settings = require('../settings.js'); const settings = require('../models/settings.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const optimizedResize = require('../util/optimized_resize.js'); const optimizedResize = require('../util/optimized_resize.js');
@ -21,7 +21,7 @@ class PostContentControl {
this._currentFitFunction = this.fitWidth; this._currentFitFunction = this.fitWidth;
const mul = this._post.canvasHeight / this._post.canvasWidth; const mul = this._post.canvasHeight / this._post.canvasWidth;
let width = this._viewportWidth; let width = this._viewportWidth;
if (!settings.getSettings().upscaleSmallPosts) { if (!settings.get().upscaleSmallPosts) {
width = Math.min(this._post.canvasWidth, width); width = Math.min(this._post.canvasWidth, width);
} }
this._resize(width, width * mul); this._resize(width, width * mul);
@ -31,7 +31,7 @@ class PostContentControl {
this._currentFitFunction = this.fitHeight; this._currentFitFunction = this.fitHeight;
const mul = this._post.canvasWidth / this._post.canvasHeight; const mul = this._post.canvasWidth / this._post.canvasHeight;
let height = this._viewportHeight; let height = this._viewportHeight;
if (!settings.getSettings().upscaleSmallPosts) { if (!settings.get().upscaleSmallPosts) {
height = Math.min(this._post.canvasHeight, height); height = Math.min(this._post.canvasHeight, height);
} }
this._resize(height * mul, height); this._resize(height * mul, height);
@ -42,13 +42,13 @@ class PostContentControl {
let mul = this._post.canvasHeight / this._post.canvasWidth; let mul = this._post.canvasHeight / this._post.canvasWidth;
if (this._viewportWidth * mul < this._viewportHeight) { if (this._viewportWidth * mul < this._viewportHeight) {
let width = this._viewportWidth; let width = this._viewportWidth;
if (!settings.getSettings().upscaleSmallPosts) { if (!settings.get().upscaleSmallPosts) {
width = Math.min(this._post.canvasWidth, width); width = Math.min(this._post.canvasWidth, width);
} }
this._resize(width, width * mul); this._resize(width, width * mul);
} else { } else {
let height = this._viewportHeight; let height = this._viewportHeight;
if (!settings.getSettings().upscaleSmallPosts) { if (!settings.get().upscaleSmallPosts) {
height = Math.min(this._post.canvasHeight, height); height = Math.min(this._post.canvasHeight, height);
} }
this._resize(height / mul, height); this._resize(height / mul, height);
@ -83,7 +83,7 @@ class PostContentControl {
const postContentNode = this._template({ const postContentNode = this._template({
post: this._post, post: this._post,
}); });
if (settings.getSettings().transparencyGrid) { if (settings.get().transparencyGrid) {
postContentNode.classList.add('transparency-grid'); postContentNode.classList.add('transparency-grid');
} }
this._containerNode.appendChild(postContentNode); this._containerNode.appendChild(postContentNode);

View file

@ -16,7 +16,7 @@ class PostEditSidebarControl {
const sourceNode = this._template({ const sourceNode = this._template({
post: this._post, post: this._post,
}); });
views.showView(this._hostNode, sourceNode); views.replaceContent(this._hostNode, sourceNode);
} }
}; };

View file

@ -25,7 +25,7 @@ class PostReadonlySidebarControl {
canViewTags: api.hasPrivilege('tags:view'), canViewTags: api.hasPrivilege('tags:view'),
}); });
views.showView( views.replaceContent(
sourceNode.querySelector('.score-container'), sourceNode.querySelector('.score-container'),
this._scoreTemplate({ this._scoreTemplate({
score: this._post.score, score: this._post.score,
@ -33,7 +33,7 @@ class PostReadonlySidebarControl {
canScore: api.hasPrivilege('posts:score'), canScore: api.hasPrivilege('posts:score'),
})); }));
views.showView( views.replaceContent(
sourceNode.querySelector('.fav-container'), sourceNode.querySelector('.fav-container'),
this._favTemplate({ this._favTemplate({
favoriteCount: this._post.favoriteCount, favoriteCount: this._post.favoriteCount,
@ -85,7 +85,7 @@ class PostReadonlySidebarControl {
'click', this._eventZoomProxy( 'click', this._eventZoomProxy(
() => this._postContentControl.fitHeight())); () => this._postContentControl.fitHeight()));
views.showView(this._hostNode, sourceNode); views.replaceContent(this._hostNode, sourceNode);
this._syncFitButton(); this._syncFitButton();
} }

View file

@ -1,41 +1,5 @@
'use strict'; 'use strict';
let pendingMessages = new Map();
let listeners = new Map();
function unlisten(messageClass) {
listeners.set(messageClass, []);
}
function listen(messageClass, handler) {
if (pendingMessages.has(messageClass)) {
let newPendingMessages = [];
for (let message of pendingMessages.get(messageClass)) {
if (!handler(message)) {
newPendingMessages.push(message);
}
}
pendingMessages.set(messageClass, newPendingMessages);
}
if (!listeners.has(messageClass)) {
listeners.set(messageClass, []);
}
listeners.get(messageClass).push(handler);
}
function notify(messageClass, message) {
if (!listeners.has(messageClass) || !listeners.get(messageClass).length) {
if (!pendingMessages.has(messageClass)) {
pendingMessages.set(messageClass, []);
}
pendingMessages.get(messageClass).push(message);
return;
}
for (let handler of listeners.get(messageClass)) {
handler(message);
}
}
class EventTarget { class EventTarget {
constructor() { constructor() {
this.eventTarget = document.createDocumentFragment(); this.eventTarget = document.createDocumentFragment();
@ -53,12 +17,6 @@ module.exports = {
Success: 'success', Success: 'success',
Error: 'error', Error: 'error',
Info: 'info', Info: 'info',
Authentication: 'auth',
SettingsChange: 'settings-change',
TagsChange: 'tags-change',
notify: notify,
listen: listen,
unlisten: unlisten,
EventTarget: EventTarget, EventTarget: EventTarget,
}; };

View file

@ -2,7 +2,7 @@
require('./util/polyfill.js'); require('./util/polyfill.js');
const misc = require('./util/misc.js'); const misc = require('./util/misc.js');
const views = require('./util/views.js');
const router = require('./router.js'); const router = require('./router.js');
history.scrollRestoration = 'manual'; history.scrollRestoration = 'manual';
@ -13,7 +13,6 @@ router.exit(
ctx.state.scrollX = window.scrollX; ctx.state.scrollX = window.scrollX;
ctx.state.scrollY = window.scrollY; ctx.state.scrollY = window.scrollY;
ctx.save(); ctx.save();
views.unlistenToMessages();
if (misc.confirmPageExit()) { if (misc.confirmPageExit()) {
next(); next();
} }
@ -33,28 +32,33 @@ router.enter(
}); });
}); });
// register controller routes
let controllers = []; let controllers = [];
controllers.push(require('./controllers/auth_controller.js')); controllers.push(require('./controllers/home_controller.js'));
controllers.push(require('./controllers/post_list_controller.js'));
controllers.push(require('./controllers/post_upload_controller.js'));
controllers.push(require('./controllers/post_controller.js'));
controllers.push(require('./controllers/users_controller.js'));
controllers.push(require('./controllers/help_controller.js')); controllers.push(require('./controllers/help_controller.js'));
controllers.push(require('./controllers/auth_controller.js'));
controllers.push(require('./controllers/password_reset_controller.js'));
controllers.push(require('./controllers/comments_controller.js')); controllers.push(require('./controllers/comments_controller.js'));
controllers.push(require('./controllers/history_controller.js')); controllers.push(require('./controllers/history_controller.js'));
controllers.push(require('./controllers/tags_controller.js')); controllers.push(require('./controllers/post_controller.js'));
controllers.push(require('./controllers/post_list_controller.js'));
controllers.push(require('./controllers/post_upload_controller.js'));
controllers.push(require('./controllers/tag_controller.js'));
controllers.push(require('./controllers/tag_list_controller.js'));
controllers.push(require('./controllers/tag_categories_controller.js'));
controllers.push(require('./controllers/settings_controller.js')); controllers.push(require('./controllers/settings_controller.js'));
controllers.push(require('./controllers/user_controller.js'));
controllers.push(require('./controllers/user_list_controller.js'));
controllers.push(require('./controllers/user_registration_controller.js'));
// home defines 404 routes, need to be registered as last // 404 controller needs to be registered last
controllers.push(require('./controllers/home_controller.js')); controllers.push(require('./controllers/not_found_controller.js'));
const tags = require('./tags.js');
const events = require('./events.js');
const views = require('./util/views.js');
for (let controller of controllers) { for (let controller of controllers) {
controller.registerRoutes(); controller(router);
} }
const tags = require('./tags.js');
const api = require('./api.js'); const api = require('./api.js');
Promise.all([tags.refreshExport(), api.loginFromCookies()]) Promise.all([tags.refreshExport(), api.loginFromCookies()])
.then(() => { .then(() => {
@ -64,9 +68,8 @@ Promise.all([tags.refreshExport(), api.loginFromCookies()])
api.forget(); api.forget();
router.start(); router.start();
} else { } else {
router.start('/'); const ctx = router.start('/');
events.notify( ctx.controller.showError(
events.Error,
'An error happened while trying to log you in: ' + 'An error happened while trying to log you in: ' +
errorMessage); errorMessage);
} }

View file

@ -0,0 +1,40 @@
'use strict';
const events = require('../events.js');
const defaultSettings = {
listPosts: {
safe: true,
sketchy: true,
unsafe: false,
},
upscaleSmallPosts: false,
endlessScroll: false,
keyboardShortcuts: true,
transparencyGrid: true,
};
class Settings extends events.EventTarget {
save(newSettings, silent) {
localStorage.setItem('settings', JSON.stringify(newSettings));
if (silent !== true) {
this.dispatchEvent(new CustomEvent('change', {
detail: {
settings: this.get(),
},
}));
}
}
get() {
let ret = {};
Object.assign(ret, defaultSettings);
try {
Object.assign(ret, JSON.parse(localStorage.getItem('settings')));
} catch (e) {
}
return ret;
}
};
module.exports = new Settings();

View file

@ -42,15 +42,13 @@ class TopNavigation extends events.EventTarget {
} }
activate(key) { activate(key) {
const event = new Event('activate');
event.key = key;
if (key) {
event.item = this.get(key);
} else {
event.item = null;
}
this.activeItem = null; this.activeItem = null;
this.dispatchEvent(event); this.dispatchEvent(new CustomEvent('activate', {
detail: {
key: key,
item: key ? this.get(key) : null,
},
}));
} }
showAll() { showAll() {

View file

@ -120,7 +120,7 @@ class Router {
window.addEventListener('popstate', this._onPopState, false); window.addEventListener('popstate', this._onPopState, false);
document.addEventListener(clickEvent, this._onClick, false); document.addEventListener(clickEvent, this._onClick, false);
const url = location.pathname + location.search + location.hash; const url = location.pathname + location.search + location.hash;
this.replace(url, null, true); return this.replace(url, null, true);
} }
stop() { stop() {

View file

@ -1,46 +0,0 @@
'use strict';
const events = require('./events.js');
function saveSettings(browsingSettings, silent) {
localStorage.setItem('settings', JSON.stringify(browsingSettings));
if (silent !== true) {
events.notify(events.Success, 'Settings saved');
events.notify(events.SettingsChange);
}
}
function getSettings(settings) {
const defaultSettings = {
listPosts: {
safe: true,
sketchy: true,
unsafe: false,
},
upscaleSmallPosts: false,
endlessScroll: false,
keyboardShortcuts: true,
transparencyGrid: true,
};
let ret = {};
let userSettings = localStorage.getItem('settings');
if (userSettings) {
userSettings = JSON.parse(userSettings);
}
if (!userSettings) {
userSettings = {};
}
for (let key of Object.keys(defaultSettings)) {
if (key in userSettings) {
ret[key] = userSettings[key];
} else {
ret[key] = defaultSettings[key];
}
}
return ret;
}
module.exports = {
getSettings: getSettings,
saveSettings: saveSettings,
};

View file

@ -1,7 +1,6 @@
'use strict'; 'use strict';
const request = require('superagent'); const request = require('superagent');
const events = require('./events.js');
let _tags = null; let _tags = null;
let _categories = null; let _categories = null;
@ -88,10 +87,6 @@ function refreshExport() {
}); });
} }
events.listen(
events.TagsChange,
() => { refreshExport(); return true; });
module.exports = { module.exports = {
getAllCategories: getAllCategories, getAllCategories: getAllCategories,
getAllTags: getAllTags, getAllTags: getAllTags,

View file

@ -1,10 +1,10 @@
'use strict'; 'use strict';
const mousetrap = require('mousetrap'); const mousetrap = require('mousetrap');
const settings = require('../settings.js'); const settings = require('../models/settings.js');
function bind(hotkey, func) { function bind(hotkey, func) {
if (settings.getSettings().keyboardShortcuts) { if (settings.get().keyboardShortcuts) {
mousetrap.bind(hotkey, func); mousetrap.bind(hotkey, func);
return true; return true;
} }

View file

@ -4,7 +4,6 @@ require('../util/polyfill.js');
const api = require('../api.js'); const api = require('../api.js');
const templates = require('../templates.js'); const templates = require('../templates.js');
const tags = require('../tags.js'); const tags = require('../tags.js');
const events = require('../events.js');
const domParser = new DOMParser(); const domParser = new DOMParser();
const misc = require('./misc.js'); const misc = require('./misc.js');
@ -238,26 +237,6 @@ function showInfo(target, message) {
return showMessage(target, message, 'info'); return showMessage(target, message, 'info');
} }
function unlistenToMessages() {
events.unlisten(events.Success);
events.unlisten(events.Error);
events.unlisten(events.Info);
}
function listenToMessages(target) {
unlistenToMessages();
const listen = (eventType, className) => {
events.listen(
eventType,
msg => {
return showMessage(target, msg, className);
});
};
listen(events.Success, 'success');
listen(events.Error, 'error');
listen(events.Info, 'info');
}
function clearMessages(target) { function clearMessages(target) {
const messagesHolder = target.querySelector('.messages'); const messagesHolder = target.querySelector('.messages');
/* TODO: animate that */ /* TODO: animate that */
@ -335,7 +314,7 @@ function enableForm(form) {
} }
} }
function showView(target, source) { function replaceContent(target, source) {
while (target.lastChild) { while (target.lastChild) {
target.removeChild(target.lastChild); target.removeChild(target.lastChild);
} }
@ -424,11 +403,9 @@ document.addEventListener('input', e => {
module.exports = { module.exports = {
htmlToDom: htmlToDom, htmlToDom: htmlToDom,
getTemplate: getTemplate, getTemplate: getTemplate,
showView: showView, replaceContent: replaceContent,
enableForm: enableForm, enableForm: enableForm,
disableForm: disableForm, disableForm: disableForm,
listenToMessages: listenToMessages,
unlistenToMessages: unlistenToMessages,
clearMessages: clearMessages, clearMessages: clearMessages,
decorateValidator: decorateValidator, decorateValidator: decorateValidator,
makeVoidElement: makeVoidElement, makeVoidElement: makeVoidElement,

View file

@ -3,24 +3,25 @@
const views = require('../util/views.js'); const views = require('../util/views.js');
const CommentListControl = require('../controls/comment_list_control.js'); const CommentListControl = require('../controls/comment_list_control.js');
class CommentsPageView { const template = views.getTemplate('comments-page');
constructor() {
this._template = views.getTemplate('comments-page');
}
render(ctx) { class CommentsPageView {
const target = ctx.target; constructor(ctx) {
const source = this._template(ctx); this._hostNode = ctx.hostNode;
this._controls = [];
const sourceNode = template(ctx);
for (let post of ctx.results) { for (let post of ctx.results) {
post.comments.sort((a, b) => { return b.id - a.id; }); post.comments.sort((a, b) => { return b.id - a.id; });
new CommentListControl( this._controls.push(
source.querySelector( new CommentListControl(
`.comments-container[data-for="${post.id}"]`), sourceNode.querySelector(
post.comments); `.comments-container[data-for="${post.id}"]`),
post.comments));
} }
views.showView(target, source); views.replaceContent(this._hostNode, sourceNode);
} }
} }

View file

@ -2,19 +2,19 @@
const views = require('../util/views.js'); const views = require('../util/views.js');
const template = () => {
return views.htmlToDom(
'<div class="wrapper"><div class="messages"></div></div>');
};
class EmptyView { class EmptyView {
constructor() { constructor() {
this._template = () => { this._hostNode = document.getElementById('content-holder');
return views.htmlToDom( views.replaceContent(this._hostNode, template());
'<div class="wrapper"><div class="messages"></div></div>');
};
} }
render(ctx) { showError(message) {
const target = document.getElementById('content-holder'); views.showError(this._hostNode, message);
const source = this._template();
views.listenToMessages(source);
views.showView(target, source);
} }
} }

View file

@ -1,55 +1,43 @@
'use strict'; 'use strict';
const router = require('../router.js'); const router = require('../router.js');
const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const holderTemplate = views.getTemplate('endless-pager');
const pageTemplate = views.getTemplate('endless-pager-page');
function _formatUrl(url, page) { function _formatUrl(url, page) {
return url.replace('{page}', page); return url.replace('{page}', page);
} }
class EndlessPageView { class EndlessPageView {
constructor() { constructor(ctx) {
this._holderTemplate = views.getTemplate('endless-pager'); this._hostNode = document.getElementById('content-holder');
this._pageTemplate = views.getTemplate('endless-pager-page');
}
render(ctx) {
const target = document.getElementById('content-holder');
const source = this._holderTemplate();
const pageHeaderHolder = source.querySelector('.page-header-holder');
this._pagesHolder = source.querySelector('.pages-holder');
views.listenToMessages(source);
views.showView(target, source);
this._active = true; this._active = true;
this._working = 0; this._working = 0;
this._init = true; this._init = true;
ctx.headerContext.target = pageHeaderHolder;
if (ctx.headerRenderer) {
ctx.headerRenderer.render(ctx.headerContext);
}
this.threshold = window.innerHeight / 3; this.threshold = window.innerHeight / 3;
this.minPageShown = null; this.minPageShown = null;
this.maxPageShown = null; this.maxPageShown = null;
this.totalPages = null; this.totalPages = null;
this.currentPage = null; this.currentPage = null;
const sourceNode = holderTemplate();
const pageHeaderHolderNode
= sourceNode.querySelector('.page-header-holder');
this._pagesHolderNode = sourceNode.querySelector('.pages-holder');
views.replaceContent(this._hostNode, sourceNode);
ctx.headerContext.hostNode = pageHeaderHolderNode;
if (ctx.headerRenderer) {
ctx.headerRenderer(ctx.headerContext);
}
this._loadPage(ctx, ctx.searchQuery.page, true); this._loadPage(ctx, ctx.searchQuery.page, true);
window.addEventListener('unload', this._scrollToTop, true);
this._probePageLoad(ctx); this._probePageLoad(ctx);
} }
unrender() {
this._active = false;
window.removeEventListener('unload', this._scrollToTop, true);
}
_scrollToTop() {
window.scroll(0, 0);
}
_probePageLoad(ctx) { _probePageLoad(ctx) {
if (this._active) { if (this._active) {
window.setTimeout(() => { window.setTimeout(() => {
@ -115,23 +103,23 @@ class EndlessPageView {
this._working--; this._working--;
}); });
}, response => { }, response => {
events.notify(events.Error, response.description); this.showError(response.description);
this._working--; this._working--;
}); });
} }
_renderPage(ctx, pageNumber, append, response) { _renderPage(ctx, pageNumber, append, response) {
if (response.total) { if (response.total) {
const pageNode = this._pageTemplate({ const pageNode = pageTemplate({
page: pageNumber, page: pageNumber,
totalPages: this.totalPages, totalPages: this.totalPages,
}); });
pageNode.setAttribute('data-page', pageNumber); pageNode.setAttribute('data-page', pageNumber);
Object.assign(ctx.pageContext, response); Object.assign(ctx.pageContext, response);
ctx.pageContext.target = pageNode.querySelector( ctx.pageContext.hostNode = pageNode.querySelector(
'.page-content-holder'); '.page-content-holder');
ctx.pageRenderer.render(ctx.pageContext); ctx.pageRenderer(ctx.pageContext);
if (pageNumber < this.minPageShown || if (pageNumber < this.minPageShown ||
this.minPageShown === null) { this.minPageShown === null) {
@ -143,22 +131,34 @@ class EndlessPageView {
} }
if (append) { if (append) {
this._pagesHolder.appendChild(pageNode); this._pagesHolderNode.appendChild(pageNode);
/*if (this._init && pageNumber !== 1) { if (this._init && pageNumber !== 1) {
window.scroll(0, pageNode.getBoundingClientRect().top); window.scroll(0, pageNode.getBoundingClientRect().top);
}*/ }
} else { } else {
this._pagesHolder.prependChild(pageNode); this._pagesHolderNode.prependChild(pageNode);
window.scroll( window.scroll(
window.scrollX, window.scrollX,
window.scrollY + pageNode.offsetHeight); window.scrollY + pageNode.offsetHeight);
} }
} else if (response.total <= (pageNumber - 1) * response.pageSize) { } else if (response.total <= (pageNumber - 1) * response.pageSize) {
events.notify(events.Info, 'No data to show'); this.showInfo('No data to show');
} }
this._init = false; this._init = false;
} }
showSuccess(message) {
views.showSuccess(this._hostNode, message);
}
showError(message) {
views.showError(this._hostNode, message);
}
showInfo(message) {
views.showInfo(this._hostNode, message);
}
} }
module.exports = EndlessPageView; module.exports = EndlessPageView;

View file

@ -3,67 +3,62 @@
const config = require('../config.js'); const config = require('../config.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const template = views.getTemplate('help');
const sectionTemplates = {
'about': views.getTemplate('help-about'),
'keyboard': views.getTemplate('help-keyboard'),
'search': views.getTemplate('help-search'),
'comments': views.getTemplate('help-comments'),
'tos': views.getTemplate('help-tos'),
};
const subsectionTemplates = {
'search': {
'default': views.getTemplate('help-search-general'),
'posts': views.getTemplate('help-search-posts'),
'users': views.getTemplate('help-search-users'),
'tags': views.getTemplate('help-search-tags'),
},
};
class HelpView { class HelpView {
constructor() { constructor(section, subsection) {
this._template = views.getTemplate('help'); this._hostNode = document.getElementById('content-holder');
this._sectionTemplates = {};
const sectionKeys = ['about', 'keyboard', 'search', 'comments', 'tos']; const sourceNode = template();
for (let section of sectionKeys) { const ctx = {
const templateName = 'help-' + section; name: config.name,
this._sectionTemplates[section] = views.getTemplate(templateName);
}
this._subsectionTemplates = {
'search': {
'default': views.getTemplate('help-search-general'),
'posts': views.getTemplate('help-search-posts'),
'users': views.getTemplate('help-search-users'),
'tags': views.getTemplate('help-search-tags'),
}
}; };
}
render(ctx) { section = section || 'about';
const target = document.getElementById('content-holder'); if (section in sectionTemplates) {
const source = this._template(); views.replaceContent(
sourceNode.querySelector('.content'),
ctx.section = ctx.section || 'about'; sectionTemplates[section](ctx));
if (ctx.section in this._sectionTemplates) {
views.showView(
source.querySelector('.content'),
this._sectionTemplates[ctx.section]({
name: config.name,
}));
} }
ctx.subsection = ctx.subsection || 'default'; subsection = subsection || 'default';
if (ctx.section in this._subsectionTemplates && if (section in subsectionTemplates &&
ctx.subsection in this._subsectionTemplates[ctx.section]) { subsection in subsectionTemplates[section]) {
views.showView( views.replaceContent(
source.querySelector('.subcontent'), sourceNode.querySelector('.subcontent'),
this._subsectionTemplates[ctx.section][ctx.subsection]({ subsectionTemplates[section][subsection](ctx));
name: config.name,
}));
} }
for (let item of source.querySelectorAll('.primary [data-name]')) { for (let itemNode of
if (item.getAttribute('data-name') === ctx.section) { sourceNode.querySelectorAll('.primary [data-name]')) {
item.className = 'active'; itemNode.classList.toggle(
} else { 'active',
item.className = ''; itemNode.getAttribute('data-name') === section);
}
} }
for (let item of source.querySelectorAll('.secondary [data-name]')) { for (let itemNode of
if (item.getAttribute('data-name') === ctx.subsection) { sourceNode.querySelectorAll('.secondary [data-name]')) {
item.className = 'active'; itemNode.classList.toggle(
} else { 'active',
item.className = ''; itemNode.getAttribute('data-name') === subsection);
}
} }
views.listenToMessages(source); views.replaceContent(this._hostNode, sourceNode);
views.showView(target, source);
views.scrollToHash(); views.scrollToHash();
} }
} }

View file

@ -1,7 +1,6 @@
'use strict'; 'use strict';
const router = require('../router.js'); const router = require('../router.js');
const config = require('../config.js');
const misc = require('../util/misc.js'); const misc = require('../util/misc.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const PostContentControl = require('../controls/post_content_control.js'); const PostContentControl = require('../controls/post_content_control.js');
@ -10,48 +9,45 @@ const PostNotesOverlayControl
const TagAutoCompleteControl = const TagAutoCompleteControl =
require('../controls/tag_auto_complete_control.js'); require('../controls/tag_auto_complete_control.js');
const template = views.getTemplate('home');
const statsTemplate = views.getTemplate('home-stats');
class HomeView { class HomeView {
constructor() { constructor(ctx) {
this._homeTemplate = views.getTemplate('home'); this._hostNode = document.getElementById('content-holder');
}
render(ctx) { const sourceNode = template(ctx);
Object.assign(ctx, { views.replaceContent(this._hostNode, sourceNode);
name: config.name,
version: config.meta.version,
buildDate: config.meta.buildDate,
});
const target = document.getElementById('content-holder');
const source = this._homeTemplate(ctx);
views.listenToMessages(source); if (this._formNode) {
views.showView(target, source); this._formNode.querySelector('input[name=all-posts')
.addEventListener('click', e => this._evtAllPostsClick(e));
const form = source.querySelector('form'); this._tagAutoCompleteControl = new TagAutoCompleteControl(
if (form) { this._searchInputNode);
form.querySelector('input[name=all-posts') this._formNode.addEventListener(
.addEventListener('click', e => { 'submit', e => this._evtFormSubmit(e));
e.preventDefault();
router.show('/posts/');
});
const searchTextInput = form.querySelector(
'input[name=search-text]');
new TagAutoCompleteControl(searchTextInput);
form.addEventListener('submit', e => {
e.preventDefault();
const text = searchTextInput.value;
searchTextInput.blur();
router.show('/posts/' + misc.formatSearchQuery({text: text}));
});
} }
const postContainerNode = source.querySelector('.post-container'); }
if (postContainerNode && ctx.featuredPost) { showSuccess(text) {
new PostContentControl( views.showSuccess(this._hostNode, text);
postContainerNode, }
ctx.featuredPost,
showError(text) {
views.showError(this._hostNode, text);
}
setStats(stats) {
views.replaceContent(this._statsContainerNode, statsTemplate(stats));
}
setFeaturedPost(postInfo) {
if (this._postContainerNode && postInfo.featuredPost) {
this._postContentControl = new PostContentControl(
this._postContainerNode,
postInfo.featuredPost,
() => { () => {
return [ return [
window.innerWidth * 0.8, window.innerWidth * 0.8,
@ -59,11 +55,39 @@ class HomeView {
]; ];
}); });
new PostNotesOverlayControl( this._postNotesOverlay = new PostNotesOverlayControl(
postContainerNode.querySelector('.post-overlay'), this._postContainerNode.querySelector('.post-overlay'),
ctx.featuredPost); postInfo.featuredPost);
} }
} }
get _statsContainerNode() {
return this._hostNode.querySelector('.stats-container');
}
get _postContainerNode() {
return this._hostNode.querySelector('.post-container');
}
get _formNode() {
return this._hostNode.querySelector('form');
}
get _searchInputNode() {
return this._formNode.querySelector('input[name=search-text]');
}
_evtAllPostsClick(e) {
e.preventDefault();
router.show('/posts/');
}
_evtFormSubmit(e) {
e.preventDefault();
this._searchInputNode.blur();
router.show('/posts/' + misc.formatSearchQuery({
text: this._searchInputNode.value}));
}
} }
module.exports = HomeView; module.exports = HomeView;

View file

@ -1,43 +1,67 @@
'use strict'; 'use strict';
const config = require('../config.js'); const config = require('../config.js');
const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
class LoginView { const template = views.getTemplate('login');
constructor() {
this._template = views.getTemplate('login');
}
render(ctx) { class LoginView extends events.EventTarget {
const target = document.getElementById('content-holder'); constructor() {
const source = this._template({ super();
this._hostNode = document.getElementById('content-holder');
views.replaceContent(this._hostNode, template({
userNamePattern: config.userNameRegex, userNamePattern: config.userNameRegex,
passwordPattern: config.passwordRegex, passwordPattern: config.passwordRegex,
canSendMails: config.canSendMails, canSendMails: config.canSendMails,
}); }));
const form = source.querySelector('form'); views.decorateValidator(this._formNode);
const userNameField = source.querySelector('#user-name'); this._userNameFieldNode.setAttribute('pattern', config.userNameRegex);
const passwordField = source.querySelector('#user-password'); this._passwordFieldNode.setAttribute('pattern', config.passwordRegex);
const rememberUserField = source.querySelector('#remember-user'); this._formNode.addEventListener('submit', e => {
views.decorateValidator(form);
userNameField.setAttribute('pattern', config.userNameRegex);
passwordField.setAttribute('pattern', config.passwordRegex);
form.addEventListener('submit', e => {
e.preventDefault(); e.preventDefault();
views.clearMessages(target); this.dispatchEvent(new CustomEvent('submit', {
views.disableForm(form); detail: {
ctx.login( name: this._userNameFieldNode.value,
userNameField.value, password: this._passwordFieldNode.value,
passwordField.value, remember: this._rememberFieldNode.checked,
rememberUserField.checked) },
.always(() => { views.enableForm(form); }); }));
}); });
}
views.listenToMessages(source); get _formNode() {
views.showView(target, source); return this._hostNode.querySelector('form');
}
get _userNameFieldNode() {
return this._formNode.querySelector('#user-name');
}
get _passwordFieldNode() {
return this._formNode.querySelector('#user-password');
}
get _rememberFieldNode() {
return this._formNode.querySelector('#remember-user');
}
disableForm() {
views.disableForm(this._formNode);
}
enableForm() {
views.enableForm(this._formNode);
}
clearMessages() {
views.clearMessages(this._hostNode);
}
showError(message) {
views.showError(this._hostNode, message);
} }
} }

View file

@ -1,11 +1,13 @@
'use strict'; 'use strict';
const router = require('../router.js'); const router = require('../router.js');
const events = require('../events.js');
const keyboard = require('../util/keyboard.js'); const keyboard = require('../util/keyboard.js');
const misc = require('../util/misc.js'); const misc = require('../util/misc.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const holderTemplate = views.getTemplate('manual-pager');
const navTemplate = views.getTemplate('manual-pager-nav');
function _formatUrl(url, page) { function _formatUrl(url, page) {
return url.replace('{page}', page); return url.replace('{page}', page);
} }
@ -56,28 +58,28 @@ function _getPages(currentPage, pageNumbers, clientUrl) {
} }
class ManualPageView { class ManualPageView {
constructor() { constructor(ctx) {
this._holderTemplate = views.getTemplate('manual-pager'); this._hostNode = document.getElementById('content-holder');
this._navTemplate = views.getTemplate('manual-pager-nav');
}
render(ctx) { const sourceNode = holderTemplate();
const target = document.getElementById('content-holder'); const pageContentHolderNode
const source = this._holderTemplate(); = sourceNode.querySelector('.page-content-holder');
const pageContentHolder = source.querySelector('.page-content-holder'); const pageHeaderHolderNode
const pageHeaderHolder = source.querySelector('.page-header-holder'); = sourceNode.querySelector('.page-header-holder');
const pageNav = source.querySelector('.page-nav'); const pageNavNode = sourceNode.querySelector('.page-nav');
const currentPage = ctx.searchQuery.page; const currentPage = ctx.searchQuery.page;
ctx.headerContext.target = pageHeaderHolder; ctx.headerContext.hostNode = pageHeaderHolderNode;
if (ctx.headerRenderer) { if (ctx.headerRenderer) {
ctx.headerRenderer.render(ctx.headerContext); ctx.headerRenderer(ctx.headerContext);
} }
views.replaceContent(this._hostNode, sourceNode);
ctx.requestPage(currentPage).then(response => { ctx.requestPage(currentPage).then(response => {
Object.assign(ctx.pageContext, response); Object.assign(ctx.pageContext, response);
ctx.pageContext.target = pageContentHolder; ctx.pageContext.hostNode = pageContentHolderNode;
ctx.pageRenderer.render(ctx.pageContext); ctx.pageRenderer(ctx.pageContext);
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);
@ -95,28 +97,35 @@ class ManualPageView {
}); });
if (response.total) { if (response.total) {
views.showView(pageNav, this._navTemplate({ views.replaceContent(
prevLink: _formatUrl(ctx.clientUrl, currentPage - 1), pageNavNode,
nextLink: _formatUrl(ctx.clientUrl, currentPage + 1), navTemplate({
prevLinkActive: currentPage > 1, prevLink: _formatUrl(ctx.clientUrl, currentPage - 1),
nextLinkActive: currentPage < totalPages, nextLink: _formatUrl(ctx.clientUrl, currentPage + 1),
pages: pages, prevLinkActive: currentPage > 1,
})); nextLinkActive: currentPage < totalPages,
pages: pages,
}));
} }
views.listenToMessages(source);
views.showView(target, source);
if (response.total <= (currentPage - 1) * response.pageSize) { if (response.total <= (currentPage - 1) * response.pageSize) {
events.notify(events.Info, 'No data to show'); this.showInfo('No data to show');
} }
}, response => { }, response => {
views.listenToMessages(source); this.showError(response.description);
views.showView(target, source);
events.notify(events.Error, response.description);
}); });
} }
unrender() { showSuccess(message) {
views.showSuccess(this._hostNode, message);
}
showError(message) {
views.showError(this._hostNode, message);
}
showInfo(message) {
views.showInfo(this._hostNode, message);
} }
} }

View file

@ -3,15 +3,14 @@
const config = require('../config.js'); const config = require('../config.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
class NotFoundView { const template = views.getTemplate('not-found');
constructor() {
this._template = views.getTemplate('not-found');
}
render(ctx) { class NotFoundView {
const target = document.getElementById('content-holder'); constructor(path) {
const source = this._template(ctx); this._hostNode = document.getElementById('content-holder');
views.showView(target, source);
const sourceNode = template({path: path});
views.replaceContent(this._hostNode, sourceNode);
} }
} }

View file

@ -1,31 +1,54 @@
'use strict'; 'use strict';
const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
class PasswordResetView { const template = views.getTemplate('password-reset');
class PasswordResetView extends events.EventTarget {
constructor() { constructor() {
this._template = views.getTemplate('password-reset'); super();
this._hostNode = document.getElementById('content-holder');
views.replaceContent(this._hostNode, template());
views.decorateValidator(this._formNode);
this._hostNode.addEventListener('submit', e => {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
userNameOrEmail: this._userNameOrEmailFieldNode.value,
},
}));
});
} }
render(ctx) { showSuccess(message) {
const target = document.getElementById('content-holder'); views.showSuccess(this._hostNode, message);
const source = this._template(); }
const form = source.querySelector('form'); showError(message) {
const userNameOrEmailField = source.querySelector('#user-name'); views.showError(this._hostNode, message);
}
views.decorateValidator(form); clearMessages() {
views.clearMessages(this._hostNode);
}
form.addEventListener('submit', e => { enableForm() {
e.preventDefault(); views.enableForm(this._formNode);
views.clearMessages(target); }
views.disableForm(form);
ctx.proceed(userNameOrEmailField.value)
.catch(() => { views.enableForm(form); });
});
views.listenToMessages(source); disableForm() {
views.showView(target, source); views.disableForm(this._formNode);
}
get _formNode() {
return this._hostNode.querySelector('form');
}
get _userNameOrEmailFieldNode() {
return this._formNode.querySelector('#user-name');
} }
} }

View file

@ -14,20 +14,16 @@ const PostEditSidebarControl =
const CommentListControl = require('../controls/comment_list_control.js'); const CommentListControl = require('../controls/comment_list_control.js');
const CommentFormControl = require('../controls/comment_form_control.js'); const CommentFormControl = require('../controls/comment_form_control.js');
const template = views.getTemplate('post');
class PostView { class PostView {
constructor() { constructor(ctx) {
this._template = views.getTemplate('post'); this._hostNode = document.getElementById('content-holder');
}
render(ctx) { const sourceNode = template(ctx);
const target = document.getElementById('content-holder'); const postContainerNode = sourceNode.querySelector('.post-container');
const source = this._template(ctx); const sidebarNode = sourceNode.querySelector('.sidebar');
views.replaceContent(this._hostNode, sourceNode);
const postContainerNode = source.querySelector('.post-container');
const sidebarNode = source.querySelector('.sidebar');
views.listenToMessages(source);
views.showView(target, source);
const postViewNode = document.body.querySelector('.content-wrapper'); const postViewNode = document.body.querySelector('.content-wrapper');
const topNavigationNode = const topNavigationNode =

View file

@ -1,33 +1,27 @@
'use strict'; 'use strict';
const router = require('../router.js'); const router = require('../router.js');
const settings = require('../settings.js'); const settings = require('../models/settings.js');
const keyboard = require('../util/keyboard.js'); const keyboard = require('../util/keyboard.js');
const misc = require('../util/misc.js'); const misc = require('../util/misc.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const TagAutoCompleteControl = const TagAutoCompleteControl =
require('../controls/tag_auto_complete_control.js'); require('../controls/tag_auto_complete_control.js');
const template = views.getTemplate('posts-header');
class PostsHeaderView { class PostsHeaderView {
constructor() { constructor(ctx) {
this._template = views.getTemplate('posts-header'); ctx.settings = settings.get();
} this._hostNode = ctx.hostNode;
views.replaceContent(this._hostNode, template(ctx));
render(ctx) { if (this._queryInputNode) {
ctx.settings = settings.getSettings(); new TagAutoCompleteControl(this._queryInputNode);
const target = ctx.target;
const source = this._template(ctx);
const form = source.querySelector('form');
const searchTextInput = form.querySelector('[name=search-text]');
if (searchTextInput) {
new TagAutoCompleteControl(searchTextInput);
} }
keyboard.bind('q', () => { keyboard.bind('q', () => {
form.querySelector('input').focus(); this._formNode.querySelector('input').focus();
}); });
keyboard.bind('p', () => { keyboard.bind('p', () => {
@ -38,31 +32,37 @@ class PostsHeaderView {
} }
}); });
for (let safetyButton of form.querySelectorAll('.safety')) { for (let safetyButton of this._formNode.querySelectorAll('.safety')) {
safetyButton.addEventListener( safetyButton.addEventListener(
'click', e => this._evtSafetyButtonClick(e, ctx.clientUrl)); 'click', e => this._evtSafetyButtonClick(e, ctx.clientUrl));
} }
form.addEventListener( this._formNode.addEventListener(
'submit', e => this._evtFormSubmit(e, searchTextInput)); 'submit', e => this._evtFormSubmit(e, this._queryInputNode));
}
views.showView(target, source); get _formNode() {
return this._hostNode.querySelector('form');
}
get _queryInputNode() {
return this._formNode.querySelector('[name=search-text]');
} }
_evtSafetyButtonClick(e, url) { _evtSafetyButtonClick(e, url) {
e.preventDefault(); e.preventDefault();
e.target.classList.toggle('disabled'); e.target.classList.toggle('disabled');
const safety = e.target.getAttribute('data-safety'); const safety = e.target.getAttribute('data-safety');
let browsingSettings = settings.getSettings(); let browsingSettings = settings.get();
browsingSettings.listPosts[safety] = browsingSettings.listPosts[safety] =
!browsingSettings.listPosts[safety]; !browsingSettings.listPosts[safety];
settings.saveSettings(browsingSettings, true); settings.save(browsingSettings, true);
router.show(url.replace(/{page}/, 1)); router.show(url.replace(/{page}/, 1));
} }
_evtFormSubmit(e, searchTextInput) { _evtFormSubmit(e, queryInputNode) {
e.preventDefault(); e.preventDefault();
const text = searchTextInput.value; const text = queryInputNode.value;
searchTextInput.blur(); queryInputNode.blur();
router.show('/posts/' + misc.formatSearchQuery({text: text})); router.show('/posts/' + misc.formatSearchQuery({text: text}));
} }
} }

View file

@ -2,15 +2,11 @@
const views = require('../util/views.js'); const views = require('../util/views.js');
class PostsPageView { const template = views.getTemplate('posts-page');
constructor() {
this._template = views.getTemplate('posts-page');
}
render(ctx) { class PostsPageView {
const target = ctx.target; constructor(ctx) {
const source = this._template(ctx); views.replaceContent(ctx.hostNode, template(ctx));
views.showView(target, source);
} }
} }

View file

@ -1,40 +1,64 @@
'use strict'; 'use strict';
const config = require('../config.js'); const config = require('../config.js');
const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
class RegistrationView { const template = views.getTemplate('user-registration');
class RegistrationView extends events.EventTarget {
constructor() { constructor() {
this._template = views.getTemplate('user-registration'); super();
this._hostNode = document.getElementById('content-holder');
views.replaceContent(this._hostNode, template({
userNamePattern: config.userNameRegex,
passwordPattern: config.passwordRegex,
}));
views.decorateValidator(this._formNode);
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
} }
render(ctx) { clearMessages() {
ctx.userNamePattern = config.userNameRegex; views.clearMessages(this._hostNode);
ctx.passwordPattern = config.passwordRegex; }
const target = document.getElementById('content-holder'); showError(message) {
const source = this._template(ctx); views.showError(this._hostNode, message);
}
const form = source.querySelector('form'); enableForm() {
const userNameField = source.querySelector('#user-name'); views.enableForm(this._formNode);
const passwordField = source.querySelector('#user-password'); }
const emailField = source.querySelector('#user-email');
views.decorateValidator(form); disableForm() {
views.disableForm(this._formNode);
}
form.addEventListener('submit', e => { _evtSubmit(e) {
e.preventDefault(); e.preventDefault();
views.clearMessages(target); this.dispatchEvent(new CustomEvent('submit', {
views.disableForm(form); detail: {
ctx.register( name: this._userNameFieldNode.value,
userNameField.value, password: this._passwordFieldNode.value,
passwordField.value, email: this._emailFieldNode.value,
emailField.value) },
.always(() => { views.enableForm(form); }); }));
}); }
views.listenToMessages(source); get _formNode() {
views.showView(target, source); return this._hostNode.querySelector('form');
}
get _userNameFieldNode() {
return this._formNode.querySelector('#user-name');
}
get _passwordFieldNode() {
return this._formNode.querySelector('#user-password');
}
get _emailFieldNode() {
return this._formNode.querySelector('#user-email');
} }
} }

View file

@ -1,36 +1,50 @@
'use strict'; 'use strict';
const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
class SettingsView { const template = views.getTemplate('settings');
constructor() {
this._template = views.getTemplate('settings'); class SettingsView extends events.EventTarget {
constructor(ctx) {
super();
this._hostNode = document.getElementById('content-holder');
views.replaceContent(
this._hostNode, template({browsingSettings: ctx.settings}));
views.decorateValidator(this._formNode);
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
} }
render(ctx) { clearMessages() {
const target = document.getElementById('content-holder'); views.clearMessages(this._hostNode);
const source = this._template({browsingSettings: ctx.getSettings()}); }
const form = source.querySelector('form'); showSuccess(text) {
views.decorateValidator(form); views.showSuccess(this._hostNode, text);
}
form.addEventListener('submit', e => { _evtSubmit(e) {
e.preventDefault(); e.preventDefault();
views.clearMessages(source); this.dispatchEvent(new CustomEvent('change', {
ctx.saveSettings({ detail: {
upscaleSmallPosts: settings: {
form.querySelector('#upscale-small-posts').checked, upscaleSmallPosts: this._formNode.querySelector(
endlessScroll: '#upscale-small-posts').checked,
form.querySelector('#endless-scroll').checked, endlessScroll: this._formNode.querySelector(
keyboardShortcuts: '#endless-scroll').checked,
form.querySelector('#keyboard-shortcuts').checked, keyboardShortcuts: this._formNode.querySelector(
transparencyGrid: '#keyboard-shortcuts').checked,
form.querySelector('#transparency-grid').checked, transparencyGrid: this._formNode.querySelector(
}); '#transparency-grid').checked,
}); },
},
}));
}
views.listenToMessages(source); get _formNode() {
views.showView(target, source); return this._hostNode.querySelector('form');
} }
} }

View file

@ -1,32 +1,28 @@
'use strict'; 'use strict';
const misc = require('../util/misc.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
class TagListHeaderView { const template = views.getTemplate('tag-categories');
constructor() {
this._template = views.getTemplate('tag-categories');
}
render(ctx) { class TagCategoriesView {
const target = document.getElementById('content-holder'); constructor(ctx) {
const source = this._template(ctx); this._hostNode = document.getElementById('content-holder');
const sourceNode = template(ctx);
const form = source.querySelector('form'); const formNode = sourceNode.querySelector('form');
const newRowTemplate = source.querySelector('.add-template'); const newRowTemplate = sourceNode.querySelector('.add-template');
const tableBody = source.querySelector('tbody'); const tableBodyNode = sourceNode.querySelector('tbody');
const addLink = source.querySelector('a.add'); const addLinkNode = sourceNode.querySelector('a.add');
const saveButton = source.querySelector('button.save');
newRowTemplate.parentNode.removeChild(newRowTemplate); newRowTemplate.parentNode.removeChild(newRowTemplate);
views.decorateValidator(form); views.decorateValidator(formNode);
for (let row of tableBody.querySelectorAll('tr')) { for (let row of tableBodyNode.querySelectorAll('tr')) {
this._addRowHandlers(row); this._addRowHandlers(row);
} }
if (addLink) { if (addLinkNode) {
addLink.addEventListener('click', e => { addLinkNode.addEventListener('click', e => {
e.preventDefault(); e.preventDefault();
let newRow = newRowTemplate.cloneNode(true); let newRow = newRowTemplate.cloneNode(true);
tableBody.appendChild(newRow); tableBody.appendChild(newRow);
@ -34,19 +30,26 @@ class TagListHeaderView {
}); });
} }
form.addEventListener('submit', e => { formNode.addEventListener('submit', e => {
this._evtSaveButtonClick(e, ctx, target); this._evtSaveButtonClick(e, ctx);
}); });
views.listenToMessages(source); views.replaceContent(this._hostNode, sourceNode);
views.showView(target, source);
} }
_evtSaveButtonClick(e, ctx, target) { showSuccess(message) {
views.showSuccess(this._hostNode, message);
}
showError(message) {
views.showError(this._hostNode, message);
}
_evtSaveButtonClick(e, ctx) {
e.preventDefault(); e.preventDefault();
views.clearMessages(target); views.clearMessages(this._hostNode);
const tableBody = target.querySelector('tbody'); const tableBodyNode = this._hostNode.querySelector('tbody');
ctx.getCategories().then(categories => { ctx.getCategories().then(categories => {
let existingCategories = {}; let existingCategories = {};
@ -59,7 +62,7 @@ class TagListHeaderView {
let removedCategories = []; let removedCategories = [];
let changedCategories = []; let changedCategories = [];
let allNames = []; let allNames = [];
for (let row of tableBody.querySelectorAll('tr')) { for (let row of tableBodyNode.querySelectorAll('tr')) {
let name = row.getAttribute('data-category'); let name = row.getAttribute('data-category');
let category = { let category = {
originalName: name, originalName: name,
@ -127,4 +130,4 @@ class TagListHeaderView {
} }
} }
module.exports = TagListHeaderView; module.exports = TagCategoriesView;

View file

@ -1,30 +1,52 @@
'use strict'; 'use strict';
const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
class TagDeleteView { const template = views.getTemplate('tag-delete');
constructor() {
this._template = views.getTemplate('tag-delete'); class TagDeleteView extends events.EventTarget {
constructor(ctx) {
super();
this._hostNode = ctx.hostNode;
this._tag = ctx.tag;
views.replaceContent(this._hostNode, template(ctx));
views.decorateValidator(this._formNode);
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
} }
render(ctx) { clearMessages() {
const target = ctx.target; views.clearMessages(this._hostNode);
const source = this._template(ctx); }
const form = source.querySelector('form'); enableForm() {
views.enableForm(this._formNode);
}
views.decorateValidator(form); disableForm() {
views.disableForm(this._formNode);
}
form.addEventListener('submit', e => { showSuccess(message) {
e.preventDefault(); views.showSuccess(this._hostNode, message);
views.clearMessages(target); }
views.disableForm(form);
ctx.delete(ctx.tag)
.catch(() => { views.enableForm(form); });
});
views.listenToMessages(source); showError(message) {
views.showView(target, source); views.showError(this._hostNode, message);
}
_evtSubmit(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
tag: this._tag,
},
}));
}
get _formNode() {
return this._hostNode.querySelector('form');
} }
} }

View file

@ -1,40 +1,66 @@
'use strict'; 'use strict';
const config = require('../config.js'); const config = require('../config.js');
const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const TagAutoCompleteControl = const TagAutoCompleteControl =
require('../controls/tag_auto_complete_control.js'); require('../controls/tag_auto_complete_control.js');
class TagMergeView { const template = views.getTemplate('tag-merge');
constructor() {
this._template = views.getTemplate('tag-merge');
}
render(ctx) { class TagMergeView extends events.EventTarget {
constructor(ctx) {
super();
this._tag = ctx.tag;
this._hostNode = ctx.hostNode;
ctx.tagNamePattern = config.tagNameRegex; ctx.tagNamePattern = config.tagNameRegex;
views.replaceContent(this._hostNode, template(ctx));
const target = ctx.target; views.decorateValidator(this._formNode);
const source = this._template(ctx); if (this._targetTagFieldNode) {
new TagAutoCompleteControl(this._targetTagFieldNode);
const form = source.querySelector('form');
const otherTagField = source.querySelector('.target input');
views.decorateValidator(form);
if (otherTagField) {
new TagAutoCompleteControl(otherTagField);
} }
form.addEventListener('submit', e => { this._formNode.addEventListener('submit', e => this._evtSubmit(e));
}
e.preventDefault(); clearMessages() {
views.clearMessages(target); views.clearMessages(this._hostNode);
views.disableForm(form); }
ctx.mergeTo(otherTagField.value)
.catch(() => { views.enableForm(form); });
});
views.listenToMessages(source); enableForm() {
views.showView(target, source); views.enableForm(this._formNode);
}
disableForm() {
views.disableForm(this._formNode);
}
showSuccess(message) {
views.showSuccess(this._hostNode, message);
}
showError(message) {
views.showError(this._hostNode, message);
}
_evtSubmit(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
tag: this._tag,
targetTagName: this._targetTagFieldNode.value,
},
}));
}
get _formNode() {
return this._hostNode.querySelector('form');
}
get _targetTagFieldNode() {
return this._formNode.querySelector('.target input');
} }
} }

View file

@ -1,54 +1,89 @@
'use strict'; 'use strict';
const config = require('../config.js'); const config = require('../config.js');
const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const TagInputControl = require('../controls/tag_input_control.js'); const TagInputControl = require('../controls/tag_input_control.js');
function split(str) { const template = views.getTemplate('tag-summary');
function _split(str) {
return str.split(/\s+/).filter(s => s); return str.split(/\s+/).filter(s => s);
} }
class TagSummaryView { class TagSummaryView extends events.EventTarget {
constructor() { constructor(ctx) {
this._template = views.getTemplate('tag-summary'); super();
}
render(ctx) { this._tag = ctx.tag;
this._hostNode = ctx.hostNode;
const baseRegex = config.tagNameRegex.replace(/[\^\$]/g, ''); const baseRegex = config.tagNameRegex.replace(/[\^\$]/g, '');
ctx.tagNamesPattern = '^((' + baseRegex + ')\\s+)*(' + baseRegex + ')$'; ctx.tagNamesPattern = '^((' + baseRegex + ')\\s+)*(' + baseRegex + ')$';
views.replaceContent(this._hostNode, template(ctx));
const target = ctx.target; views.decorateValidator(this._formNode);
const source = this._template(ctx);
const form = source.querySelector('form'); if (this._implicationsFieldNode) {
const namesField = source.querySelector('.names input'); new TagInputControl(this._implicationsFieldNode);
const categoryField = source.querySelector('.category select');
const implicationsField = source.querySelector('.implications input');
const suggestionsField = source.querySelector('.suggestions input');
if (implicationsField) {
new TagInputControl(implicationsField);
} }
if (suggestionsField) { if (this._suggestionsFieldNode) {
new TagInputControl(suggestionsField); new TagInputControl(this._suggestionsFieldNode);
} }
views.decorateValidator(form); this._formNode.addEventListener('submit', e => this._evtSubmit(e));
}
form.addEventListener('submit', e => { clearMessages() {
e.preventDefault(); views.clearMessages(this._hostNode);
views.clearMessages(target); }
views.disableForm(form);
ctx.save({
names: split(namesField.value),
category: categoryField.value,
implications: split(implicationsField.value),
suggestions: split(suggestionsField.value),
}).always(() => { views.enableForm(form); });
});
views.listenToMessages(source); enableForm() {
views.showView(target, source); views.enableForm(this._formNode);
}
disableForm() {
views.disableForm(this._formNode);
}
showSuccess(message) {
views.showSuccess(this._hostNode, message);
}
showError(message) {
views.showError(this._hostNode, message);
}
_evtSubmit(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
tag: this._tag,
names: _split(this._namesFieldNode.value),
category: this._categoryFieldNode.value,
implications: _split(this._implicationsFieldNode.value),
suggestions: _split(this._suggestionsFieldNode.value),
},
}));
}
get _formNode() {
return this._hostNode.querySelector('form');
}
get _namesFieldNode() {
return this._formNode.querySelector('.names input');
}
get _categoryFieldNode() {
return this._formNode.querySelector('.category select');
}
get _implicationsFieldNode() {
return this._formNode.querySelector('.implications input');
}
get _suggestionsFieldNode() {
return this._formNode.querySelector('.suggestions input');
} }
} }

View file

@ -1,25 +1,22 @@
'use strict'; 'use strict';
const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const TagSummaryView = require('./tag_summary_view.js'); const TagSummaryView = require('./tag_summary_view.js');
const TagMergeView = require('./tag_merge_view.js'); const TagMergeView = require('./tag_merge_view.js');
const TagDeleteView = require('./tag_delete_view.js'); const TagDeleteView = require('./tag_delete_view.js');
class TagView { const template = views.getTemplate('tag');
constructor() {
this._template = views.getTemplate('tag');
this._summaryView = new TagSummaryView();
this._mergeView = new TagMergeView();
this._deleteView = new TagDeleteView();
}
render(ctx) { class TagView extends events.EventTarget {
const target = document.getElementById('content-holder'); constructor(ctx) {
const source = this._template(ctx); super();
this._hostNode = document.getElementById('content-holder');
ctx.section = ctx.section || 'summary'; ctx.section = ctx.section || 'summary';
views.replaceContent(this._hostNode, template(ctx));
for (let item of source.querySelectorAll('[data-name]')) { for (let item of this._hostNode.querySelectorAll('[data-name]')) {
if (item.getAttribute('data-name') === ctx.section) { if (item.getAttribute('data-name') === ctx.section) {
item.className = 'active'; item.className = 'active';
} else { } else {
@ -27,21 +24,47 @@ class TagView {
} }
} }
let view = null; ctx.hostNode = this._hostNode.querySelector('.tag-content-holder');
if (ctx.section == 'merge') { if (ctx.section == 'merge') {
view = this._mergeView; this._view = new TagMergeView(ctx);
this._view.addEventListener('submit', e => {
this.dispatchEvent(
new CustomEvent('merge', {detail: e.detail}));
});
} else if (ctx.section == 'delete') { } else if (ctx.section == 'delete') {
view = this._deleteView; this._view = new TagDeleteView(ctx);
this._view.addEventListener('submit', e => {
this.dispatchEvent(
new CustomEvent('delete', {detail: e.detail}));
});
} else { } else {
view = this._summaryView; this._view = new TagSummaryView(ctx);
this._view.addEventListener('submit', e => {
this.dispatchEvent(
new CustomEvent('change', {detail: e.detail}));
});
} }
ctx.target = source.querySelector('.tag-content-holder'); }
view.render(ctx);
views.listenToMessages(source); clearMessages() {
views.showView(target, source); this._view.clearMessages();
}
enableForm() {
this._view.enableForm();
}
disableForm() {
this._view.disableForm();
}
showSuccess(message) {
this._view.showSuccess(message);
}
showError(message) {
this._view.showError(message);
} }
} }
module.exports = TagView; module.exports = TagView;

View file

@ -7,34 +7,39 @@ const views = require('../util/views.js');
const TagAutoCompleteControl = const TagAutoCompleteControl =
require('../controls/tag_auto_complete_control.js'); require('../controls/tag_auto_complete_control.js');
const template = views.getTemplate('tags-header');
class TagsHeaderView { class TagsHeaderView {
constructor() { constructor(ctx) {
this._template = views.getTemplate('tags-header'); this._hostNode = ctx.hostNode;
} views.replaceContent(this._hostNode, template(ctx));
render(ctx) { if (this._queryInputNode) {
const target = ctx.target; new TagAutoCompleteControl(this._queryInputNode);
const source = this._template(ctx);
const form = source.querySelector('form');
const searchTextInput = form.querySelector('[name=search-text]');
if (searchTextInput) {
new TagAutoCompleteControl(searchTextInput);
} }
keyboard.bind('q', () => { keyboard.bind('q', () => {
form.querySelector('input').focus(); form.querySelector('input').focus();
}); });
form.addEventListener('submit', e => { this._formNode.addEventListener('submit', e => this._evtSubmit(e));
e.preventDefault(); }
const text = searchTextInput.value;
searchTextInput.blur();
router.show('/tags/' + misc.formatSearchQuery({text: text}));
});
views.showView(target, source); get _formNode() {
return this._hostNode.querySelector('form');
}
get _queryInputNode() {
return this._hostNode.querySelector('[name=search-text]');
}
_evtSubmit(e) {
e.preventDefault();
this._queryInputNode.blur();
router.show(
'/tags/' + misc.formatSearchQuery({
text: this._queryInputNode.value,
}));
} }
} }

View file

@ -2,15 +2,11 @@
const views = require('../util/views.js'); const views = require('../util/views.js');
class TagsPageView { const template = views.getTemplate('tags-page');
constructor() {
this._template = views.getTemplate('tags-page');
}
render(ctx) { class TagsPageView {
const target = ctx.target; constructor(ctx) {
const source = this._template(ctx); views.replaceContent(ctx.hostNode, template(ctx));
views.showView(target, source);
} }
} }

View file

@ -2,24 +2,19 @@
const views = require('../util/views.js'); const views = require('../util/views.js');
const template = views.getTemplate('top-navigation');
class TopNavigationView { class TopNavigationView {
constructor() { constructor() {
this._template = views.getTemplate('top-navigation'); this._hostNode = document.getElementById('top-navigation-holder');
this._navHolder = document.getElementById('top-navigation-holder');
this._lastCtx = null;
} }
render(ctx) { render(ctx) {
this._lastCtx = ctx; views.replaceContent(this._hostNode, template(ctx));
const target = this._navHolder;
const source = this._template(ctx);
views.showView(this._navHolder, source);
} }
activate(key) { activate(key) {
const allItemNodes = document.querySelectorAll( for (let itemNode of this._hostNode.querySelectorAll('[data-name]')) {
'#top-navigation-holder [data-name]');
for (let itemNode of allItemNodes) {
itemNode.classList.toggle( itemNode.classList.toggle(
'active', itemNode.getAttribute('data-name') === key); 'active', itemNode.getAttribute('data-name') === key);
} }

View file

@ -1,30 +1,54 @@
'use strict'; 'use strict';
const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
class UserDeleteView { const template = views.getTemplate('user-delete');
constructor() {
this._template = views.getTemplate('user-delete'); class UserDeleteView extends events.EventTarget {
constructor(ctx) {
super();
this._user = ctx.user;
this._hostNode = ctx.hostNode;
views.replaceContent(this._hostNode, template(ctx));
views.decorateValidator(this._formNode);
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
} }
render(ctx) { clearMessages() {
const target = ctx.target; views.clearMessages(this._hostNode);
const source = this._template(ctx); }
const form = source.querySelector('form'); showSuccess(message) {
views.showSuccess(this._hostNode, message);
}
views.decorateValidator(form); showError(message) {
views.showError(this._hostNode, message);
}
form.addEventListener('submit', e => { enableForm() {
e.preventDefault(); views.enableForm(this._formNode);
views.clearMessages(target); }
views.disableForm(form);
ctx.delete() disableForm() {
.catch(() => { views.enableForm(form); }); views.disableForm(this._formNode);
}); }
_evtSubmit(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
user: this._user,
},
}));
}
get _formNode() {
return this._hostNode.querySelector('form');
views.listenToMessages(source);
views.showView(target, source);
} }
} }

View file

@ -1,63 +1,102 @@
'use strict'; 'use strict';
const config = require('../config.js'); const config = require('../config.js');
const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const FileDropperControl = require('../controls/file_dropper_control.js'); const FileDropperControl = require('../controls/file_dropper_control.js');
class UserEditView { const template = views.getTemplate('user-edit');
constructor() {
this._template = views.getTemplate('user-edit'); class UserEditView extends events.EventTarget {
} constructor(ctx) {
super();
render(ctx) {
ctx.userNamePattern = config.userNameRegex + /|^$/.source; ctx.userNamePattern = config.userNameRegex + /|^$/.source;
ctx.passwordPattern = config.passwordRegex + /|^$/.source; ctx.passwordPattern = config.passwordRegex + /|^$/.source;
const target = ctx.target; this._user = ctx.user;
const source = this._template(ctx); this._hostNode = ctx.hostNode;
views.replaceContent(this._hostNode, template(ctx));
views.decorateValidator(this._formNode);
const form = source.querySelector('form'); this._avatarContent = null;
const avatarContentField = source.querySelector('#avatar-content'); if (this._avatarContentFieldNode) {
views.decorateValidator(form);
let avatarContent = null;
if (avatarContentField) {
new FileDropperControl( new FileDropperControl(
avatarContentField, this._avatarContentFieldNode,
{ {
lock: true, lock: true,
resolve: files => { resolve: files => {
source.querySelector( this._hostNode.querySelector(
'[name=avatar-style][value=manual]').checked = true; '[name=avatar-style][value=manual]').checked = true;
avatarContent = files[0]; this._avatarContent = files[0];
}, },
}); });
} }
form.addEventListener('submit', e => { this._formNode.addEventListener('submit', e => this._evtSubmit(e));
const rankField = source.querySelector('#user-rank'); }
const emailField = source.querySelector('#user-email');
const userNameField = source.querySelector('#user-name');
const passwordField = source.querySelector('#user-password');
const avatarStyleField = source.querySelector(
'[name=avatar-style]:checked');
e.preventDefault(); clearMessages() {
views.clearMessages(target); views.clearMessages(this._hostNode);
views.disableForm(form); }
ctx.edit({
name: userNameField.value,
password: passwordField.value,
email: emailField.value,
rank: rankField.value,
avatarStyle: avatarStyleField.value,
avatarContent: avatarContent})
.always(() => { views.enableForm(form); });
});
views.listenToMessages(source); showSuccess(message) {
views.showView(target, source); views.showSuccess(this._hostNode, message);
}
showError(message) {
views.showError(this._hostNode, message);
}
enableForm() {
views.enableForm(this._formNode);
}
disableForm() {
views.disableForm(this._formNode);
}
_evtSubmit(e) {
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,
avatarContent: this._avatarContent,
},
}));
}
get _formNode() {
return this._hostNode.querySelector('form');
}
get _rankFieldNode() {
return this._formNode.querySelector('#user-rank');
}
get _emailFieldNode() {
return this._formNode.querySelector('#user-email');
}
get _userNameFieldNode() {
return this._formNode.querySelector('#user-name');
}
get _passwordFieldNode() {
return this._formNode.querySelector('#user-password');
}
get _avatarContentFieldNode() {
return this._formNode.querySelector('#avatar-content');
}
get _avatarStyleFieldNode() {
return this._formNode.querySelector('[name=avatar-style]:checked');
} }
} }

View file

@ -2,16 +2,12 @@
const views = require('../util/views.js'); const views = require('../util/views.js');
class UserSummaryView { const template = views.getTemplate('user-summary');
constructor() {
this._template = views.getTemplate('user-summary');
}
render(ctx) { class UserSummaryView {
const target = ctx.target; constructor(ctx) {
const source = this._template(ctx); this._hostNode = ctx.hostNode;
views.listenToMessages(source); views.replaceContent(this._hostNode, template(ctx));
views.showView(target, source);
} }
} }

View file

@ -1,25 +1,22 @@
'use strict'; 'use strict';
const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const UserDeleteView = require('./user_delete_view.js'); const UserDeleteView = require('./user_delete_view.js');
const UserSummaryView = require('./user_summary_view.js'); const UserSummaryView = require('./user_summary_view.js');
const UserEditView = require('./user_edit_view.js'); const UserEditView = require('./user_edit_view.js');
class UserView { const template = views.getTemplate('user');
constructor() {
this._template = views.getTemplate('user');
this._deleteView = new UserDeleteView();
this._summaryView = new UserSummaryView();
this._editView = new UserEditView();
}
render(ctx) { class UserView extends events.EventTarget {
const target = document.getElementById('content-holder'); constructor(ctx) {
const source = this._template(ctx); super();
this._hostNode = document.getElementById('content-holder');
ctx.section = ctx.section || 'summary'; ctx.section = ctx.section || 'summary';
views.replaceContent(this._hostNode, template(ctx));
for (let item of source.querySelectorAll('[data-name]')) { for (let item of this._hostNode.querySelectorAll('[data-name]')) {
if (item.getAttribute('data-name') === ctx.section) { if (item.getAttribute('data-name') === ctx.section) {
item.className = 'active'; item.className = 'active';
} else { } else {
@ -27,19 +24,42 @@ class UserView {
} }
} }
let view = null; ctx.hostNode = this._hostNode.querySelector('#user-content-holder');
if (ctx.section == 'edit') { if (ctx.section == 'edit') {
view = this._editView; this._view = new UserEditView(ctx);
this._view.addEventListener('submit', e => {
this.dispatchEvent(
new CustomEvent('change', {detail: e.detail}));
});
} else if (ctx.section == 'delete') { } else if (ctx.section == 'delete') {
view = this._deleteView; this._view = new UserDeleteView(ctx);
this._view.addEventListener('submit', e => {
this.dispatchEvent(
new CustomEvent('delete', {detail: e.detail}));
});
} else { } else {
view = this._summaryView; this._view = new UserSummaryView(ctx);
} }
ctx.target = source.querySelector('#user-content-holder'); }
view.render(ctx);
views.listenToMessages(source); clearMessages() {
views.showView(target, source); this._view.clearMessages();
}
showSuccess(message) {
this._view.showSuccess(message);
}
showError(message) {
this._view.showError(message);
}
enableForm() {
this._view.enableForm();
}
disableForm() {
this._view.disableForm();
} }
} }

View file

@ -5,30 +5,35 @@ const keyboard = require('../util/keyboard.js');
const misc = require('../util/misc.js'); const misc = require('../util/misc.js');
const views = require('../util/views.js'); const views = require('../util/views.js');
const template = views.getTemplate('users-header');
class UsersHeaderView { class UsersHeaderView {
constructor() { constructor(ctx) {
this._template = views.getTemplate('users-header'); this._hostNode = ctx.hostNode;
} views.replaceContent(this._hostNode, template(ctx));
render(ctx) {
const target = ctx.target;
const source = this._template(ctx);
const form = source.querySelector('form');
keyboard.bind('q', () => { keyboard.bind('q', () => {
form.querySelector('input').focus(); this._formNode.querySelector('input').focus();
}); });
form.addEventListener('submit', e => { this._formNode.addEventListener('submit', e => this._evtSubmit(e));
e.preventDefault(); }
const searchTextInput = form.querySelector('[name=search-text]');
const text = searchTextInput.value;
searchTextInput.blur();
router.show('/users/' + misc.formatSearchQuery({text: text}));
});
views.showView(target, source); get _formNode() {
return this._hostNode.querySelector('form');
}
get _queryInputNode() {
return this._formNode.querySelector('[name=search-text]');
}
_evtSubmit(e) {
e.preventDefault();
this._queryInputNode.blur();
router.show(
'/users/' + misc.formatSearchQuery({
text: this._queryInputNode.value,
}));
} }
} }

View file

@ -2,15 +2,11 @@
const views = require('../util/views.js'); const views = require('../util/views.js');
class UsersPageView { const template = views.getTemplate('users-page');
constructor() {
this._template = views.getTemplate('users-page');
}
render(ctx) { class UsersPageView {
const target = ctx.target; constructor(ctx) {
const source = this._template(ctx); views.replaceContent(ctx.hostNode, template(ctx));
views.showView(target, source);
} }
} }