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:
parent
c74f06da35
commit
54e3099c56
68 changed files with 1755 additions and 1561 deletions
|
@ -2,9 +2,7 @@
|
|||
<div class='messages'></div>
|
||||
<header>
|
||||
<h1><%= ctx.name %></h1>
|
||||
<aside>
|
||||
Serving <%= ctx.postCount %> posts (<%= ctx.makeFileSize(ctx.diskUsage) %>)
|
||||
</aside>
|
||||
<aside class='stats-container'></aside>
|
||||
</header>
|
||||
<% if (ctx.canListPosts) { %>
|
||||
<form class='horizontal'>
|
||||
|
|
1
client/html/home_stats.tpl
Normal file
1
client/html/home_stats.tpl
Normal file
|
@ -0,0 +1 @@
|
|||
Serving <%= ctx.postCount %> posts (<%= ctx.makeFileSize(ctx.diskUsage) %>)
|
|
@ -6,8 +6,9 @@ const request = require('superagent');
|
|||
const config = require('./config.js');
|
||||
const events = require('./events.js');
|
||||
|
||||
class Api {
|
||||
class Api extends events.EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
this.user = null;
|
||||
this.userName = null;
|
||||
this.userPassword = null;
|
||||
|
@ -136,11 +137,10 @@ class Api {
|
|||
options);
|
||||
this.user = response;
|
||||
resolve();
|
||||
events.notify(events.Authentication);
|
||||
this.dispatchEvent(new CustomEvent('login'));
|
||||
}).catch(response => {
|
||||
reject(response.description);
|
||||
this.logout();
|
||||
events.notify(events.Authentication);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ class Api {
|
|||
this.user = null;
|
||||
this.userName = null;
|
||||
this.userPassword = null;
|
||||
events.notify(events.Authentication);
|
||||
this.dispatchEvent(new CustomEvent('logout'));
|
||||
}
|
||||
|
||||
forget() {
|
||||
|
|
|
@ -2,105 +2,47 @@
|
|||
|
||||
const router = require('../router.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 PasswordResetView = require('../views/password_reset_view.js');
|
||||
|
||||
class AuthController {
|
||||
class LoginController {
|
||||
constructor() {
|
||||
api.forget();
|
||||
topNavigation.activate('login');
|
||||
|
||||
this._loginView = new LoginView();
|
||||
this._passwordResetView = new PasswordResetView();
|
||||
this._loginView.addEventListener('submit', e => this._evtLogin(e));
|
||||
}
|
||||
|
||||
registerRoutes() {
|
||||
router.enter(
|
||||
/\/password-reset\/([^:]+):([^:]+)$/,
|
||||
(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() {
|
||||
_evtLogin(e) {
|
||||
this._loginView.clearMessages();
|
||||
this._loginView.disableForm();
|
||||
api.forget();
|
||||
TopNavigation.activate('login');
|
||||
this._loginView.render({
|
||||
login: (name, password, doRemember) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
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);
|
||||
api.login(e.detail.name, e.detail.password, e.detail.remember)
|
||||
.then(() => {
|
||||
const ctx = router.show('/');
|
||||
ctx.controller.showSuccess('Logged in');
|
||||
}, errorMessage => {
|
||||
router.show('/');
|
||||
events.notify(events.Error, errorMessage);
|
||||
this._loginView.showError(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();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,29 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
const api = require('../api.js');
|
||||
const router = require('../router.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 EmptyView = require('../views/empty_view.js');
|
||||
|
||||
class CommentsController {
|
||||
registerRoutes() {
|
||||
router.enter('/comments/:query?',
|
||||
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
|
||||
(ctx, next) => { this._listCommentsRoute(ctx); });
|
||||
this._commentsPageView = new CommentsPageView();
|
||||
this._emptyView = new EmptyView();
|
||||
}
|
||||
constructor(ctx) {
|
||||
topNavigation.activate('comments');
|
||||
|
||||
_listCommentsRoute(ctx) {
|
||||
TopNavigation.activate('comments');
|
||||
|
||||
pageController.run({
|
||||
this._pageController = new PageController({
|
||||
searchQuery: ctx.searchQuery,
|
||||
clientUrl: '/comments/' + misc.formatSearchQuery({page: '{page}'}),
|
||||
requestPage: pageController.createHistoryCacheProxy(
|
||||
requestPage: PageController.createHistoryCacheProxy(
|
||||
ctx,
|
||||
page => {
|
||||
return api.get(
|
||||
|
@ -31,12 +21,18 @@ class CommentsController {
|
|||
`&page=${page}&pageSize=10&fields=` +
|
||||
'id,comments,commentCount,thumbnailUrl');
|
||||
}),
|
||||
pageRenderer: this._commentsPageView,
|
||||
pageContext: {
|
||||
canViewPosts: api.hasPrivilege('posts:view'),
|
||||
}
|
||||
pageRenderer: pageCtx => {
|
||||
Object.assign(pageCtx, {
|
||||
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); });
|
||||
};
|
||||
|
|
|
@ -1,35 +1,23 @@
|
|||
'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');
|
||||
|
||||
class HelpController {
|
||||
constructor() {
|
||||
this._helpView = new HelpView();
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
constructor(section, subsection) {
|
||||
topNavigation.activate('help');
|
||||
this._helpView = new HelpView(section, 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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
const router = require('../router.js');
|
||||
const TopNavigation = require('../models/top_navigation.js');
|
||||
const topNavigation = require('../models/top_navigation.js');
|
||||
|
||||
class HistoryController {
|
||||
registerRoutes() {
|
||||
router.enter(
|
||||
'/history',
|
||||
(ctx, next) => { this._listHistoryRoute(); });
|
||||
}
|
||||
|
||||
_listHistoryRoute() {
|
||||
TopNavigation.activate('');
|
||||
constructor() {
|
||||
topNavigation.activate('');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new HistoryController();
|
||||
module.exports = router => {
|
||||
router.enter('/history', (ctx, next) => {
|
||||
ctx.controller = new HistoryController();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,53 +1,49 @@
|
|||
'use strict';
|
||||
|
||||
const router = require('../router.js');
|
||||
const api = require('../api.js');
|
||||
const events = require('../events.js');
|
||||
const TopNavigation = require('../models/top_navigation.js');
|
||||
const config = require('../config.js');
|
||||
const topNavigation = require('../models/top_navigation.js');
|
||||
const HomeView = require('../views/home_view.js');
|
||||
const NotFoundView = require('../views/not_found_view.js');
|
||||
|
||||
class HomeController {
|
||||
constructor() {
|
||||
this._homeView = new HomeView();
|
||||
this._notFoundView = new NotFoundView();
|
||||
}
|
||||
topNavigation.activate('home');
|
||||
|
||||
registerRoutes() {
|
||||
router.enter(
|
||||
'/',
|
||||
(ctx, next) => { this._indexRoute(); });
|
||||
router.enter(
|
||||
'*',
|
||||
(ctx, next) => { this._notFoundRoute(ctx); });
|
||||
}
|
||||
|
||||
_indexRoute() {
|
||||
TopNavigation.activate('home');
|
||||
this._homeView = new HomeView({
|
||||
name: config.name,
|
||||
version: config.meta.version,
|
||||
buildDate: config.meta.buildDate,
|
||||
canListPosts: api.hasPrivilege('posts:list'),
|
||||
});
|
||||
|
||||
api.get('/info')
|
||||
.then(response => {
|
||||
this._homeView.render({
|
||||
canListPosts: api.hasPrivilege('posts:list'),
|
||||
this._homeView.setStats({
|
||||
diskUsage: response.diskUsage,
|
||||
postCount: response.postCount,
|
||||
});
|
||||
this._homeView.setFeaturedPost({
|
||||
featuredPost: response.featuredPost,
|
||||
featuringUser: response.featuringUser,
|
||||
featuringTime: response.featuringTime,
|
||||
});
|
||||
},
|
||||
response => {
|
||||
this._homeView.render({
|
||||
canListPosts: api.hasPrivilege('posts:list'),
|
||||
});
|
||||
events.notify(events.Error, response.description);
|
||||
this._homeView.showError(response.description);
|
||||
});
|
||||
}
|
||||
|
||||
_notFoundRoute(ctx) {
|
||||
TopNavigation.activate('');
|
||||
this._notFoundView.render({path: ctx.canonicalPath});
|
||||
showSuccess(message) {
|
||||
this._homeView.showSuccess(message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new HomeController();
|
||||
showError(message) {
|
||||
this._homeView.showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = router => {
|
||||
router.enter('/', (ctx, next) => {
|
||||
ctx.controller = new HomeController();
|
||||
});
|
||||
};
|
||||
|
|
17
client/js/controllers/not_found_controller.js
Normal file
17
client/js/controllers/not_found_controller.js
Normal 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);
|
||||
});
|
||||
};
|
|
@ -1,43 +1,35 @@
|
|||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const settings = require('../settings.js');
|
||||
const settings = require('../models/settings.js');
|
||||
const EndlessPageView = require('../views/endless_page_view.js');
|
||||
const ManualPageView = require('../views/manual_page_view.js');
|
||||
|
||||
class PageController {
|
||||
constructor() {
|
||||
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();
|
||||
|
||||
constructor(ctx) {
|
||||
const extendedContext = {
|
||||
clientUrl: ctx.clientUrl,
|
||||
searchQuery: ctx.searchQuery,
|
||||
};
|
||||
|
||||
ctx.headerContext = ctx.headerContext || {};
|
||||
ctx.pageContext = ctx.pageContext || {};
|
||||
Object.assign(ctx.headerContext, extendedContext);
|
||||
Object.assign(ctx.pageContext, extendedContext);
|
||||
this._pageView.render(ctx);
|
||||
ctx.headerContext = Object.assign({}, extendedContext);
|
||||
ctx.pageContext = Object.assign({}, extendedContext);
|
||||
|
||||
if (settings.get().endlessScroll) {
|
||||
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 => {
|
||||
if (routerCtx.state.response) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -52,10 +44,6 @@ class PageController {
|
|||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._pageView.unrender();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new PageController();
|
||||
module.exports = PageController;
|
||||
|
|
63
client/js/controllers/password_reset_controller.js
Normal file
63
client/js/controllers/password_reset_controller.js
Normal 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]);
|
||||
});
|
||||
};
|
|
@ -1,38 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
const router = require('../router.js');
|
||||
const api = require('../api.js');
|
||||
const events = require('../events.js');
|
||||
const settings = require('../settings.js');
|
||||
const settings = require('../models/settings.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 EmptyView = require('../views/empty_view.js');
|
||||
|
||||
class PostController {
|
||||
constructor() {
|
||||
this._postView = new PostView();
|
||||
this._emptyView = new EmptyView();
|
||||
}
|
||||
constructor(id, editMode) {
|
||||
topNavigation.activate('posts');
|
||||
|
||||
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([
|
||||
Post.get(id),
|
||||
api.get(`/post/${id}/around?fields=id&query=` +
|
||||
this._decorateSearchQuery('')),
|
||||
]).then(responses => {
|
||||
const [post, aroundResponse] = responses;
|
||||
this._postView.render({
|
||||
this._view = new PostView({
|
||||
post: post,
|
||||
editMode: editMode,
|
||||
nextPostId: aroundResponse.next ? aroundResponse.next.id : null,
|
||||
|
@ -42,13 +27,13 @@ class PostController {
|
|||
canCreateComments: api.hasPrivilege('comments:create'),
|
||||
});
|
||||
}, response => {
|
||||
this._emptyView.render();
|
||||
events.notify(events.Error, response.description);
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(response.description);
|
||||
});
|
||||
}
|
||||
|
||||
_decorateSearchQuery(text) {
|
||||
const browsingSettings = settings.getSettings();
|
||||
const browsingSettings = settings.get();
|
||||
let disabledSafety = [];
|
||||
for (let key of Object.keys(browsingSettings.listPosts)) {
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,35 +1,22 @@
|
|||
'use strict';
|
||||
|
||||
const router = require('../router.js');
|
||||
const api = require('../api.js');
|
||||
const settings = require('../settings.js');
|
||||
const settings = require('../models/settings.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 PostsPageView = require('../views/posts_page_view.js');
|
||||
|
||||
class PostListController {
|
||||
constructor() {
|
||||
this._postsHeaderView = new PostsHeaderView();
|
||||
this._postsPageView = new PostsPageView();
|
||||
}
|
||||
constructor(ctx) {
|
||||
topNavigation.activate('posts');
|
||||
|
||||
registerRoutes() {
|
||||
router.enter(
|
||||
'/posts/:query?',
|
||||
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
|
||||
(ctx, next) => { this._listPostsRoute(ctx); });
|
||||
}
|
||||
|
||||
_listPostsRoute(ctx) {
|
||||
TopNavigation.activate('posts');
|
||||
|
||||
pageController.run({
|
||||
this._pageController = new PageController({
|
||||
searchQuery: ctx.searchQuery,
|
||||
clientUrl: '/posts/' + misc.formatSearchQuery({
|
||||
text: ctx.searchQuery.text, page: '{page}'}),
|
||||
requestPage: pageController.createHistoryCacheProxy(
|
||||
requestPage: PageController.createHistoryCacheProxy(
|
||||
ctx,
|
||||
page => {
|
||||
const text
|
||||
|
@ -39,16 +26,20 @@ class PostListController {
|
|||
'&fields=id,type,tags,score,favoriteCount,' +
|
||||
'commentCount,thumbnailUrl');
|
||||
}),
|
||||
headerRenderer: this._postsHeaderView,
|
||||
pageRenderer: this._postsPageView,
|
||||
pageContext: {
|
||||
canViewPosts: api.hasPrivilege('posts:view'),
|
||||
}
|
||||
headerRenderer: headerCtx => {
|
||||
return new PostsHeaderView(headerCtx);
|
||||
},
|
||||
pageRenderer: pageCtx => {
|
||||
Object.assign(pageCtx, {
|
||||
canViewPosts: api.hasPrivilege('posts:view'),
|
||||
});
|
||||
return new PostsPageView(pageCtx);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_decorateSearchQuery(text) {
|
||||
const browsingSettings = settings.getSettings();
|
||||
const browsingSettings = settings.get();
|
||||
let disabledSafety = [];
|
||||
for (let key of Object.keys(browsingSettings.listPosts)) {
|
||||
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); });
|
||||
};
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
'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');
|
||||
|
||||
class PostUploadController {
|
||||
constructor() {
|
||||
topNavigation.activate('upload');
|
||||
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();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
'use strict';
|
||||
|
||||
const router = require('../router.js');
|
||||
const settings = require('../settings.js');
|
||||
const TopNavigation = require('../models/top_navigation.js');
|
||||
const settings = require('../models/settings.js');
|
||||
const topNavigation = require('../models/top_navigation.js');
|
||||
const SettingsView = require('../views/settings_view.js');
|
||||
|
||||
class SettingsController {
|
||||
constructor() {
|
||||
this._settingsView = new SettingsView();
|
||||
}
|
||||
|
||||
registerRoutes() {
|
||||
router.enter('/settings', (ctx, next) => { this._settingsRoute(); });
|
||||
}
|
||||
|
||||
_settingsRoute() {
|
||||
TopNavigation.activate('settings');
|
||||
this._settingsView.render({
|
||||
getSettings: () => settings.getSettings(),
|
||||
saveSettings: newSettings => settings.saveSettings(newSettings),
|
||||
topNavigation.activate('settings');
|
||||
this._view = new SettingsView({
|
||||
settings: settings.get(),
|
||||
});
|
||||
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();
|
||||
});
|
||||
};
|
||||
|
|
80
client/js/controllers/tag_categories_controller.js
Normal file
80
client/js/controllers/tag_categories_controller.js
Normal 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);
|
||||
});
|
||||
};
|
115
client/js/controllers/tag_controller.js
Normal file
115
client/js/controllers/tag_controller.js
Normal 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');
|
||||
});
|
||||
};
|
54
client/js/controllers/tag_list_controller.js
Normal file
54
client/js/controllers/tag_list_controller.js
Normal 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); });
|
||||
};
|
|
@ -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();
|
|
@ -1,70 +1,68 @@
|
|||
'use strict';
|
||||
|
||||
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 TopNavigation = require('../models/top_navigation.js');
|
||||
|
||||
class TopNavigationController {
|
||||
constructor() {
|
||||
this._topNavigationView = new TopNavigationView();
|
||||
|
||||
TopNavigation.addEventListener(
|
||||
topNavigation.addEventListener(
|
||||
'activate', e => this._evtActivate(e));
|
||||
|
||||
events.listen(
|
||||
events.Authentication,
|
||||
() => {
|
||||
this._render();
|
||||
return true;
|
||||
});
|
||||
api.addEventListener('login', e => this._evtAuthChange(e));
|
||||
api.addEventListener('logout', e => this._evtAuthChange(e));
|
||||
|
||||
this._render();
|
||||
}
|
||||
|
||||
_evtAuthChange(e) {
|
||||
this._render();
|
||||
}
|
||||
|
||||
_evtActivate(e) {
|
||||
this._topNavigationView.activate(e.key);
|
||||
}
|
||||
|
||||
_updateNavigationFromPrivileges() {
|
||||
TopNavigation.get('account').url = '/user/' + api.userName;
|
||||
TopNavigation.get('account').imageUrl =
|
||||
topNavigation.get('account').url = '/user/' + api.userName;
|
||||
topNavigation.get('account').imageUrl =
|
||||
api.user ? api.user.avatarUrl : null;
|
||||
|
||||
TopNavigation.showAll();
|
||||
topNavigation.showAll();
|
||||
if (!api.hasPrivilege('posts:list')) {
|
||||
TopNavigation.hide('posts');
|
||||
topNavigation.hide('posts');
|
||||
}
|
||||
if (!api.hasPrivilege('posts:create')) {
|
||||
TopNavigation.hide('upload');
|
||||
topNavigation.hide('upload');
|
||||
}
|
||||
if (!api.hasPrivilege('comments:list')) {
|
||||
TopNavigation.hide('comments');
|
||||
topNavigation.hide('comments');
|
||||
}
|
||||
if (!api.hasPrivilege('tags:list')) {
|
||||
TopNavigation.hide('tags');
|
||||
topNavigation.hide('tags');
|
||||
}
|
||||
if (!api.hasPrivilege('users:list')) {
|
||||
TopNavigation.hide('users');
|
||||
topNavigation.hide('users');
|
||||
}
|
||||
if (api.isLoggedIn()) {
|
||||
TopNavigation.hide('register');
|
||||
TopNavigation.hide('login');
|
||||
topNavigation.hide('register');
|
||||
topNavigation.hide('login');
|
||||
} else {
|
||||
TopNavigation.hide('account');
|
||||
TopNavigation.hide('logout');
|
||||
topNavigation.hide('account');
|
||||
topNavigation.hide('logout');
|
||||
}
|
||||
}
|
||||
|
||||
_render() {
|
||||
this._updateNavigationFromPrivileges();
|
||||
console.log(TopNavigation.getAll());
|
||||
this._topNavigationView.render({
|
||||
items: TopNavigation.getAll(),
|
||||
items: topNavigation.getAll(),
|
||||
});
|
||||
this._topNavigationView.activate(
|
||||
TopNavigation.activeItem ? TopNavigation.activeItem.key : '');
|
||||
};
|
||||
topNavigation.activeItem ? topNavigation.activeItem.key : '');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new TopNavigationController();
|
||||
|
|
166
client/js/controllers/user_controller.js
Normal file
166
client/js/controllers/user_controller.js
Normal 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');
|
||||
});
|
||||
};
|
47
client/js/controllers/user_list_controller.js
Normal file
47
client/js/controllers/user_list_controller.js
Normal 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); });
|
||||
};
|
41
client/js/controllers/user_registration_controller.js
Normal file
41
client/js/controllers/user_registration_controller.js
Normal 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();
|
||||
});
|
||||
};
|
|
@ -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();
|
|
@ -25,7 +25,7 @@ class CommentControl {
|
|||
canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`),
|
||||
});
|
||||
|
||||
views.showView(
|
||||
views.replaceContent(
|
||||
sourceNode.querySelector('.score-container'),
|
||||
this._scoreTemplate({
|
||||
score: this._comment.score,
|
||||
|
@ -77,7 +77,7 @@ class CommentControl {
|
|||
canCancel: true
|
||||
});
|
||||
|
||||
views.showView(this._hostNode, sourceNode);
|
||||
views.replaceContent(this._hostNode, sourceNode);
|
||||
}
|
||||
|
||||
_evtScoreClick(e, scoreGetter) {
|
||||
|
|
|
@ -47,7 +47,7 @@ class CommentFormControl {
|
|||
this._growTextArea();
|
||||
});
|
||||
|
||||
views.showView(this._hostNode, sourceNode);
|
||||
views.replaceContent(this._hostNode, sourceNode);
|
||||
}
|
||||
|
||||
enterEditMode() {
|
||||
|
|
|
@ -19,7 +19,7 @@ class CommentListControl {
|
|||
canListComments: api.hasPrivilege('comments:list'),
|
||||
});
|
||||
|
||||
views.showView(this._hostNode, sourceNode);
|
||||
views.replaceContent(this._hostNode, sourceNode);
|
||||
|
||||
this._renderComments();
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ class CommentListControl {
|
|||
});
|
||||
commentList.appendChild(commentListItemNode);
|
||||
}
|
||||
views.showView(this._hostNode.querySelector('ul'), commentList);
|
||||
views.replaceContent(this._hostNode.querySelector('ul'), commentList);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ class FileDropperControl {
|
|||
this._fileInputNode.addEventListener(
|
||||
'change', e => this._evtFileChange(e));
|
||||
|
||||
views.showView(target, source);
|
||||
views.replaceContent(target, source);
|
||||
}
|
||||
|
||||
_resolve(files) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const settings = require('../settings.js');
|
||||
const settings = require('../models/settings.js');
|
||||
const views = require('../util/views.js');
|
||||
const optimizedResize = require('../util/optimized_resize.js');
|
||||
|
||||
|
@ -21,7 +21,7 @@ class PostContentControl {
|
|||
this._currentFitFunction = this.fitWidth;
|
||||
const mul = this._post.canvasHeight / this._post.canvasWidth;
|
||||
let width = this._viewportWidth;
|
||||
if (!settings.getSettings().upscaleSmallPosts) {
|
||||
if (!settings.get().upscaleSmallPosts) {
|
||||
width = Math.min(this._post.canvasWidth, width);
|
||||
}
|
||||
this._resize(width, width * mul);
|
||||
|
@ -31,7 +31,7 @@ class PostContentControl {
|
|||
this._currentFitFunction = this.fitHeight;
|
||||
const mul = this._post.canvasWidth / this._post.canvasHeight;
|
||||
let height = this._viewportHeight;
|
||||
if (!settings.getSettings().upscaleSmallPosts) {
|
||||
if (!settings.get().upscaleSmallPosts) {
|
||||
height = Math.min(this._post.canvasHeight, height);
|
||||
}
|
||||
this._resize(height * mul, height);
|
||||
|
@ -42,13 +42,13 @@ class PostContentControl {
|
|||
let mul = this._post.canvasHeight / this._post.canvasWidth;
|
||||
if (this._viewportWidth * mul < this._viewportHeight) {
|
||||
let width = this._viewportWidth;
|
||||
if (!settings.getSettings().upscaleSmallPosts) {
|
||||
if (!settings.get().upscaleSmallPosts) {
|
||||
width = Math.min(this._post.canvasWidth, width);
|
||||
}
|
||||
this._resize(width, width * mul);
|
||||
} else {
|
||||
let height = this._viewportHeight;
|
||||
if (!settings.getSettings().upscaleSmallPosts) {
|
||||
if (!settings.get().upscaleSmallPosts) {
|
||||
height = Math.min(this._post.canvasHeight, height);
|
||||
}
|
||||
this._resize(height / mul, height);
|
||||
|
@ -83,7 +83,7 @@ class PostContentControl {
|
|||
const postContentNode = this._template({
|
||||
post: this._post,
|
||||
});
|
||||
if (settings.getSettings().transparencyGrid) {
|
||||
if (settings.get().transparencyGrid) {
|
||||
postContentNode.classList.add('transparency-grid');
|
||||
}
|
||||
this._containerNode.appendChild(postContentNode);
|
||||
|
|
|
@ -16,7 +16,7 @@ class PostEditSidebarControl {
|
|||
const sourceNode = this._template({
|
||||
post: this._post,
|
||||
});
|
||||
views.showView(this._hostNode, sourceNode);
|
||||
views.replaceContent(this._hostNode, sourceNode);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ class PostReadonlySidebarControl {
|
|||
canViewTags: api.hasPrivilege('tags:view'),
|
||||
});
|
||||
|
||||
views.showView(
|
||||
views.replaceContent(
|
||||
sourceNode.querySelector('.score-container'),
|
||||
this._scoreTemplate({
|
||||
score: this._post.score,
|
||||
|
@ -33,7 +33,7 @@ class PostReadonlySidebarControl {
|
|||
canScore: api.hasPrivilege('posts:score'),
|
||||
}));
|
||||
|
||||
views.showView(
|
||||
views.replaceContent(
|
||||
sourceNode.querySelector('.fav-container'),
|
||||
this._favTemplate({
|
||||
favoriteCount: this._post.favoriteCount,
|
||||
|
@ -85,7 +85,7 @@ class PostReadonlySidebarControl {
|
|||
'click', this._eventZoomProxy(
|
||||
() => this._postContentControl.fitHeight()));
|
||||
|
||||
views.showView(this._hostNode, sourceNode);
|
||||
views.replaceContent(this._hostNode, sourceNode);
|
||||
|
||||
this._syncFitButton();
|
||||
}
|
||||
|
|
|
@ -1,41 +1,5 @@
|
|||
'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 {
|
||||
constructor() {
|
||||
this.eventTarget = document.createDocumentFragment();
|
||||
|
@ -53,12 +17,6 @@ module.exports = {
|
|||
Success: 'success',
|
||||
Error: 'error',
|
||||
Info: 'info',
|
||||
Authentication: 'auth',
|
||||
SettingsChange: 'settings-change',
|
||||
TagsChange: 'tags-change',
|
||||
|
||||
notify: notify,
|
||||
listen: listen,
|
||||
unlisten: unlisten,
|
||||
EventTarget: EventTarget,
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require('./util/polyfill.js');
|
||||
const misc = require('./util/misc.js');
|
||||
|
||||
const views = require('./util/views.js');
|
||||
const router = require('./router.js');
|
||||
|
||||
history.scrollRestoration = 'manual';
|
||||
|
@ -13,7 +13,6 @@ router.exit(
|
|||
ctx.state.scrollX = window.scrollX;
|
||||
ctx.state.scrollY = window.scrollY;
|
||||
ctx.save();
|
||||
views.unlistenToMessages();
|
||||
if (misc.confirmPageExit()) {
|
||||
next();
|
||||
}
|
||||
|
@ -33,28 +32,33 @@ router.enter(
|
|||
});
|
||||
});
|
||||
|
||||
// register controller routes
|
||||
let controllers = [];
|
||||
controllers.push(require('./controllers/auth_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/home_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/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/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
|
||||
controllers.push(require('./controllers/home_controller.js'));
|
||||
// 404 controller needs to be registered last
|
||||
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) {
|
||||
controller.registerRoutes();
|
||||
controller(router);
|
||||
}
|
||||
|
||||
const tags = require('./tags.js');
|
||||
const api = require('./api.js');
|
||||
Promise.all([tags.refreshExport(), api.loginFromCookies()])
|
||||
.then(() => {
|
||||
|
@ -64,9 +68,8 @@ Promise.all([tags.refreshExport(), api.loginFromCookies()])
|
|||
api.forget();
|
||||
router.start();
|
||||
} else {
|
||||
router.start('/');
|
||||
events.notify(
|
||||
events.Error,
|
||||
const ctx = router.start('/');
|
||||
ctx.controller.showError(
|
||||
'An error happened while trying to log you in: ' +
|
||||
errorMessage);
|
||||
}
|
||||
|
|
40
client/js/models/settings.js
Normal file
40
client/js/models/settings.js
Normal 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();
|
|
@ -42,15 +42,13 @@ class TopNavigation extends events.EventTarget {
|
|||
}
|
||||
|
||||
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.dispatchEvent(event);
|
||||
this.dispatchEvent(new CustomEvent('activate', {
|
||||
detail: {
|
||||
key: key,
|
||||
item: key ? this.get(key) : null,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
showAll() {
|
||||
|
|
|
@ -120,7 +120,7 @@ class Router {
|
|||
window.addEventListener('popstate', this._onPopState, false);
|
||||
document.addEventListener(clickEvent, this._onClick, false);
|
||||
const url = location.pathname + location.search + location.hash;
|
||||
this.replace(url, null, true);
|
||||
return this.replace(url, null, true);
|
||||
}
|
||||
|
||||
stop() {
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -1,7 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const request = require('superagent');
|
||||
const events = require('./events.js');
|
||||
|
||||
let _tags = null;
|
||||
let _categories = null;
|
||||
|
@ -88,10 +87,6 @@ function refreshExport() {
|
|||
});
|
||||
}
|
||||
|
||||
events.listen(
|
||||
events.TagsChange,
|
||||
() => { refreshExport(); return true; });
|
||||
|
||||
module.exports = {
|
||||
getAllCategories: getAllCategories,
|
||||
getAllTags: getAllTags,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
const mousetrap = require('mousetrap');
|
||||
const settings = require('../settings.js');
|
||||
const settings = require('../models/settings.js');
|
||||
|
||||
function bind(hotkey, func) {
|
||||
if (settings.getSettings().keyboardShortcuts) {
|
||||
if (settings.get().keyboardShortcuts) {
|
||||
mousetrap.bind(hotkey, func);
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ require('../util/polyfill.js');
|
|||
const api = require('../api.js');
|
||||
const templates = require('../templates.js');
|
||||
const tags = require('../tags.js');
|
||||
const events = require('../events.js');
|
||||
const domParser = new DOMParser();
|
||||
const misc = require('./misc.js');
|
||||
|
||||
|
@ -238,26 +237,6 @@ function showInfo(target, message) {
|
|||
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) {
|
||||
const messagesHolder = target.querySelector('.messages');
|
||||
/* TODO: animate that */
|
||||
|
@ -335,7 +314,7 @@ function enableForm(form) {
|
|||
}
|
||||
}
|
||||
|
||||
function showView(target, source) {
|
||||
function replaceContent(target, source) {
|
||||
while (target.lastChild) {
|
||||
target.removeChild(target.lastChild);
|
||||
}
|
||||
|
@ -424,11 +403,9 @@ document.addEventListener('input', e => {
|
|||
module.exports = {
|
||||
htmlToDom: htmlToDom,
|
||||
getTemplate: getTemplate,
|
||||
showView: showView,
|
||||
replaceContent: replaceContent,
|
||||
enableForm: enableForm,
|
||||
disableForm: disableForm,
|
||||
listenToMessages: listenToMessages,
|
||||
unlistenToMessages: unlistenToMessages,
|
||||
clearMessages: clearMessages,
|
||||
decorateValidator: decorateValidator,
|
||||
makeVoidElement: makeVoidElement,
|
||||
|
|
|
@ -3,24 +3,25 @@
|
|||
const views = require('../util/views.js');
|
||||
const CommentListControl = require('../controls/comment_list_control.js');
|
||||
|
||||
class CommentsPageView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('comments-page');
|
||||
}
|
||||
const template = views.getTemplate('comments-page');
|
||||
|
||||
render(ctx) {
|
||||
const target = ctx.target;
|
||||
const source = this._template(ctx);
|
||||
class CommentsPageView {
|
||||
constructor(ctx) {
|
||||
this._hostNode = ctx.hostNode;
|
||||
this._controls = [];
|
||||
|
||||
const sourceNode = template(ctx);
|
||||
|
||||
for (let post of ctx.results) {
|
||||
post.comments.sort((a, b) => { return b.id - a.id; });
|
||||
new CommentListControl(
|
||||
source.querySelector(
|
||||
`.comments-container[data-for="${post.id}"]`),
|
||||
post.comments);
|
||||
this._controls.push(
|
||||
new CommentListControl(
|
||||
sourceNode.querySelector(
|
||||
`.comments-container[data-for="${post.id}"]`),
|
||||
post.comments));
|
||||
}
|
||||
|
||||
views.showView(target, source);
|
||||
views.replaceContent(this._hostNode, sourceNode);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,19 +2,19 @@
|
|||
|
||||
const views = require('../util/views.js');
|
||||
|
||||
const template = () => {
|
||||
return views.htmlToDom(
|
||||
'<div class="wrapper"><div class="messages"></div></div>');
|
||||
};
|
||||
|
||||
class EmptyView {
|
||||
constructor() {
|
||||
this._template = () => {
|
||||
return views.htmlToDom(
|
||||
'<div class="wrapper"><div class="messages"></div></div>');
|
||||
};
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
views.replaceContent(this._hostNode, template());
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this._template();
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
showError(message) {
|
||||
views.showError(this._hostNode, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,55 +1,43 @@
|
|||
'use strict';
|
||||
|
||||
const router = require('../router.js');
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
const holderTemplate = views.getTemplate('endless-pager');
|
||||
const pageTemplate = views.getTemplate('endless-pager-page');
|
||||
|
||||
function _formatUrl(url, page) {
|
||||
return url.replace('{page}', page);
|
||||
}
|
||||
|
||||
class EndlessPageView {
|
||||
constructor() {
|
||||
this._holderTemplate = views.getTemplate('endless-pager');
|
||||
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);
|
||||
constructor(ctx) {
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
this._active = true;
|
||||
this._working = 0;
|
||||
this._init = true;
|
||||
|
||||
ctx.headerContext.target = pageHeaderHolder;
|
||||
if (ctx.headerRenderer) {
|
||||
ctx.headerRenderer.render(ctx.headerContext);
|
||||
}
|
||||
|
||||
this.threshold = window.innerHeight / 3;
|
||||
this.minPageShown = null;
|
||||
this.maxPageShown = null;
|
||||
this.totalPages = 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);
|
||||
window.addEventListener('unload', this._scrollToTop, true);
|
||||
this._probePageLoad(ctx);
|
||||
}
|
||||
|
||||
unrender() {
|
||||
this._active = false;
|
||||
window.removeEventListener('unload', this._scrollToTop, true);
|
||||
}
|
||||
|
||||
_scrollToTop() {
|
||||
window.scroll(0, 0);
|
||||
}
|
||||
|
||||
_probePageLoad(ctx) {
|
||||
if (this._active) {
|
||||
window.setTimeout(() => {
|
||||
|
@ -115,23 +103,23 @@ class EndlessPageView {
|
|||
this._working--;
|
||||
});
|
||||
}, response => {
|
||||
events.notify(events.Error, response.description);
|
||||
this.showError(response.description);
|
||||
this._working--;
|
||||
});
|
||||
}
|
||||
|
||||
_renderPage(ctx, pageNumber, append, response) {
|
||||
if (response.total) {
|
||||
const pageNode = this._pageTemplate({
|
||||
const pageNode = pageTemplate({
|
||||
page: pageNumber,
|
||||
totalPages: this.totalPages,
|
||||
});
|
||||
pageNode.setAttribute('data-page', pageNumber);
|
||||
|
||||
Object.assign(ctx.pageContext, response);
|
||||
ctx.pageContext.target = pageNode.querySelector(
|
||||
ctx.pageContext.hostNode = pageNode.querySelector(
|
||||
'.page-content-holder');
|
||||
ctx.pageRenderer.render(ctx.pageContext);
|
||||
ctx.pageRenderer(ctx.pageContext);
|
||||
|
||||
if (pageNumber < this.minPageShown ||
|
||||
this.minPageShown === null) {
|
||||
|
@ -143,22 +131,34 @@ class EndlessPageView {
|
|||
}
|
||||
|
||||
if (append) {
|
||||
this._pagesHolder.appendChild(pageNode);
|
||||
/*if (this._init && pageNumber !== 1) {
|
||||
this._pagesHolderNode.appendChild(pageNode);
|
||||
if (this._init && pageNumber !== 1) {
|
||||
window.scroll(0, pageNode.getBoundingClientRect().top);
|
||||
}*/
|
||||
}
|
||||
} else {
|
||||
this._pagesHolder.prependChild(pageNode);
|
||||
this._pagesHolderNode.prependChild(pageNode);
|
||||
|
||||
window.scroll(
|
||||
window.scrollX,
|
||||
window.scrollY + pageNode.offsetHeight);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
views.showSuccess(this._hostNode, message);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
views.showError(this._hostNode, message);
|
||||
}
|
||||
|
||||
showInfo(message) {
|
||||
views.showInfo(this._hostNode, message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EndlessPageView;
|
||||
|
|
|
@ -3,67 +3,62 @@
|
|||
const config = require('../config.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 {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('help');
|
||||
this._sectionTemplates = {};
|
||||
const sectionKeys = ['about', 'keyboard', 'search', 'comments', 'tos'];
|
||||
for (let section of sectionKeys) {
|
||||
const templateName = 'help-' + section;
|
||||
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'),
|
||||
}
|
||||
constructor(section, subsection) {
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
|
||||
const sourceNode = template();
|
||||
const ctx = {
|
||||
name: config.name,
|
||||
};
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this._template();
|
||||
|
||||
ctx.section = ctx.section || 'about';
|
||||
if (ctx.section in this._sectionTemplates) {
|
||||
views.showView(
|
||||
source.querySelector('.content'),
|
||||
this._sectionTemplates[ctx.section]({
|
||||
name: config.name,
|
||||
}));
|
||||
section = section || 'about';
|
||||
if (section in sectionTemplates) {
|
||||
views.replaceContent(
|
||||
sourceNode.querySelector('.content'),
|
||||
sectionTemplates[section](ctx));
|
||||
}
|
||||
|
||||
ctx.subsection = ctx.subsection || 'default';
|
||||
if (ctx.section in this._subsectionTemplates &&
|
||||
ctx.subsection in this._subsectionTemplates[ctx.section]) {
|
||||
views.showView(
|
||||
source.querySelector('.subcontent'),
|
||||
this._subsectionTemplates[ctx.section][ctx.subsection]({
|
||||
name: config.name,
|
||||
}));
|
||||
subsection = subsection || 'default';
|
||||
if (section in subsectionTemplates &&
|
||||
subsection in subsectionTemplates[section]) {
|
||||
views.replaceContent(
|
||||
sourceNode.querySelector('.subcontent'),
|
||||
subsectionTemplates[section][subsection](ctx));
|
||||
}
|
||||
|
||||
for (let item of source.querySelectorAll('.primary [data-name]')) {
|
||||
if (item.getAttribute('data-name') === ctx.section) {
|
||||
item.className = 'active';
|
||||
} else {
|
||||
item.className = '';
|
||||
}
|
||||
for (let itemNode of
|
||||
sourceNode.querySelectorAll('.primary [data-name]')) {
|
||||
itemNode.classList.toggle(
|
||||
'active',
|
||||
itemNode.getAttribute('data-name') === section);
|
||||
}
|
||||
|
||||
for (let item of source.querySelectorAll('.secondary [data-name]')) {
|
||||
if (item.getAttribute('data-name') === ctx.subsection) {
|
||||
item.className = 'active';
|
||||
} else {
|
||||
item.className = '';
|
||||
}
|
||||
for (let itemNode of
|
||||
sourceNode.querySelectorAll('.secondary [data-name]')) {
|
||||
itemNode.classList.toggle(
|
||||
'active',
|
||||
itemNode.getAttribute('data-name') === subsection);
|
||||
}
|
||||
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
|
||||
views.replaceContent(this._hostNode, sourceNode);
|
||||
views.scrollToHash();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const router = require('../router.js');
|
||||
const config = require('../config.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const views = require('../util/views.js');
|
||||
const PostContentControl = require('../controls/post_content_control.js');
|
||||
|
@ -10,48 +9,45 @@ const PostNotesOverlayControl
|
|||
const TagAutoCompleteControl =
|
||||
require('../controls/tag_auto_complete_control.js');
|
||||
|
||||
const template = views.getTemplate('home');
|
||||
const statsTemplate = views.getTemplate('home-stats');
|
||||
|
||||
class HomeView {
|
||||
constructor() {
|
||||
this._homeTemplate = views.getTemplate('home');
|
||||
}
|
||||
constructor(ctx) {
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
|
||||
render(ctx) {
|
||||
Object.assign(ctx, {
|
||||
name: config.name,
|
||||
version: config.meta.version,
|
||||
buildDate: config.meta.buildDate,
|
||||
});
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this._homeTemplate(ctx);
|
||||
const sourceNode = template(ctx);
|
||||
views.replaceContent(this._hostNode, sourceNode);
|
||||
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
if (this._formNode) {
|
||||
this._formNode.querySelector('input[name=all-posts')
|
||||
.addEventListener('click', e => this._evtAllPostsClick(e));
|
||||
|
||||
const form = source.querySelector('form');
|
||||
if (form) {
|
||||
form.querySelector('input[name=all-posts')
|
||||
.addEventListener('click', 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}));
|
||||
});
|
||||
this._tagAutoCompleteControl = new TagAutoCompleteControl(
|
||||
this._searchInputNode);
|
||||
this._formNode.addEventListener(
|
||||
'submit', e => this._evtFormSubmit(e));
|
||||
}
|
||||
|
||||
const postContainerNode = source.querySelector('.post-container');
|
||||
}
|
||||
|
||||
if (postContainerNode && ctx.featuredPost) {
|
||||
new PostContentControl(
|
||||
postContainerNode,
|
||||
ctx.featuredPost,
|
||||
showSuccess(text) {
|
||||
views.showSuccess(this._hostNode, text);
|
||||
}
|
||||
|
||||
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 [
|
||||
window.innerWidth * 0.8,
|
||||
|
@ -59,11 +55,39 @@ class HomeView {
|
|||
];
|
||||
});
|
||||
|
||||
new PostNotesOverlayControl(
|
||||
postContainerNode.querySelector('.post-overlay'),
|
||||
ctx.featuredPost);
|
||||
this._postNotesOverlay = new PostNotesOverlayControl(
|
||||
this._postContainerNode.querySelector('.post-overlay'),
|
||||
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;
|
||||
|
|
|
@ -1,43 +1,67 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../config.js');
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class LoginView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('login');
|
||||
}
|
||||
const template = views.getTemplate('login');
|
||||
|
||||
render(ctx) {
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this._template({
|
||||
class LoginView extends events.EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
|
||||
views.replaceContent(this._hostNode, template({
|
||||
userNamePattern: config.userNameRegex,
|
||||
passwordPattern: config.passwordRegex,
|
||||
canSendMails: config.canSendMails,
|
||||
});
|
||||
}));
|
||||
|
||||
const form = source.querySelector('form');
|
||||
const userNameField = source.querySelector('#user-name');
|
||||
const passwordField = source.querySelector('#user-password');
|
||||
const rememberUserField = source.querySelector('#remember-user');
|
||||
|
||||
views.decorateValidator(form);
|
||||
userNameField.setAttribute('pattern', config.userNameRegex);
|
||||
passwordField.setAttribute('pattern', config.passwordRegex);
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
views.decorateValidator(this._formNode);
|
||||
this._userNameFieldNode.setAttribute('pattern', config.userNameRegex);
|
||||
this._passwordFieldNode.setAttribute('pattern', config.passwordRegex);
|
||||
this._formNode.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
views.clearMessages(target);
|
||||
views.disableForm(form);
|
||||
ctx.login(
|
||||
userNameField.value,
|
||||
passwordField.value,
|
||||
rememberUserField.checked)
|
||||
.always(() => { views.enableForm(form); });
|
||||
this.dispatchEvent(new CustomEvent('submit', {
|
||||
detail: {
|
||||
name: this._userNameFieldNode.value,
|
||||
password: this._passwordFieldNode.value,
|
||||
remember: this._rememberFieldNode.checked,
|
||||
},
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
get _formNode() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
const router = require('../router.js');
|
||||
const events = require('../events.js');
|
||||
const keyboard = require('../util/keyboard.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
const holderTemplate = views.getTemplate('manual-pager');
|
||||
const navTemplate = views.getTemplate('manual-pager-nav');
|
||||
|
||||
function _formatUrl(url, page) {
|
||||
return url.replace('{page}', page);
|
||||
}
|
||||
|
@ -56,28 +58,28 @@ function _getPages(currentPage, pageNumbers, clientUrl) {
|
|||
}
|
||||
|
||||
class ManualPageView {
|
||||
constructor() {
|
||||
this._holderTemplate = views.getTemplate('manual-pager');
|
||||
this._navTemplate = views.getTemplate('manual-pager-nav');
|
||||
}
|
||||
constructor(ctx) {
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
|
||||
render(ctx) {
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this._holderTemplate();
|
||||
const pageContentHolder = source.querySelector('.page-content-holder');
|
||||
const pageHeaderHolder = source.querySelector('.page-header-holder');
|
||||
const pageNav = source.querySelector('.page-nav');
|
||||
const sourceNode = holderTemplate();
|
||||
const pageContentHolderNode
|
||||
= sourceNode.querySelector('.page-content-holder');
|
||||
const pageHeaderHolderNode
|
||||
= sourceNode.querySelector('.page-header-holder');
|
||||
const pageNavNode = sourceNode.querySelector('.page-nav');
|
||||
const currentPage = ctx.searchQuery.page;
|
||||
|
||||
ctx.headerContext.target = pageHeaderHolder;
|
||||
ctx.headerContext.hostNode = pageHeaderHolderNode;
|
||||
if (ctx.headerRenderer) {
|
||||
ctx.headerRenderer.render(ctx.headerContext);
|
||||
ctx.headerRenderer(ctx.headerContext);
|
||||
}
|
||||
|
||||
views.replaceContent(this._hostNode, sourceNode);
|
||||
|
||||
ctx.requestPage(currentPage).then(response => {
|
||||
Object.assign(ctx.pageContext, response);
|
||||
ctx.pageContext.target = pageContentHolder;
|
||||
ctx.pageRenderer.render(ctx.pageContext);
|
||||
ctx.pageContext.hostNode = pageContentHolderNode;
|
||||
ctx.pageRenderer(ctx.pageContext);
|
||||
|
||||
const totalPages = Math.ceil(response.total / response.pageSize);
|
||||
const pageNumbers = _getVisiblePageNumbers(currentPage, totalPages);
|
||||
|
@ -95,28 +97,35 @@ class ManualPageView {
|
|||
});
|
||||
|
||||
if (response.total) {
|
||||
views.showView(pageNav, this._navTemplate({
|
||||
prevLink: _formatUrl(ctx.clientUrl, currentPage - 1),
|
||||
nextLink: _formatUrl(ctx.clientUrl, currentPage + 1),
|
||||
prevLinkActive: currentPage > 1,
|
||||
nextLinkActive: currentPage < totalPages,
|
||||
pages: pages,
|
||||
}));
|
||||
views.replaceContent(
|
||||
pageNavNode,
|
||||
navTemplate({
|
||||
prevLink: _formatUrl(ctx.clientUrl, currentPage - 1),
|
||||
nextLink: _formatUrl(ctx.clientUrl, currentPage + 1),
|
||||
prevLinkActive: currentPage > 1,
|
||||
nextLinkActive: currentPage < totalPages,
|
||||
pages: pages,
|
||||
}));
|
||||
}
|
||||
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
if (response.total <= (currentPage - 1) * response.pageSize) {
|
||||
events.notify(events.Info, 'No data to show');
|
||||
this.showInfo('No data to show');
|
||||
}
|
||||
}, response => {
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
events.notify(events.Error, response.description);
|
||||
this.showError(response.description);
|
||||
});
|
||||
}
|
||||
|
||||
unrender() {
|
||||
showSuccess(message) {
|
||||
views.showSuccess(this._hostNode, message);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
views.showError(this._hostNode, message);
|
||||
}
|
||||
|
||||
showInfo(message) {
|
||||
views.showInfo(this._hostNode, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,15 +3,14 @@
|
|||
const config = require('../config.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class NotFoundView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('not-found');
|
||||
}
|
||||
const template = views.getTemplate('not-found');
|
||||
|
||||
render(ctx) {
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this._template(ctx);
|
||||
views.showView(target, source);
|
||||
class NotFoundView {
|
||||
constructor(path) {
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
|
||||
const sourceNode = template({path: path});
|
||||
views.replaceContent(this._hostNode, sourceNode);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,31 +1,54 @@
|
|||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class PasswordResetView {
|
||||
const template = views.getTemplate('password-reset');
|
||||
|
||||
class PasswordResetView extends events.EventTarget {
|
||||
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) {
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this._template();
|
||||
showSuccess(message) {
|
||||
views.showSuccess(this._hostNode, message);
|
||||
}
|
||||
|
||||
const form = source.querySelector('form');
|
||||
const userNameOrEmailField = source.querySelector('#user-name');
|
||||
showError(message) {
|
||||
views.showError(this._hostNode, message);
|
||||
}
|
||||
|
||||
views.decorateValidator(form);
|
||||
clearMessages() {
|
||||
views.clearMessages(this._hostNode);
|
||||
}
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
views.clearMessages(target);
|
||||
views.disableForm(form);
|
||||
ctx.proceed(userNameOrEmailField.value)
|
||||
.catch(() => { views.enableForm(form); });
|
||||
});
|
||||
enableForm() {
|
||||
views.enableForm(this._formNode);
|
||||
}
|
||||
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
disableForm() {
|
||||
views.disableForm(this._formNode);
|
||||
}
|
||||
|
||||
get _formNode() {
|
||||
return this._hostNode.querySelector('form');
|
||||
}
|
||||
|
||||
get _userNameOrEmailFieldNode() {
|
||||
return this._formNode.querySelector('#user-name');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,20 +14,16 @@ const PostEditSidebarControl =
|
|||
const CommentListControl = require('../controls/comment_list_control.js');
|
||||
const CommentFormControl = require('../controls/comment_form_control.js');
|
||||
|
||||
const template = views.getTemplate('post');
|
||||
|
||||
class PostView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('post');
|
||||
}
|
||||
constructor(ctx) {
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
|
||||
render(ctx) {
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this._template(ctx);
|
||||
|
||||
const postContainerNode = source.querySelector('.post-container');
|
||||
const sidebarNode = source.querySelector('.sidebar');
|
||||
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
const sourceNode = template(ctx);
|
||||
const postContainerNode = sourceNode.querySelector('.post-container');
|
||||
const sidebarNode = sourceNode.querySelector('.sidebar');
|
||||
views.replaceContent(this._hostNode, sourceNode);
|
||||
|
||||
const postViewNode = document.body.querySelector('.content-wrapper');
|
||||
const topNavigationNode =
|
||||
|
|
|
@ -1,33 +1,27 @@
|
|||
'use strict';
|
||||
|
||||
const router = require('../router.js');
|
||||
const settings = require('../settings.js');
|
||||
const settings = require('../models/settings.js');
|
||||
const keyboard = require('../util/keyboard.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const views = require('../util/views.js');
|
||||
const TagAutoCompleteControl =
|
||||
require('../controls/tag_auto_complete_control.js');
|
||||
|
||||
const template = views.getTemplate('posts-header');
|
||||
|
||||
class PostsHeaderView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('posts-header');
|
||||
}
|
||||
constructor(ctx) {
|
||||
ctx.settings = settings.get();
|
||||
this._hostNode = ctx.hostNode;
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
|
||||
render(ctx) {
|
||||
ctx.settings = settings.getSettings();
|
||||
|
||||
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);
|
||||
if (this._queryInputNode) {
|
||||
new TagAutoCompleteControl(this._queryInputNode);
|
||||
}
|
||||
|
||||
keyboard.bind('q', () => {
|
||||
form.querySelector('input').focus();
|
||||
this._formNode.querySelector('input').focus();
|
||||
});
|
||||
|
||||
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(
|
||||
'click', e => this._evtSafetyButtonClick(e, ctx.clientUrl));
|
||||
}
|
||||
form.addEventListener(
|
||||
'submit', e => this._evtFormSubmit(e, searchTextInput));
|
||||
this._formNode.addEventListener(
|
||||
'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) {
|
||||
e.preventDefault();
|
||||
e.target.classList.toggle('disabled');
|
||||
const safety = e.target.getAttribute('data-safety');
|
||||
let browsingSettings = settings.getSettings();
|
||||
let browsingSettings = settings.get();
|
||||
browsingSettings.listPosts[safety] =
|
||||
!browsingSettings.listPosts[safety];
|
||||
settings.saveSettings(browsingSettings, true);
|
||||
settings.save(browsingSettings, true);
|
||||
router.show(url.replace(/{page}/, 1));
|
||||
}
|
||||
|
||||
_evtFormSubmit(e, searchTextInput) {
|
||||
_evtFormSubmit(e, queryInputNode) {
|
||||
e.preventDefault();
|
||||
const text = searchTextInput.value;
|
||||
searchTextInput.blur();
|
||||
const text = queryInputNode.value;
|
||||
queryInputNode.blur();
|
||||
router.show('/posts/' + misc.formatSearchQuery({text: text}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,11 @@
|
|||
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class PostsPageView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('posts-page');
|
||||
}
|
||||
const template = views.getTemplate('posts-page');
|
||||
|
||||
render(ctx) {
|
||||
const target = ctx.target;
|
||||
const source = this._template(ctx);
|
||||
views.showView(target, source);
|
||||
class PostsPageView {
|
||||
constructor(ctx) {
|
||||
views.replaceContent(ctx.hostNode, template(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,40 +1,64 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../config.js');
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class RegistrationView {
|
||||
const template = views.getTemplate('user-registration');
|
||||
|
||||
class RegistrationView extends events.EventTarget {
|
||||
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) {
|
||||
ctx.userNamePattern = config.userNameRegex;
|
||||
ctx.passwordPattern = config.passwordRegex;
|
||||
clearMessages() {
|
||||
views.clearMessages(this._hostNode);
|
||||
}
|
||||
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this._template(ctx);
|
||||
showError(message) {
|
||||
views.showError(this._hostNode, message);
|
||||
}
|
||||
|
||||
const form = source.querySelector('form');
|
||||
const userNameField = source.querySelector('#user-name');
|
||||
const passwordField = source.querySelector('#user-password');
|
||||
const emailField = source.querySelector('#user-email');
|
||||
enableForm() {
|
||||
views.enableForm(this._formNode);
|
||||
}
|
||||
|
||||
views.decorateValidator(form);
|
||||
disableForm() {
|
||||
views.disableForm(this._formNode);
|
||||
}
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
views.clearMessages(target);
|
||||
views.disableForm(form);
|
||||
ctx.register(
|
||||
userNameField.value,
|
||||
passwordField.value,
|
||||
emailField.value)
|
||||
.always(() => { views.enableForm(form); });
|
||||
});
|
||||
_evtSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('submit', {
|
||||
detail: {
|
||||
name: this._userNameFieldNode.value,
|
||||
password: this._passwordFieldNode.value,
|
||||
email: this._emailFieldNode.value,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
get _formNode() {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,36 +1,50 @@
|
|||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class SettingsView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('settings');
|
||||
const 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) {
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this._template({browsingSettings: ctx.getSettings()});
|
||||
clearMessages() {
|
||||
views.clearMessages(this._hostNode);
|
||||
}
|
||||
|
||||
const form = source.querySelector('form');
|
||||
views.decorateValidator(form);
|
||||
showSuccess(text) {
|
||||
views.showSuccess(this._hostNode, text);
|
||||
}
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
views.clearMessages(source);
|
||||
ctx.saveSettings({
|
||||
upscaleSmallPosts:
|
||||
form.querySelector('#upscale-small-posts').checked,
|
||||
endlessScroll:
|
||||
form.querySelector('#endless-scroll').checked,
|
||||
keyboardShortcuts:
|
||||
form.querySelector('#keyboard-shortcuts').checked,
|
||||
transparencyGrid:
|
||||
form.querySelector('#transparency-grid').checked,
|
||||
});
|
||||
});
|
||||
_evtSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('change', {
|
||||
detail: {
|
||||
settings: {
|
||||
upscaleSmallPosts: this._formNode.querySelector(
|
||||
'#upscale-small-posts').checked,
|
||||
endlessScroll: this._formNode.querySelector(
|
||||
'#endless-scroll').checked,
|
||||
keyboardShortcuts: this._formNode.querySelector(
|
||||
'#keyboard-shortcuts').checked,
|
||||
transparencyGrid: this._formNode.querySelector(
|
||||
'#transparency-grid').checked,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
get _formNode() {
|
||||
return this._hostNode.querySelector('form');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,32 +1,28 @@
|
|||
'use strict';
|
||||
|
||||
const misc = require('../util/misc.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class TagListHeaderView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('tag-categories');
|
||||
}
|
||||
const template = views.getTemplate('tag-categories');
|
||||
|
||||
render(ctx) {
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this._template(ctx);
|
||||
class TagCategoriesView {
|
||||
constructor(ctx) {
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
const sourceNode = template(ctx);
|
||||
|
||||
const form = source.querySelector('form');
|
||||
const newRowTemplate = source.querySelector('.add-template');
|
||||
const tableBody = source.querySelector('tbody');
|
||||
const addLink = source.querySelector('a.add');
|
||||
const saveButton = source.querySelector('button.save');
|
||||
const formNode = sourceNode.querySelector('form');
|
||||
const newRowTemplate = sourceNode.querySelector('.add-template');
|
||||
const tableBodyNode = sourceNode.querySelector('tbody');
|
||||
const addLinkNode = sourceNode.querySelector('a.add');
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (addLink) {
|
||||
addLink.addEventListener('click', e => {
|
||||
if (addLinkNode) {
|
||||
addLinkNode.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
let newRow = newRowTemplate.cloneNode(true);
|
||||
tableBody.appendChild(newRow);
|
||||
|
@ -34,19 +30,26 @@ class TagListHeaderView {
|
|||
});
|
||||
}
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
this._evtSaveButtonClick(e, ctx, target);
|
||||
formNode.addEventListener('submit', e => {
|
||||
this._evtSaveButtonClick(e, ctx);
|
||||
});
|
||||
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
views.replaceContent(this._hostNode, sourceNode);
|
||||
}
|
||||
|
||||
_evtSaveButtonClick(e, ctx, target) {
|
||||
showSuccess(message) {
|
||||
views.showSuccess(this._hostNode, message);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
views.showError(this._hostNode, message);
|
||||
}
|
||||
|
||||
_evtSaveButtonClick(e, ctx) {
|
||||
e.preventDefault();
|
||||
|
||||
views.clearMessages(target);
|
||||
const tableBody = target.querySelector('tbody');
|
||||
views.clearMessages(this._hostNode);
|
||||
const tableBodyNode = this._hostNode.querySelector('tbody');
|
||||
|
||||
ctx.getCategories().then(categories => {
|
||||
let existingCategories = {};
|
||||
|
@ -59,7 +62,7 @@ class TagListHeaderView {
|
|||
let removedCategories = [];
|
||||
let changedCategories = [];
|
||||
let allNames = [];
|
||||
for (let row of tableBody.querySelectorAll('tr')) {
|
||||
for (let row of tableBodyNode.querySelectorAll('tr')) {
|
||||
let name = row.getAttribute('data-category');
|
||||
let category = {
|
||||
originalName: name,
|
||||
|
@ -127,4 +130,4 @@ class TagListHeaderView {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = TagListHeaderView;
|
||||
module.exports = TagCategoriesView;
|
||||
|
|
|
@ -1,30 +1,52 @@
|
|||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class TagDeleteView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('tag-delete');
|
||||
const 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) {
|
||||
const target = ctx.target;
|
||||
const source = this._template(ctx);
|
||||
clearMessages() {
|
||||
views.clearMessages(this._hostNode);
|
||||
}
|
||||
|
||||
const form = source.querySelector('form');
|
||||
enableForm() {
|
||||
views.enableForm(this._formNode);
|
||||
}
|
||||
|
||||
views.decorateValidator(form);
|
||||
disableForm() {
|
||||
views.disableForm(this._formNode);
|
||||
}
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
views.clearMessages(target);
|
||||
views.disableForm(form);
|
||||
ctx.delete(ctx.tag)
|
||||
.catch(() => { views.enableForm(form); });
|
||||
});
|
||||
showSuccess(message) {
|
||||
views.showSuccess(this._hostNode, message);
|
||||
}
|
||||
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
showError(message) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,40 +1,66 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../config.js');
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
const TagAutoCompleteControl =
|
||||
require('../controls/tag_auto_complete_control.js');
|
||||
|
||||
class TagMergeView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('tag-merge');
|
||||
}
|
||||
const 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;
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
|
||||
const target = ctx.target;
|
||||
const source = this._template(ctx);
|
||||
|
||||
const form = source.querySelector('form');
|
||||
const otherTagField = source.querySelector('.target input');
|
||||
|
||||
views.decorateValidator(form);
|
||||
if (otherTagField) {
|
||||
new TagAutoCompleteControl(otherTagField);
|
||||
views.decorateValidator(this._formNode);
|
||||
if (this._targetTagFieldNode) {
|
||||
new TagAutoCompleteControl(this._targetTagFieldNode);
|
||||
}
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
views.clearMessages(target);
|
||||
views.disableForm(form);
|
||||
ctx.mergeTo(otherTagField.value)
|
||||
.catch(() => { views.enableForm(form); });
|
||||
});
|
||||
clearMessages() {
|
||||
views.clearMessages(this._hostNode);
|
||||
}
|
||||
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
enableForm() {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,54 +1,89 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../config.js');
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.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);
|
||||
}
|
||||
|
||||
class TagSummaryView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('tag-summary');
|
||||
}
|
||||
class TagSummaryView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
|
||||
render(ctx) {
|
||||
this._tag = ctx.tag;
|
||||
this._hostNode = ctx.hostNode;
|
||||
const baseRegex = config.tagNameRegex.replace(/[\^\$]/g, '');
|
||||
ctx.tagNamesPattern = '^((' + baseRegex + ')\\s+)*(' + baseRegex + ')$';
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
|
||||
const target = ctx.target;
|
||||
const source = this._template(ctx);
|
||||
views.decorateValidator(this._formNode);
|
||||
|
||||
const form = source.querySelector('form');
|
||||
const namesField = source.querySelector('.names input');
|
||||
const categoryField = source.querySelector('.category select');
|
||||
const implicationsField = source.querySelector('.implications input');
|
||||
const suggestionsField = source.querySelector('.suggestions input');
|
||||
|
||||
if (implicationsField) {
|
||||
new TagInputControl(implicationsField);
|
||||
if (this._implicationsFieldNode) {
|
||||
new TagInputControl(this._implicationsFieldNode);
|
||||
}
|
||||
if (suggestionsField) {
|
||||
new TagInputControl(suggestionsField);
|
||||
if (this._suggestionsFieldNode) {
|
||||
new TagInputControl(this._suggestionsFieldNode);
|
||||
}
|
||||
|
||||
views.decorateValidator(form);
|
||||
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
|
||||
}
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
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); });
|
||||
});
|
||||
clearMessages() {
|
||||
views.clearMessages(this._hostNode);
|
||||
}
|
||||
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
enableForm() {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,22 @@
|
|||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
const TagSummaryView = require('./tag_summary_view.js');
|
||||
const TagMergeView = require('./tag_merge_view.js');
|
||||
const TagDeleteView = require('./tag_delete_view.js');
|
||||
|
||||
class TagView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('tag');
|
||||
this._summaryView = new TagSummaryView();
|
||||
this._mergeView = new TagMergeView();
|
||||
this._deleteView = new TagDeleteView();
|
||||
}
|
||||
const template = views.getTemplate('tag');
|
||||
|
||||
render(ctx) {
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this._template(ctx);
|
||||
class TagView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
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) {
|
||||
item.className = 'active';
|
||||
} else {
|
||||
|
@ -27,21 +24,47 @@ class TagView {
|
|||
}
|
||||
}
|
||||
|
||||
let view = null;
|
||||
ctx.hostNode = this._hostNode.querySelector('.tag-content-holder');
|
||||
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') {
|
||||
view = this._deleteView;
|
||||
this._view = new TagDeleteView(ctx);
|
||||
this._view.addEventListener('submit', e => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('delete', {detail: e.detail}));
|
||||
});
|
||||
} 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);
|
||||
views.showView(target, source);
|
||||
clearMessages() {
|
||||
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;
|
||||
|
||||
|
|
|
@ -7,34 +7,39 @@ const views = require('../util/views.js');
|
|||
const TagAutoCompleteControl =
|
||||
require('../controls/tag_auto_complete_control.js');
|
||||
|
||||
const template = views.getTemplate('tags-header');
|
||||
|
||||
class TagsHeaderView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('tags-header');
|
||||
}
|
||||
constructor(ctx) {
|
||||
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');
|
||||
const searchTextInput = form.querySelector('[name=search-text]');
|
||||
|
||||
if (searchTextInput) {
|
||||
new TagAutoCompleteControl(searchTextInput);
|
||||
if (this._queryInputNode) {
|
||||
new TagAutoCompleteControl(this._queryInputNode);
|
||||
}
|
||||
|
||||
keyboard.bind('q', () => {
|
||||
form.querySelector('input').focus();
|
||||
});
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
const text = searchTextInput.value;
|
||||
searchTextInput.blur();
|
||||
router.show('/tags/' + misc.formatSearchQuery({text: text}));
|
||||
});
|
||||
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
|
||||
}
|
||||
|
||||
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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,15 +2,11 @@
|
|||
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class TagsPageView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('tags-page');
|
||||
}
|
||||
const template = views.getTemplate('tags-page');
|
||||
|
||||
render(ctx) {
|
||||
const target = ctx.target;
|
||||
const source = this._template(ctx);
|
||||
views.showView(target, source);
|
||||
class TagsPageView {
|
||||
constructor(ctx) {
|
||||
views.replaceContent(ctx.hostNode, template(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,24 +2,19 @@
|
|||
|
||||
const views = require('../util/views.js');
|
||||
|
||||
const template = views.getTemplate('top-navigation');
|
||||
|
||||
class TopNavigationView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('top-navigation');
|
||||
this._navHolder = document.getElementById('top-navigation-holder');
|
||||
this._lastCtx = null;
|
||||
this._hostNode = document.getElementById('top-navigation-holder');
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
this._lastCtx = ctx;
|
||||
const target = this._navHolder;
|
||||
const source = this._template(ctx);
|
||||
views.showView(this._navHolder, source);
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
}
|
||||
|
||||
activate(key) {
|
||||
const allItemNodes = document.querySelectorAll(
|
||||
'#top-navigation-holder [data-name]');
|
||||
for (let itemNode of allItemNodes) {
|
||||
for (let itemNode of this._hostNode.querySelectorAll('[data-name]')) {
|
||||
itemNode.classList.toggle(
|
||||
'active', itemNode.getAttribute('data-name') === key);
|
||||
}
|
||||
|
|
|
@ -1,30 +1,54 @@
|
|||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class UserDeleteView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('user-delete');
|
||||
const 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) {
|
||||
const target = ctx.target;
|
||||
const source = this._template(ctx);
|
||||
clearMessages() {
|
||||
views.clearMessages(this._hostNode);
|
||||
}
|
||||
|
||||
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 => {
|
||||
e.preventDefault();
|
||||
views.clearMessages(target);
|
||||
views.disableForm(form);
|
||||
ctx.delete()
|
||||
.catch(() => { views.enableForm(form); });
|
||||
});
|
||||
enableForm() {
|
||||
views.enableForm(this._formNode);
|
||||
}
|
||||
|
||||
disableForm() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,63 +1,102 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../config.js');
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
const FileDropperControl = require('../controls/file_dropper_control.js');
|
||||
|
||||
class UserEditView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('user-edit');
|
||||
}
|
||||
const template = views.getTemplate('user-edit');
|
||||
|
||||
class UserEditView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
|
||||
render(ctx) {
|
||||
ctx.userNamePattern = config.userNameRegex + /|^$/.source;
|
||||
ctx.passwordPattern = config.passwordRegex + /|^$/.source;
|
||||
|
||||
const target = ctx.target;
|
||||
const source = this._template(ctx);
|
||||
this._user = ctx.user;
|
||||
this._hostNode = ctx.hostNode;
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
views.decorateValidator(this._formNode);
|
||||
|
||||
const form = source.querySelector('form');
|
||||
const avatarContentField = source.querySelector('#avatar-content');
|
||||
|
||||
views.decorateValidator(form);
|
||||
|
||||
let avatarContent = null;
|
||||
if (avatarContentField) {
|
||||
this._avatarContent = null;
|
||||
if (this._avatarContentFieldNode) {
|
||||
new FileDropperControl(
|
||||
avatarContentField,
|
||||
this._avatarContentFieldNode,
|
||||
{
|
||||
lock: true,
|
||||
resolve: files => {
|
||||
source.querySelector(
|
||||
this._hostNode.querySelector(
|
||||
'[name=avatar-style][value=manual]').checked = true;
|
||||
avatarContent = files[0];
|
||||
this._avatarContent = files[0];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener('submit', 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');
|
||||
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
views.clearMessages(target);
|
||||
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); });
|
||||
});
|
||||
clearMessages() {
|
||||
views.clearMessages(this._hostNode);
|
||||
}
|
||||
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
showSuccess(message) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,16 +2,12 @@
|
|||
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class UserSummaryView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('user-summary');
|
||||
}
|
||||
const template = views.getTemplate('user-summary');
|
||||
|
||||
render(ctx) {
|
||||
const target = ctx.target;
|
||||
const source = this._template(ctx);
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
class UserSummaryView {
|
||||
constructor(ctx) {
|
||||
this._hostNode = ctx.hostNode;
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,22 @@
|
|||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
const UserDeleteView = require('./user_delete_view.js');
|
||||
const UserSummaryView = require('./user_summary_view.js');
|
||||
const UserEditView = require('./user_edit_view.js');
|
||||
|
||||
class UserView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('user');
|
||||
this._deleteView = new UserDeleteView();
|
||||
this._summaryView = new UserSummaryView();
|
||||
this._editView = new UserEditView();
|
||||
}
|
||||
const template = views.getTemplate('user');
|
||||
|
||||
render(ctx) {
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this._template(ctx);
|
||||
class UserView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
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) {
|
||||
item.className = 'active';
|
||||
} else {
|
||||
|
@ -27,19 +24,42 @@ class UserView {
|
|||
}
|
||||
}
|
||||
|
||||
let view = null;
|
||||
ctx.hostNode = this._hostNode.querySelector('#user-content-holder');
|
||||
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') {
|
||||
view = this._deleteView;
|
||||
this._view = new UserDeleteView(ctx);
|
||||
this._view.addEventListener('submit', e => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('delete', {detail: e.detail}));
|
||||
});
|
||||
} else {
|
||||
view = this._summaryView;
|
||||
this._view = new UserSummaryView(ctx);
|
||||
}
|
||||
ctx.target = source.querySelector('#user-content-holder');
|
||||
view.render(ctx);
|
||||
}
|
||||
|
||||
views.listenToMessages(source);
|
||||
views.showView(target, source);
|
||||
clearMessages() {
|
||||
this._view.clearMessages();
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
this._view.showSuccess(message);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this._view.showError(message);
|
||||
}
|
||||
|
||||
enableForm() {
|
||||
this._view.enableForm();
|
||||
}
|
||||
|
||||
disableForm() {
|
||||
this._view.disableForm();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,30 +5,35 @@ const keyboard = require('../util/keyboard.js');
|
|||
const misc = require('../util/misc.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
const template = views.getTemplate('users-header');
|
||||
|
||||
class UsersHeaderView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('users-header');
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
const target = ctx.target;
|
||||
const source = this._template(ctx);
|
||||
|
||||
const form = source.querySelector('form');
|
||||
constructor(ctx) {
|
||||
this._hostNode = ctx.hostNode;
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
|
||||
keyboard.bind('q', () => {
|
||||
form.querySelector('input').focus();
|
||||
this._formNode.querySelector('input').focus();
|
||||
});
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
const searchTextInput = form.querySelector('[name=search-text]');
|
||||
const text = searchTextInput.value;
|
||||
searchTextInput.blur();
|
||||
router.show('/users/' + misc.formatSearchQuery({text: text}));
|
||||
});
|
||||
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
|
||||
}
|
||||
|
||||
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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,15 +2,11 @@
|
|||
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class UsersPageView {
|
||||
constructor() {
|
||||
this._template = views.getTemplate('users-page');
|
||||
}
|
||||
const template = views.getTemplate('users-page');
|
||||
|
||||
render(ctx) {
|
||||
const target = ctx.target;
|
||||
const source = this._template(ctx);
|
||||
views.showView(target, source);
|
||||
class UsersPageView {
|
||||
constructor(ctx) {
|
||||
views.replaceContent(ctx.hostNode, template(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue