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