diff --git a/client/html/home.tpl b/client/html/home.tpl index 69c58fe..88f2c58 100644 --- a/client/html/home.tpl +++ b/client/html/home.tpl @@ -2,9 +2,7 @@

<%= ctx.name %>

- +
<% if (ctx.canListPosts) { %>
diff --git a/client/html/home_stats.tpl b/client/html/home_stats.tpl new file mode 100644 index 0000000..b66b687 --- /dev/null +++ b/client/html/home_stats.tpl @@ -0,0 +1 @@ +Serving <%= ctx.postCount %> posts (<%= ctx.makeFileSize(ctx.diskUsage) %>) diff --git a/client/js/api.js b/client/js/api.js index 24537de..3390a66 100644 --- a/client/js/api.js +++ b/client/js/api.js @@ -6,8 +6,9 @@ const request = require('superagent'); const config = require('./config.js'); const events = require('./events.js'); -class Api { +class Api extends events.EventTarget { constructor() { + super(); this.user = null; this.userName = null; this.userPassword = null; @@ -136,11 +137,10 @@ class Api { options); this.user = response; resolve(); - events.notify(events.Authentication); + this.dispatchEvent(new CustomEvent('login')); }).catch(response => { reject(response.description); this.logout(); - events.notify(events.Authentication); }); }); } @@ -149,7 +149,7 @@ class Api { this.user = null; this.userName = null; this.userPassword = null; - events.notify(events.Authentication); + this.dispatchEvent(new CustomEvent('logout')); } forget() { diff --git a/client/js/controllers/auth_controller.js b/client/js/controllers/auth_controller.js index f470e22..baae595 100644 --- a/client/js/controllers/auth_controller.js +++ b/client/js/controllers/auth_controller.js @@ -2,105 +2,47 @@ const router = require('../router.js'); const api = require('../api.js'); -const events = require('../events.js'); -const TopNavigation = require('../models/top_navigation.js'); +const topNavigation = require('../models/top_navigation.js'); const LoginView = require('../views/login_view.js'); -const PasswordResetView = require('../views/password_reset_view.js'); -class AuthController { +class LoginController { constructor() { + api.forget(); + topNavigation.activate('login'); + this._loginView = new LoginView(); - this._passwordResetView = new PasswordResetView(); + this._loginView.addEventListener('submit', e => this._evtLogin(e)); } - registerRoutes() { - router.enter( - /\/password-reset\/([^:]+):([^:]+)$/, - (ctx, next) => { - this._passwordResetFinishRoute(ctx.params[0], ctx.params[1]); - }); - router.enter( - '/password-reset', - (ctx, next) => { this._passwordResetRoute(); }); - router.enter( - '/login', - (ctx, next) => { this._loginRoute(); }); - router.enter( - '/logout', - (ctx, next) => { this._logoutRoute(); }); - } - - _loginRoute() { + _evtLogin(e) { + this._loginView.clearMessages(); + this._loginView.disableForm(); api.forget(); - TopNavigation.activate('login'); - this._loginView.render({ - login: (name, password, doRemember) => { - return new Promise((resolve, reject) => { - api.forget(); - api.login(name, password, doRemember) - .then(() => { - resolve(); - router.show('/'); - events.notify(events.Success, 'Logged in'); - }, errorMessage => { - reject(errorMessage); - events.notify(events.Error, errorMessage); - }); - }); - }}); - } - - _logoutRoute() { - api.forget(); - api.logout(); - router.show('/'); - events.notify(events.Success, 'Logged out'); - } - - _passwordResetRoute() { - TopNavigation.activate('login'); - this._passwordResetView.render({ - proceed: (...args) => { - return this._passwordReset(...args); - }}); - } - - _passwordResetFinishRoute(name, token) { - api.forget(); - api.logout(); - let password = null; - api.post('/password-reset/' + name, {token: token}) - .then(response => { - password = response.password; - return api.login(name, password, false); - }, response => { - return Promise.reject(response.description); - }).then(() => { - router.show('/'); - events.notify(events.Success, 'New password: ' + password); + api.login(e.detail.name, e.detail.password, e.detail.remember) + .then(() => { + const ctx = router.show('/'); + ctx.controller.showSuccess('Logged in'); }, errorMessage => { - router.show('/'); - events.notify(events.Error, errorMessage); + this._loginView.showError(errorMessage); + this._loginView.enableForm(); }); } - - _passwordReset(nameOrEmail) { - api.forget(); - api.logout(); - return new Promise((resolve, reject) => { - api.get('/password-reset/' + nameOrEmail) - .then(() => { - resolve(); - events.notify( - events.Success, - 'E-mail has been sent. To finish the procedure, ' + - 'please click the link it contains.'); - }, response => { - reject(); - events.notify(events.Error, response.description); - }); - }); - } } -module.exports = new AuthController(); +class LogoutController { + constructor() { + api.forget(); + api.logout(); + const ctx = router.show('/'); + ctx.controller.showSuccess('Logged out'); + } +} + +module.exports = router => { + router.enter('/login', (ctx, next) => { + ctx.controller = new LoginController(); + }); + router.enter('/logout', (ctx, next) => { + ctx.controller = new LogoutController(); + }); +}; diff --git a/client/js/controllers/comments_controller.js b/client/js/controllers/comments_controller.js index 6ab819b..b332511 100644 --- a/client/js/controllers/comments_controller.js +++ b/client/js/controllers/comments_controller.js @@ -1,29 +1,19 @@ 'use strict'; const api = require('../api.js'); -const router = require('../router.js'); const misc = require('../util/misc.js'); -const pageController = require('../controllers/page_controller.js'); -const TopNavigation = require('../models/top_navigation.js'); +const topNavigation = require('../models/top_navigation.js'); +const PageController = require('../controllers/page_controller.js'); const CommentsPageView = require('../views/comments_page_view.js'); -const EmptyView = require('../views/empty_view.js'); class CommentsController { - registerRoutes() { - router.enter('/comments/:query?', - (ctx, next) => { misc.parseSearchQueryRoute(ctx, next); }, - (ctx, next) => { this._listCommentsRoute(ctx); }); - this._commentsPageView = new CommentsPageView(); - this._emptyView = new EmptyView(); - } + constructor(ctx) { + topNavigation.activate('comments'); - _listCommentsRoute(ctx) { - TopNavigation.activate('comments'); - - pageController.run({ + this._pageController = new PageController({ searchQuery: ctx.searchQuery, clientUrl: '/comments/' + misc.formatSearchQuery({page: '{page}'}), - requestPage: pageController.createHistoryCacheProxy( + requestPage: PageController.createHistoryCacheProxy( ctx, page => { return api.get( @@ -31,12 +21,18 @@ class CommentsController { `&page=${page}&pageSize=10&fields=` + 'id,comments,commentCount,thumbnailUrl'); }), - pageRenderer: this._commentsPageView, - pageContext: { - canViewPosts: api.hasPrivilege('posts:view'), - } + pageRenderer: pageCtx => { + Object.assign(pageCtx, { + canViewPosts: api.hasPrivilege('posts:view'), + }); + return new CommentsPageView(pageCtx); + }, }); } -} +}; -module.exports = new CommentsController(); +module.exports = router => { + router.enter('/comments/:query?', + (ctx, next) => { misc.parseSearchQueryRoute(ctx, next); }, + (ctx, next) => { new CommentsController(ctx); }); +}; diff --git a/client/js/controllers/help_controller.js b/client/js/controllers/help_controller.js index 9c199a6..f7dcafa 100644 --- a/client/js/controllers/help_controller.js +++ b/client/js/controllers/help_controller.js @@ -1,35 +1,23 @@ 'use strict'; -const router = require('../router.js'); -const TopNavigation = require('../models/top_navigation.js'); +const topNavigation = require('../models/top_navigation.js'); const HelpView = require('../views/help_view.js'); class HelpController { - constructor() { - this._helpView = new HelpView(); - } - - registerRoutes() { - router.enter( - '/help', - (ctx, next) => { this._showHelpRoute(); }); - router.enter( - '/help/:section', - (ctx, next) => { this._showHelpRoute(ctx.params.section); }); - router.enter( - '/help/:section/:subsection', - (ctx, next) => { - this._showHelpRoute(ctx.params.section, ctx.params.subsection); - }); - } - - _showHelpRoute(section, subsection) { - TopNavigation.activate('help'); - this._helpView.render({ - section: section, - subsection: subsection, - }); + constructor(section, subsection) { + topNavigation.activate('help'); + this._helpView = new HelpView(section, subsection); } } -module.exports = new HelpController(); +module.exports = router => { + router.enter('/help', (ctx, next) => { + new HelpController(); + }); + router.enter('/help/:section', (ctx, next) => { + new HelpController(ctx.params.section); + }); + router.enter('/help/:section/:subsection', (ctx, next) => { + new HelpController(ctx.params.section, ctx.params.subsection); + }); +}; diff --git a/client/js/controllers/history_controller.js b/client/js/controllers/history_controller.js index 1793fe8..f09e0b6 100644 --- a/client/js/controllers/history_controller.js +++ b/client/js/controllers/history_controller.js @@ -1,18 +1,15 @@ 'use strict'; -const router = require('../router.js'); -const TopNavigation = require('../models/top_navigation.js'); +const topNavigation = require('../models/top_navigation.js'); class HistoryController { - registerRoutes() { - router.enter( - '/history', - (ctx, next) => { this._listHistoryRoute(); }); - } - - _listHistoryRoute() { - TopNavigation.activate(''); + constructor() { + topNavigation.activate(''); } } -module.exports = new HistoryController(); +module.exports = router => { + router.enter('/history', (ctx, next) => { + ctx.controller = new HistoryController(); + }); +}; diff --git a/client/js/controllers/home_controller.js b/client/js/controllers/home_controller.js index 11151c3..37856a3 100644 --- a/client/js/controllers/home_controller.js +++ b/client/js/controllers/home_controller.js @@ -1,53 +1,49 @@ 'use strict'; -const router = require('../router.js'); const api = require('../api.js'); -const events = require('../events.js'); -const TopNavigation = require('../models/top_navigation.js'); +const config = require('../config.js'); +const topNavigation = require('../models/top_navigation.js'); const HomeView = require('../views/home_view.js'); -const NotFoundView = require('../views/not_found_view.js'); class HomeController { constructor() { - this._homeView = new HomeView(); - this._notFoundView = new NotFoundView(); - } + topNavigation.activate('home'); - registerRoutes() { - router.enter( - '/', - (ctx, next) => { this._indexRoute(); }); - router.enter( - '*', - (ctx, next) => { this._notFoundRoute(ctx); }); - } - - _indexRoute() { - TopNavigation.activate('home'); + this._homeView = new HomeView({ + name: config.name, + version: config.meta.version, + buildDate: config.meta.buildDate, + canListPosts: api.hasPrivilege('posts:list'), + }); api.get('/info') .then(response => { - this._homeView.render({ - canListPosts: api.hasPrivilege('posts:list'), + this._homeView.setStats({ diskUsage: response.diskUsage, postCount: response.postCount, + }); + this._homeView.setFeaturedPost({ featuredPost: response.featuredPost, featuringUser: response.featuringUser, featuringTime: response.featuringTime, }); }, response => { - this._homeView.render({ - canListPosts: api.hasPrivilege('posts:list'), - }); - events.notify(events.Error, response.description); + this._homeView.showError(response.description); }); } - _notFoundRoute(ctx) { - TopNavigation.activate(''); - this._notFoundView.render({path: ctx.canonicalPath}); + showSuccess(message) { + this._homeView.showSuccess(message); } -} -module.exports = new HomeController(); + showError(message) { + this._homeView.showError(message); + } +}; + +module.exports = router => { + router.enter('/', (ctx, next) => { + ctx.controller = new HomeController(); + }); +}; diff --git a/client/js/controllers/not_found_controller.js b/client/js/controllers/not_found_controller.js new file mode 100644 index 0000000..1a8ae34 --- /dev/null +++ b/client/js/controllers/not_found_controller.js @@ -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); + }); +}; diff --git a/client/js/controllers/page_controller.js b/client/js/controllers/page_controller.js index 2047484..5228674 100644 --- a/client/js/controllers/page_controller.js +++ b/client/js/controllers/page_controller.js @@ -1,43 +1,35 @@ 'use strict'; -const events = require('../events.js'); -const settings = require('../settings.js'); +const settings = require('../models/settings.js'); const EndlessPageView = require('../views/endless_page_view.js'); const ManualPageView = require('../views/manual_page_view.js'); class PageController { - constructor() { - events.listen(events.SettingsChange, () => { - this._update(); - return true; - }); - this._update(); - } - - _update() { - if (settings.getSettings().endlessScroll) { - this._pageView = new EndlessPageView(); - } else { - this._pageView = new ManualPageView(); - } - } - - run(ctx) { - this._pageView.unrender(); - + constructor(ctx) { const extendedContext = { clientUrl: ctx.clientUrl, searchQuery: ctx.searchQuery, }; - ctx.headerContext = ctx.headerContext || {}; - ctx.pageContext = ctx.pageContext || {}; - Object.assign(ctx.headerContext, extendedContext); - Object.assign(ctx.pageContext, extendedContext); - this._pageView.render(ctx); + ctx.headerContext = Object.assign({}, extendedContext); + ctx.pageContext = Object.assign({}, extendedContext); + + if (settings.get().endlessScroll) { + this._view = new EndlessPageView(ctx); + } else { + this._view = new ManualPageView(ctx); + } } - createHistoryCacheProxy(routerCtx, requestPage) { + showSuccess(message) { + this._view.showSuccess(message); + } + + showError(message) { + this._view.showError(message); + } + + static createHistoryCacheProxy(routerCtx, requestPage) { return page => { if (routerCtx.state.response) { return new Promise((resolve, reject) => { @@ -52,10 +44,6 @@ class PageController { return promise; }; } - - stop() { - this._pageView.unrender(); - } } -module.exports = new PageController(); +module.exports = PageController; diff --git a/client/js/controllers/password_reset_controller.js b/client/js/controllers/password_reset_controller.js new file mode 100644 index 0000000..00a288c --- /dev/null +++ b/client/js/controllers/password_reset_controller.js @@ -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]); + }); +}; diff --git a/client/js/controllers/post_controller.js b/client/js/controllers/post_controller.js index 2155750..0a63e5e 100644 --- a/client/js/controllers/post_controller.js +++ b/client/js/controllers/post_controller.js @@ -1,38 +1,23 @@ 'use strict'; -const router = require('../router.js'); const api = require('../api.js'); -const events = require('../events.js'); -const settings = require('../settings.js'); +const settings = require('../models/settings.js'); const Post = require('../models/post.js'); -const TopNavigation = require('../models/top_navigation.js'); +const topNavigation = require('../models/top_navigation.js'); const PostView = require('../views/post_view.js'); const EmptyView = require('../views/empty_view.js'); class PostController { - constructor() { - this._postView = new PostView(); - this._emptyView = new EmptyView(); - } + constructor(id, editMode) { + topNavigation.activate('posts'); - registerRoutes() { - router.enter( - '/post/:id', - (ctx, next) => { this._showPostRoute(ctx.params.id, false); }); - router.enter( - '/post/:id/edit', - (ctx, next) => { this._showPostRoute(ctx.params.id, true); }); - } - - _showPostRoute(id, editMode) { - TopNavigation.activate('posts'); Promise.all([ Post.get(id), api.get(`/post/${id}/around?fields=id&query=` + this._decorateSearchQuery('')), ]).then(responses => { const [post, aroundResponse] = responses; - this._postView.render({ + this._view = new PostView({ post: post, editMode: editMode, nextPostId: aroundResponse.next ? aroundResponse.next.id : null, @@ -42,13 +27,13 @@ class PostController { canCreateComments: api.hasPrivilege('comments:create'), }); }, response => { - this._emptyView.render(); - events.notify(events.Error, response.description); + this._view = new EmptyView(); + this._view.showError(response.description); }); } _decorateSearchQuery(text) { - const browsingSettings = settings.getSettings(); + const browsingSettings = settings.get(); let disabledSafety = []; for (let key of Object.keys(browsingSettings.listPosts)) { if (browsingSettings.listPosts[key] === false) { @@ -62,4 +47,11 @@ class PostController { } } -module.exports = new PostController(); +module.exports = router => { + router.enter('/post/:id', (ctx, next) => { + ctx.controller = new PostController(ctx.params.id, false); + }); + router.enter('/post/:id/edit', (ctx, next) => { + ctx.controller = new PostController(ctx.params.id, true); + }); +}; diff --git a/client/js/controllers/post_list_controller.js b/client/js/controllers/post_list_controller.js index 31d7777..8d33ec8 100644 --- a/client/js/controllers/post_list_controller.js +++ b/client/js/controllers/post_list_controller.js @@ -1,35 +1,22 @@ 'use strict'; -const router = require('../router.js'); const api = require('../api.js'); -const settings = require('../settings.js'); +const settings = require('../models/settings.js'); const misc = require('../util/misc.js'); -const pageController = require('../controllers/page_controller.js'); -const TopNavigation = require('../models/top_navigation.js'); +const topNavigation = require('../models/top_navigation.js'); +const PageController = require('../controllers/page_controller.js'); const PostsHeaderView = require('../views/posts_header_view.js'); const PostsPageView = require('../views/posts_page_view.js'); class PostListController { - constructor() { - this._postsHeaderView = new PostsHeaderView(); - this._postsPageView = new PostsPageView(); - } + constructor(ctx) { + topNavigation.activate('posts'); - registerRoutes() { - router.enter( - '/posts/:query?', - (ctx, next) => { misc.parseSearchQueryRoute(ctx, next); }, - (ctx, next) => { this._listPostsRoute(ctx); }); - } - - _listPostsRoute(ctx) { - TopNavigation.activate('posts'); - - pageController.run({ + this._pageController = new PageController({ searchQuery: ctx.searchQuery, clientUrl: '/posts/' + misc.formatSearchQuery({ text: ctx.searchQuery.text, page: '{page}'}), - requestPage: pageController.createHistoryCacheProxy( + requestPage: PageController.createHistoryCacheProxy( ctx, page => { const text @@ -39,16 +26,20 @@ class PostListController { '&fields=id,type,tags,score,favoriteCount,' + 'commentCount,thumbnailUrl'); }), - headerRenderer: this._postsHeaderView, - pageRenderer: this._postsPageView, - pageContext: { - canViewPosts: api.hasPrivilege('posts:view'), - } + headerRenderer: headerCtx => { + return new PostsHeaderView(headerCtx); + }, + pageRenderer: pageCtx => { + Object.assign(pageCtx, { + canViewPosts: api.hasPrivilege('posts:view'), + }); + return new PostsPageView(pageCtx); + }, }); } _decorateSearchQuery(text) { - const browsingSettings = settings.getSettings(); + const browsingSettings = settings.get(); let disabledSafety = []; for (let key of Object.keys(browsingSettings.listPosts)) { if (browsingSettings.listPosts[key] === false) { @@ -62,4 +53,9 @@ class PostListController { } } -module.exports = new PostListController(); +module.exports = router => { + router.enter( + '/posts/:query?', + (ctx, next) => { misc.parseSearchQueryRoute(ctx, next); }, + (ctx, next) => { ctx.controller = new PostListController(ctx); }); +}; diff --git a/client/js/controllers/post_upload_controller.js b/client/js/controllers/post_upload_controller.js index 0f02a0f..beb8ec6 100644 --- a/client/js/controllers/post_upload_controller.js +++ b/client/js/controllers/post_upload_controller.js @@ -1,24 +1,17 @@ 'use strict'; -const router = require('../router.js'); -const TopNavigation = require('../models/top_navigation.js'); +const topNavigation = require('../models/top_navigation.js'); const EmptyView = require('../views/empty_view.js'); class PostUploadController { constructor() { + topNavigation.activate('upload'); this._emptyView = new EmptyView(); } - - registerRoutes() { - router.enter( - '/upload', - (ctx, next) => { this._uploadPostsRoute(); }); - } - - _uploadPostsRoute() { - TopNavigation.activate('upload'); - this._emptyView.render(); - } } -module.exports = new PostUploadController(); +module.exports = router => { + router.enter('/upload', (ctx, next) => { + ctx.controller = new PostUploadController(); + }); +}; diff --git a/client/js/controllers/settings_controller.js b/client/js/controllers/settings_controller.js index 360b480..e7bba6e 100644 --- a/client/js/controllers/settings_controller.js +++ b/client/js/controllers/settings_controller.js @@ -1,26 +1,27 @@ 'use strict'; -const router = require('../router.js'); -const settings = require('../settings.js'); -const TopNavigation = require('../models/top_navigation.js'); +const settings = require('../models/settings.js'); +const topNavigation = require('../models/top_navigation.js'); const SettingsView = require('../views/settings_view.js'); class SettingsController { constructor() { - this._settingsView = new SettingsView(); - } - - registerRoutes() { - router.enter('/settings', (ctx, next) => { this._settingsRoute(); }); - } - - _settingsRoute() { - TopNavigation.activate('settings'); - this._settingsView.render({ - getSettings: () => settings.getSettings(), - saveSettings: newSettings => settings.saveSettings(newSettings), + topNavigation.activate('settings'); + this._view = new SettingsView({ + settings: settings.get(), }); + this._view.addEventListener('change', e => this._evtChange(e)); + } + + _evtChange(e) { + this._view.clearMessages(); + settings.save(e.detail.settings); + this._view.showSuccess('Settings saved.'); } }; -module.exports = new SettingsController(); +module.exports = router => { + router.enter('/settings', (ctx, next) => { + ctx.controller = new SettingsController(); + }); +}; diff --git a/client/js/controllers/tag_categories_controller.js b/client/js/controllers/tag_categories_controller.js new file mode 100644 index 0000000..00a6c93 --- /dev/null +++ b/client/js/controllers/tag_categories_controller.js @@ -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); + }); +}; diff --git a/client/js/controllers/tag_controller.js b/client/js/controllers/tag_controller.js new file mode 100644 index 0000000..99316f4 --- /dev/null +++ b/client/js/controllers/tag_controller.js @@ -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'); + }); +}; diff --git a/client/js/controllers/tag_list_controller.js b/client/js/controllers/tag_list_controller.js new file mode 100644 index 0000000..f948de4 --- /dev/null +++ b/client/js/controllers/tag_list_controller.js @@ -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); }); +}; diff --git a/client/js/controllers/tags_controller.js b/client/js/controllers/tags_controller.js deleted file mode 100644 index fef4977..0000000 --- a/client/js/controllers/tags_controller.js +++ /dev/null @@ -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(); diff --git a/client/js/controllers/top_navigation_controller.js b/client/js/controllers/top_navigation_controller.js index 6b0d47f..85fa76c 100644 --- a/client/js/controllers/top_navigation_controller.js +++ b/client/js/controllers/top_navigation_controller.js @@ -1,70 +1,68 @@ 'use strict'; const api = require('../api.js'); -const events = require('../events.js'); +const topNavigation = require('../models/top_navigation.js'); const TopNavigationView = require('../views/top_navigation_view.js'); -const TopNavigation = require('../models/top_navigation.js'); class TopNavigationController { constructor() { this._topNavigationView = new TopNavigationView(); - TopNavigation.addEventListener( + topNavigation.addEventListener( 'activate', e => this._evtActivate(e)); - events.listen( - events.Authentication, - () => { - this._render(); - return true; - }); + api.addEventListener('login', e => this._evtAuthChange(e)); + api.addEventListener('logout', e => this._evtAuthChange(e)); this._render(); } + _evtAuthChange(e) { + this._render(); + } + _evtActivate(e) { this._topNavigationView.activate(e.key); } _updateNavigationFromPrivileges() { - TopNavigation.get('account').url = '/user/' + api.userName; - TopNavigation.get('account').imageUrl = + topNavigation.get('account').url = '/user/' + api.userName; + topNavigation.get('account').imageUrl = api.user ? api.user.avatarUrl : null; - TopNavigation.showAll(); + topNavigation.showAll(); if (!api.hasPrivilege('posts:list')) { - TopNavigation.hide('posts'); + topNavigation.hide('posts'); } if (!api.hasPrivilege('posts:create')) { - TopNavigation.hide('upload'); + topNavigation.hide('upload'); } if (!api.hasPrivilege('comments:list')) { - TopNavigation.hide('comments'); + topNavigation.hide('comments'); } if (!api.hasPrivilege('tags:list')) { - TopNavigation.hide('tags'); + topNavigation.hide('tags'); } if (!api.hasPrivilege('users:list')) { - TopNavigation.hide('users'); + topNavigation.hide('users'); } if (api.isLoggedIn()) { - TopNavigation.hide('register'); - TopNavigation.hide('login'); + topNavigation.hide('register'); + topNavigation.hide('login'); } else { - TopNavigation.hide('account'); - TopNavigation.hide('logout'); + topNavigation.hide('account'); + topNavigation.hide('logout'); } } _render() { this._updateNavigationFromPrivileges(); - console.log(TopNavigation.getAll()); this._topNavigationView.render({ - items: TopNavigation.getAll(), + items: topNavigation.getAll(), }); this._topNavigationView.activate( - TopNavigation.activeItem ? TopNavigation.activeItem.key : ''); - }; + topNavigation.activeItem ? topNavigation.activeItem.key : ''); + } } module.exports = new TopNavigationController(); diff --git a/client/js/controllers/user_controller.js b/client/js/controllers/user_controller.js new file mode 100644 index 0000000..161c568 --- /dev/null +++ b/client/js/controllers/user_controller.js @@ -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'); + }); +}; diff --git a/client/js/controllers/user_list_controller.js b/client/js/controllers/user_list_controller.js new file mode 100644 index 0000000..892568f --- /dev/null +++ b/client/js/controllers/user_list_controller.js @@ -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); }); +}; diff --git a/client/js/controllers/user_registration_controller.js b/client/js/controllers/user_registration_controller.js new file mode 100644 index 0000000..a23aaa0 --- /dev/null +++ b/client/js/controllers/user_registration_controller.js @@ -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(); + }); +}; diff --git a/client/js/controllers/users_controller.js b/client/js/controllers/users_controller.js deleted file mode 100644 index 32b311d..0000000 --- a/client/js/controllers/users_controller.js +++ /dev/null @@ -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(); diff --git a/client/js/controls/comment_control.js b/client/js/controls/comment_control.js index 5e283d4..4ee3e51 100644 --- a/client/js/controls/comment_control.js +++ b/client/js/controls/comment_control.js @@ -25,7 +25,7 @@ class CommentControl { canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`), }); - views.showView( + views.replaceContent( sourceNode.querySelector('.score-container'), this._scoreTemplate({ score: this._comment.score, @@ -77,7 +77,7 @@ class CommentControl { canCancel: true }); - views.showView(this._hostNode, sourceNode); + views.replaceContent(this._hostNode, sourceNode); } _evtScoreClick(e, scoreGetter) { diff --git a/client/js/controls/comment_form_control.js b/client/js/controls/comment_form_control.js index ea4aabe..7d488aa 100644 --- a/client/js/controls/comment_form_control.js +++ b/client/js/controls/comment_form_control.js @@ -47,7 +47,7 @@ class CommentFormControl { this._growTextArea(); }); - views.showView(this._hostNode, sourceNode); + views.replaceContent(this._hostNode, sourceNode); } enterEditMode() { diff --git a/client/js/controls/comment_list_control.js b/client/js/controls/comment_list_control.js index 91b13f8..f38ab49 100644 --- a/client/js/controls/comment_list_control.js +++ b/client/js/controls/comment_list_control.js @@ -19,7 +19,7 @@ class CommentListControl { canListComments: api.hasPrivilege('comments:list'), }); - views.showView(this._hostNode, sourceNode); + views.replaceContent(this._hostNode, sourceNode); this._renderComments(); } @@ -42,7 +42,7 @@ class CommentListControl { }); commentList.appendChild(commentListItemNode); } - views.showView(this._hostNode.querySelector('ul'), commentList); + views.replaceContent(this._hostNode.querySelector('ul'), commentList); } }; diff --git a/client/js/controls/file_dropper_control.js b/client/js/controls/file_dropper_control.js index 46bee77..0476c13 100644 --- a/client/js/controls/file_dropper_control.js +++ b/client/js/controls/file_dropper_control.js @@ -28,7 +28,7 @@ class FileDropperControl { this._fileInputNode.addEventListener( 'change', e => this._evtFileChange(e)); - views.showView(target, source); + views.replaceContent(target, source); } _resolve(files) { diff --git a/client/js/controls/post_content_control.js b/client/js/controls/post_content_control.js index bd9d01e..886caa1 100644 --- a/client/js/controls/post_content_control.js +++ b/client/js/controls/post_content_control.js @@ -1,6 +1,6 @@ 'use strict'; -const settings = require('../settings.js'); +const settings = require('../models/settings.js'); const views = require('../util/views.js'); const optimizedResize = require('../util/optimized_resize.js'); @@ -21,7 +21,7 @@ class PostContentControl { this._currentFitFunction = this.fitWidth; const mul = this._post.canvasHeight / this._post.canvasWidth; let width = this._viewportWidth; - if (!settings.getSettings().upscaleSmallPosts) { + if (!settings.get().upscaleSmallPosts) { width = Math.min(this._post.canvasWidth, width); } this._resize(width, width * mul); @@ -31,7 +31,7 @@ class PostContentControl { this._currentFitFunction = this.fitHeight; const mul = this._post.canvasWidth / this._post.canvasHeight; let height = this._viewportHeight; - if (!settings.getSettings().upscaleSmallPosts) { + if (!settings.get().upscaleSmallPosts) { height = Math.min(this._post.canvasHeight, height); } this._resize(height * mul, height); @@ -42,13 +42,13 @@ class PostContentControl { let mul = this._post.canvasHeight / this._post.canvasWidth; if (this._viewportWidth * mul < this._viewportHeight) { let width = this._viewportWidth; - if (!settings.getSettings().upscaleSmallPosts) { + if (!settings.get().upscaleSmallPosts) { width = Math.min(this._post.canvasWidth, width); } this._resize(width, width * mul); } else { let height = this._viewportHeight; - if (!settings.getSettings().upscaleSmallPosts) { + if (!settings.get().upscaleSmallPosts) { height = Math.min(this._post.canvasHeight, height); } this._resize(height / mul, height); @@ -83,7 +83,7 @@ class PostContentControl { const postContentNode = this._template({ post: this._post, }); - if (settings.getSettings().transparencyGrid) { + if (settings.get().transparencyGrid) { postContentNode.classList.add('transparency-grid'); } this._containerNode.appendChild(postContentNode); diff --git a/client/js/controls/post_edit_sidebar_control.js b/client/js/controls/post_edit_sidebar_control.js index 9393c0b..798df58 100644 --- a/client/js/controls/post_edit_sidebar_control.js +++ b/client/js/controls/post_edit_sidebar_control.js @@ -16,7 +16,7 @@ class PostEditSidebarControl { const sourceNode = this._template({ post: this._post, }); - views.showView(this._hostNode, sourceNode); + views.replaceContent(this._hostNode, sourceNode); } }; diff --git a/client/js/controls/post_readonly_sidebar_control.js b/client/js/controls/post_readonly_sidebar_control.js index a51925c..f15ed2b 100644 --- a/client/js/controls/post_readonly_sidebar_control.js +++ b/client/js/controls/post_readonly_sidebar_control.js @@ -25,7 +25,7 @@ class PostReadonlySidebarControl { canViewTags: api.hasPrivilege('tags:view'), }); - views.showView( + views.replaceContent( sourceNode.querySelector('.score-container'), this._scoreTemplate({ score: this._post.score, @@ -33,7 +33,7 @@ class PostReadonlySidebarControl { canScore: api.hasPrivilege('posts:score'), })); - views.showView( + views.replaceContent( sourceNode.querySelector('.fav-container'), this._favTemplate({ favoriteCount: this._post.favoriteCount, @@ -85,7 +85,7 @@ class PostReadonlySidebarControl { 'click', this._eventZoomProxy( () => this._postContentControl.fitHeight())); - views.showView(this._hostNode, sourceNode); + views.replaceContent(this._hostNode, sourceNode); this._syncFitButton(); } diff --git a/client/js/events.js b/client/js/events.js index 177ad27..4ac6c73 100644 --- a/client/js/events.js +++ b/client/js/events.js @@ -1,41 +1,5 @@ 'use strict'; -let pendingMessages = new Map(); -let listeners = new Map(); - -function unlisten(messageClass) { - listeners.set(messageClass, []); -} - -function listen(messageClass, handler) { - if (pendingMessages.has(messageClass)) { - let newPendingMessages = []; - for (let message of pendingMessages.get(messageClass)) { - if (!handler(message)) { - newPendingMessages.push(message); - } - } - pendingMessages.set(messageClass, newPendingMessages); - } - if (!listeners.has(messageClass)) { - listeners.set(messageClass, []); - } - listeners.get(messageClass).push(handler); -} - -function notify(messageClass, message) { - if (!listeners.has(messageClass) || !listeners.get(messageClass).length) { - if (!pendingMessages.has(messageClass)) { - pendingMessages.set(messageClass, []); - } - pendingMessages.get(messageClass).push(message); - return; - } - for (let handler of listeners.get(messageClass)) { - handler(message); - } -} - class EventTarget { constructor() { this.eventTarget = document.createDocumentFragment(); @@ -53,12 +17,6 @@ module.exports = { Success: 'success', Error: 'error', Info: 'info', - Authentication: 'auth', - SettingsChange: 'settings-change', - TagsChange: 'tags-change', - notify: notify, - listen: listen, - unlisten: unlisten, EventTarget: EventTarget, }; diff --git a/client/js/main.js b/client/js/main.js index 58c4fa6..1cca87d 100644 --- a/client/js/main.js +++ b/client/js/main.js @@ -2,7 +2,7 @@ require('./util/polyfill.js'); const misc = require('./util/misc.js'); - +const views = require('./util/views.js'); const router = require('./router.js'); history.scrollRestoration = 'manual'; @@ -13,7 +13,6 @@ router.exit( ctx.state.scrollX = window.scrollX; ctx.state.scrollY = window.scrollY; ctx.save(); - views.unlistenToMessages(); if (misc.confirmPageExit()) { next(); } @@ -33,28 +32,33 @@ router.enter( }); }); +// register controller routes let controllers = []; -controllers.push(require('./controllers/auth_controller.js')); -controllers.push(require('./controllers/post_list_controller.js')); -controllers.push(require('./controllers/post_upload_controller.js')); -controllers.push(require('./controllers/post_controller.js')); -controllers.push(require('./controllers/users_controller.js')); +controllers.push(require('./controllers/home_controller.js')); controllers.push(require('./controllers/help_controller.js')); +controllers.push(require('./controllers/auth_controller.js')); +controllers.push(require('./controllers/password_reset_controller.js')); controllers.push(require('./controllers/comments_controller.js')); controllers.push(require('./controllers/history_controller.js')); -controllers.push(require('./controllers/tags_controller.js')); +controllers.push(require('./controllers/post_controller.js')); +controllers.push(require('./controllers/post_list_controller.js')); +controllers.push(require('./controllers/post_upload_controller.js')); +controllers.push(require('./controllers/tag_controller.js')); +controllers.push(require('./controllers/tag_list_controller.js')); +controllers.push(require('./controllers/tag_categories_controller.js')); controllers.push(require('./controllers/settings_controller.js')); +controllers.push(require('./controllers/user_controller.js')); +controllers.push(require('./controllers/user_list_controller.js')); +controllers.push(require('./controllers/user_registration_controller.js')); -// home defines 404 routes, need to be registered as last -controllers.push(require('./controllers/home_controller.js')); +// 404 controller needs to be registered last +controllers.push(require('./controllers/not_found_controller.js')); -const tags = require('./tags.js'); -const events = require('./events.js'); -const views = require('./util/views.js'); for (let controller of controllers) { - controller.registerRoutes(); + controller(router); } +const tags = require('./tags.js'); const api = require('./api.js'); Promise.all([tags.refreshExport(), api.loginFromCookies()]) .then(() => { @@ -64,9 +68,8 @@ Promise.all([tags.refreshExport(), api.loginFromCookies()]) api.forget(); router.start(); } else { - router.start('/'); - events.notify( - events.Error, + const ctx = router.start('/'); + ctx.controller.showError( 'An error happened while trying to log you in: ' + errorMessage); } diff --git a/client/js/models/settings.js b/client/js/models/settings.js new file mode 100644 index 0000000..31eb862 --- /dev/null +++ b/client/js/models/settings.js @@ -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(); diff --git a/client/js/models/top_navigation.js b/client/js/models/top_navigation.js index b8caeba..73a2ccf 100644 --- a/client/js/models/top_navigation.js +++ b/client/js/models/top_navigation.js @@ -42,15 +42,13 @@ class TopNavigation extends events.EventTarget { } activate(key) { - const event = new Event('activate'); - event.key = key; - if (key) { - event.item = this.get(key); - } else { - event.item = null; - } this.activeItem = null; - this.dispatchEvent(event); + this.dispatchEvent(new CustomEvent('activate', { + detail: { + key: key, + item: key ? this.get(key) : null, + }, + })); } showAll() { diff --git a/client/js/router.js b/client/js/router.js index 5767081..426d6d4 100644 --- a/client/js/router.js +++ b/client/js/router.js @@ -120,7 +120,7 @@ class Router { window.addEventListener('popstate', this._onPopState, false); document.addEventListener(clickEvent, this._onClick, false); const url = location.pathname + location.search + location.hash; - this.replace(url, null, true); + return this.replace(url, null, true); } stop() { diff --git a/client/js/settings.js b/client/js/settings.js deleted file mode 100644 index 433ff13..0000000 --- a/client/js/settings.js +++ /dev/null @@ -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, -}; diff --git a/client/js/tags.js b/client/js/tags.js index 8efca7f..59d508e 100644 --- a/client/js/tags.js +++ b/client/js/tags.js @@ -1,7 +1,6 @@ 'use strict'; const request = require('superagent'); -const events = require('./events.js'); let _tags = null; let _categories = null; @@ -88,10 +87,6 @@ function refreshExport() { }); } -events.listen( - events.TagsChange, - () => { refreshExport(); return true; }); - module.exports = { getAllCategories: getAllCategories, getAllTags: getAllTags, diff --git a/client/js/util/keyboard.js b/client/js/util/keyboard.js index e15e0de..51ee7e2 100644 --- a/client/js/util/keyboard.js +++ b/client/js/util/keyboard.js @@ -1,10 +1,10 @@ 'use strict'; const mousetrap = require('mousetrap'); -const settings = require('../settings.js'); +const settings = require('../models/settings.js'); function bind(hotkey, func) { - if (settings.getSettings().keyboardShortcuts) { + if (settings.get().keyboardShortcuts) { mousetrap.bind(hotkey, func); return true; } diff --git a/client/js/util/views.js b/client/js/util/views.js index 9ad5775..c2404f6 100644 --- a/client/js/util/views.js +++ b/client/js/util/views.js @@ -4,7 +4,6 @@ require('../util/polyfill.js'); const api = require('../api.js'); const templates = require('../templates.js'); const tags = require('../tags.js'); -const events = require('../events.js'); const domParser = new DOMParser(); const misc = require('./misc.js'); @@ -238,26 +237,6 @@ function showInfo(target, message) { return showMessage(target, message, 'info'); } -function unlistenToMessages() { - events.unlisten(events.Success); - events.unlisten(events.Error); - events.unlisten(events.Info); -} - -function listenToMessages(target) { - unlistenToMessages(); - const listen = (eventType, className) => { - events.listen( - eventType, - msg => { - return showMessage(target, msg, className); - }); - }; - listen(events.Success, 'success'); - listen(events.Error, 'error'); - listen(events.Info, 'info'); -} - function clearMessages(target) { const messagesHolder = target.querySelector('.messages'); /* TODO: animate that */ @@ -335,7 +314,7 @@ function enableForm(form) { } } -function showView(target, source) { +function replaceContent(target, source) { while (target.lastChild) { target.removeChild(target.lastChild); } @@ -424,11 +403,9 @@ document.addEventListener('input', e => { module.exports = { htmlToDom: htmlToDom, getTemplate: getTemplate, - showView: showView, + replaceContent: replaceContent, enableForm: enableForm, disableForm: disableForm, - listenToMessages: listenToMessages, - unlistenToMessages: unlistenToMessages, clearMessages: clearMessages, decorateValidator: decorateValidator, makeVoidElement: makeVoidElement, diff --git a/client/js/views/comments_page_view.js b/client/js/views/comments_page_view.js index 9816beb..f9aa62f 100644 --- a/client/js/views/comments_page_view.js +++ b/client/js/views/comments_page_view.js @@ -3,24 +3,25 @@ const views = require('../util/views.js'); const CommentListControl = require('../controls/comment_list_control.js'); -class CommentsPageView { - constructor() { - this._template = views.getTemplate('comments-page'); - } +const template = views.getTemplate('comments-page'); - render(ctx) { - const target = ctx.target; - const source = this._template(ctx); +class CommentsPageView { + constructor(ctx) { + this._hostNode = ctx.hostNode; + this._controls = []; + + const sourceNode = template(ctx); for (let post of ctx.results) { post.comments.sort((a, b) => { return b.id - a.id; }); - new CommentListControl( - source.querySelector( - `.comments-container[data-for="${post.id}"]`), - post.comments); + this._controls.push( + new CommentListControl( + sourceNode.querySelector( + `.comments-container[data-for="${post.id}"]`), + post.comments)); } - views.showView(target, source); + views.replaceContent(this._hostNode, sourceNode); } } diff --git a/client/js/views/empty_view.js b/client/js/views/empty_view.js index 3926865..d58a5de 100644 --- a/client/js/views/empty_view.js +++ b/client/js/views/empty_view.js @@ -2,19 +2,19 @@ const views = require('../util/views.js'); +const template = () => { + return views.htmlToDom( + '
'); +}; + class EmptyView { constructor() { - this._template = () => { - return views.htmlToDom( - '
'); - }; + this._hostNode = document.getElementById('content-holder'); + views.replaceContent(this._hostNode, template()); } - render(ctx) { - const target = document.getElementById('content-holder'); - const source = this._template(); - views.listenToMessages(source); - views.showView(target, source); + showError(message) { + views.showError(this._hostNode, message); } } diff --git a/client/js/views/endless_page_view.js b/client/js/views/endless_page_view.js index e9175a1..9736d81 100644 --- a/client/js/views/endless_page_view.js +++ b/client/js/views/endless_page_view.js @@ -1,55 +1,43 @@ 'use strict'; const router = require('../router.js'); -const events = require('../events.js'); const views = require('../util/views.js'); +const holderTemplate = views.getTemplate('endless-pager'); +const pageTemplate = views.getTemplate('endless-pager-page'); + function _formatUrl(url, page) { return url.replace('{page}', page); } class EndlessPageView { - constructor() { - this._holderTemplate = views.getTemplate('endless-pager'); - this._pageTemplate = views.getTemplate('endless-pager-page'); - } - - render(ctx) { - const target = document.getElementById('content-holder'); - const source = this._holderTemplate(); - const pageHeaderHolder = source.querySelector('.page-header-holder'); - this._pagesHolder = source.querySelector('.pages-holder'); - views.listenToMessages(source); - views.showView(target, source); + constructor(ctx) { + this._hostNode = document.getElementById('content-holder'); this._active = true; this._working = 0; this._init = true; - ctx.headerContext.target = pageHeaderHolder; - if (ctx.headerRenderer) { - ctx.headerRenderer.render(ctx.headerContext); - } - this.threshold = window.innerHeight / 3; this.minPageShown = null; this.maxPageShown = null; this.totalPages = null; this.currentPage = null; + const sourceNode = holderTemplate(); + const pageHeaderHolderNode + = sourceNode.querySelector('.page-header-holder'); + this._pagesHolderNode = sourceNode.querySelector('.pages-holder'); + views.replaceContent(this._hostNode, sourceNode); + + ctx.headerContext.hostNode = pageHeaderHolderNode; + if (ctx.headerRenderer) { + ctx.headerRenderer(ctx.headerContext); + } + this._loadPage(ctx, ctx.searchQuery.page, true); - window.addEventListener('unload', this._scrollToTop, true); this._probePageLoad(ctx); } - unrender() { - this._active = false; - window.removeEventListener('unload', this._scrollToTop, true); - } - - _scrollToTop() { - window.scroll(0, 0); - } - _probePageLoad(ctx) { if (this._active) { window.setTimeout(() => { @@ -115,23 +103,23 @@ class EndlessPageView { this._working--; }); }, response => { - events.notify(events.Error, response.description); + this.showError(response.description); this._working--; }); } _renderPage(ctx, pageNumber, append, response) { if (response.total) { - const pageNode = this._pageTemplate({ + const pageNode = pageTemplate({ page: pageNumber, totalPages: this.totalPages, }); pageNode.setAttribute('data-page', pageNumber); Object.assign(ctx.pageContext, response); - ctx.pageContext.target = pageNode.querySelector( + ctx.pageContext.hostNode = pageNode.querySelector( '.page-content-holder'); - ctx.pageRenderer.render(ctx.pageContext); + ctx.pageRenderer(ctx.pageContext); if (pageNumber < this.minPageShown || this.minPageShown === null) { @@ -143,22 +131,34 @@ class EndlessPageView { } if (append) { - this._pagesHolder.appendChild(pageNode); - /*if (this._init && pageNumber !== 1) { + this._pagesHolderNode.appendChild(pageNode); + if (this._init && pageNumber !== 1) { window.scroll(0, pageNode.getBoundingClientRect().top); - }*/ + } } else { - this._pagesHolder.prependChild(pageNode); + this._pagesHolderNode.prependChild(pageNode); window.scroll( window.scrollX, window.scrollY + pageNode.offsetHeight); } } else if (response.total <= (pageNumber - 1) * response.pageSize) { - events.notify(events.Info, 'No data to show'); + this.showInfo('No data to show'); } this._init = false; } + + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } + + showError(message) { + views.showError(this._hostNode, message); + } + + showInfo(message) { + views.showInfo(this._hostNode, message); + } } module.exports = EndlessPageView; diff --git a/client/js/views/help_view.js b/client/js/views/help_view.js index ffa4afd..e8747ac 100644 --- a/client/js/views/help_view.js +++ b/client/js/views/help_view.js @@ -3,67 +3,62 @@ const config = require('../config.js'); const views = require('../util/views.js'); +const template = views.getTemplate('help'); +const sectionTemplates = { + 'about': views.getTemplate('help-about'), + 'keyboard': views.getTemplate('help-keyboard'), + 'search': views.getTemplate('help-search'), + 'comments': views.getTemplate('help-comments'), + 'tos': views.getTemplate('help-tos'), +}; +const subsectionTemplates = { + 'search': { + 'default': views.getTemplate('help-search-general'), + 'posts': views.getTemplate('help-search-posts'), + 'users': views.getTemplate('help-search-users'), + 'tags': views.getTemplate('help-search-tags'), + }, +}; + class HelpView { - constructor() { - this._template = views.getTemplate('help'); - this._sectionTemplates = {}; - const sectionKeys = ['about', 'keyboard', 'search', 'comments', 'tos']; - for (let section of sectionKeys) { - const templateName = 'help-' + section; - this._sectionTemplates[section] = views.getTemplate(templateName); - } - this._subsectionTemplates = { - 'search': { - 'default': views.getTemplate('help-search-general'), - 'posts': views.getTemplate('help-search-posts'), - 'users': views.getTemplate('help-search-users'), - 'tags': views.getTemplate('help-search-tags'), - } + constructor(section, subsection) { + this._hostNode = document.getElementById('content-holder'); + + const sourceNode = template(); + const ctx = { + name: config.name, }; - } - render(ctx) { - const target = document.getElementById('content-holder'); - const source = this._template(); - - ctx.section = ctx.section || 'about'; - if (ctx.section in this._sectionTemplates) { - views.showView( - source.querySelector('.content'), - this._sectionTemplates[ctx.section]({ - name: config.name, - })); + section = section || 'about'; + if (section in sectionTemplates) { + views.replaceContent( + sourceNode.querySelector('.content'), + sectionTemplates[section](ctx)); } - ctx.subsection = ctx.subsection || 'default'; - if (ctx.section in this._subsectionTemplates && - ctx.subsection in this._subsectionTemplates[ctx.section]) { - views.showView( - source.querySelector('.subcontent'), - this._subsectionTemplates[ctx.section][ctx.subsection]({ - name: config.name, - })); + subsection = subsection || 'default'; + if (section in subsectionTemplates && + subsection in subsectionTemplates[section]) { + views.replaceContent( + sourceNode.querySelector('.subcontent'), + subsectionTemplates[section][subsection](ctx)); } - for (let item of source.querySelectorAll('.primary [data-name]')) { - if (item.getAttribute('data-name') === ctx.section) { - item.className = 'active'; - } else { - item.className = ''; - } + for (let itemNode of + sourceNode.querySelectorAll('.primary [data-name]')) { + itemNode.classList.toggle( + 'active', + itemNode.getAttribute('data-name') === section); } - for (let item of source.querySelectorAll('.secondary [data-name]')) { - if (item.getAttribute('data-name') === ctx.subsection) { - item.className = 'active'; - } else { - item.className = ''; - } + for (let itemNode of + sourceNode.querySelectorAll('.secondary [data-name]')) { + itemNode.classList.toggle( + 'active', + itemNode.getAttribute('data-name') === subsection); } - views.listenToMessages(source); - views.showView(target, source); - + views.replaceContent(this._hostNode, sourceNode); views.scrollToHash(); } } diff --git a/client/js/views/home_view.js b/client/js/views/home_view.js index 25f51c4..72f0d6a 100644 --- a/client/js/views/home_view.js +++ b/client/js/views/home_view.js @@ -1,7 +1,6 @@ 'use strict'; const router = require('../router.js'); -const config = require('../config.js'); const misc = require('../util/misc.js'); const views = require('../util/views.js'); const PostContentControl = require('../controls/post_content_control.js'); @@ -10,48 +9,45 @@ const PostNotesOverlayControl const TagAutoCompleteControl = require('../controls/tag_auto_complete_control.js'); +const template = views.getTemplate('home'); +const statsTemplate = views.getTemplate('home-stats'); + class HomeView { - constructor() { - this._homeTemplate = views.getTemplate('home'); - } + constructor(ctx) { + this._hostNode = document.getElementById('content-holder'); - render(ctx) { - Object.assign(ctx, { - name: config.name, - version: config.meta.version, - buildDate: config.meta.buildDate, - }); - const target = document.getElementById('content-holder'); - const source = this._homeTemplate(ctx); + const sourceNode = template(ctx); + views.replaceContent(this._hostNode, sourceNode); - views.listenToMessages(source); - views.showView(target, source); + if (this._formNode) { + this._formNode.querySelector('input[name=all-posts') + .addEventListener('click', e => this._evtAllPostsClick(e)); - const form = source.querySelector('form'); - if (form) { - form.querySelector('input[name=all-posts') - .addEventListener('click', e => { - e.preventDefault(); - router.show('/posts/'); - }); - - const searchTextInput = form.querySelector( - 'input[name=search-text]'); - new TagAutoCompleteControl(searchTextInput); - form.addEventListener('submit', e => { - e.preventDefault(); - const text = searchTextInput.value; - searchTextInput.blur(); - router.show('/posts/' + misc.formatSearchQuery({text: text})); - }); + this._tagAutoCompleteControl = new TagAutoCompleteControl( + this._searchInputNode); + this._formNode.addEventListener( + 'submit', e => this._evtFormSubmit(e)); } - const postContainerNode = source.querySelector('.post-container'); + } - if (postContainerNode && ctx.featuredPost) { - new PostContentControl( - postContainerNode, - ctx.featuredPost, + showSuccess(text) { + views.showSuccess(this._hostNode, text); + } + + showError(text) { + views.showError(this._hostNode, text); + } + + setStats(stats) { + views.replaceContent(this._statsContainerNode, statsTemplate(stats)); + } + + setFeaturedPost(postInfo) { + if (this._postContainerNode && postInfo.featuredPost) { + this._postContentControl = new PostContentControl( + this._postContainerNode, + postInfo.featuredPost, () => { return [ window.innerWidth * 0.8, @@ -59,11 +55,39 @@ class HomeView { ]; }); - new PostNotesOverlayControl( - postContainerNode.querySelector('.post-overlay'), - ctx.featuredPost); + this._postNotesOverlay = new PostNotesOverlayControl( + this._postContainerNode.querySelector('.post-overlay'), + postInfo.featuredPost); } } + + get _statsContainerNode() { + return this._hostNode.querySelector('.stats-container'); + } + + get _postContainerNode() { + return this._hostNode.querySelector('.post-container'); + } + + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _searchInputNode() { + return this._formNode.querySelector('input[name=search-text]'); + } + + _evtAllPostsClick(e) { + e.preventDefault(); + router.show('/posts/'); + } + + _evtFormSubmit(e) { + e.preventDefault(); + this._searchInputNode.blur(); + router.show('/posts/' + misc.formatSearchQuery({ + text: this._searchInputNode.value})); + } } module.exports = HomeView; diff --git a/client/js/views/login_view.js b/client/js/views/login_view.js index ab07093..a19a0cc 100644 --- a/client/js/views/login_view.js +++ b/client/js/views/login_view.js @@ -1,43 +1,67 @@ 'use strict'; const config = require('../config.js'); +const events = require('../events.js'); const views = require('../util/views.js'); -class LoginView { - constructor() { - this._template = views.getTemplate('login'); - } +const template = views.getTemplate('login'); - render(ctx) { - const target = document.getElementById('content-holder'); - const source = this._template({ +class LoginView extends events.EventTarget { + constructor() { + super(); + this._hostNode = document.getElementById('content-holder'); + + views.replaceContent(this._hostNode, template({ userNamePattern: config.userNameRegex, passwordPattern: config.passwordRegex, canSendMails: config.canSendMails, - }); + })); - const form = source.querySelector('form'); - const userNameField = source.querySelector('#user-name'); - const passwordField = source.querySelector('#user-password'); - const rememberUserField = source.querySelector('#remember-user'); - - views.decorateValidator(form); - userNameField.setAttribute('pattern', config.userNameRegex); - passwordField.setAttribute('pattern', config.passwordRegex); - - form.addEventListener('submit', e => { + views.decorateValidator(this._formNode); + this._userNameFieldNode.setAttribute('pattern', config.userNameRegex); + this._passwordFieldNode.setAttribute('pattern', config.passwordRegex); + this._formNode.addEventListener('submit', e => { e.preventDefault(); - views.clearMessages(target); - views.disableForm(form); - ctx.login( - userNameField.value, - passwordField.value, - rememberUserField.checked) - .always(() => { views.enableForm(form); }); + this.dispatchEvent(new CustomEvent('submit', { + detail: { + name: this._userNameFieldNode.value, + password: this._passwordFieldNode.value, + remember: this._rememberFieldNode.checked, + }, + })); }); + } - views.listenToMessages(source); - views.showView(target, source); + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _userNameFieldNode() { + return this._formNode.querySelector('#user-name'); + } + + get _passwordFieldNode() { + return this._formNode.querySelector('#user-password'); + } + + get _rememberFieldNode() { + return this._formNode.querySelector('#remember-user'); + } + + disableForm() { + views.disableForm(this._formNode); + } + + enableForm() { + views.enableForm(this._formNode); + } + + clearMessages() { + views.clearMessages(this._hostNode); + } + + showError(message) { + views.showError(this._hostNode, message); } } diff --git a/client/js/views/manual_page_view.js b/client/js/views/manual_page_view.js index 58761d5..1d9bb54 100644 --- a/client/js/views/manual_page_view.js +++ b/client/js/views/manual_page_view.js @@ -1,11 +1,13 @@ 'use strict'; const router = require('../router.js'); -const events = require('../events.js'); const keyboard = require('../util/keyboard.js'); const misc = require('../util/misc.js'); const views = require('../util/views.js'); +const holderTemplate = views.getTemplate('manual-pager'); +const navTemplate = views.getTemplate('manual-pager-nav'); + function _formatUrl(url, page) { return url.replace('{page}', page); } @@ -56,28 +58,28 @@ function _getPages(currentPage, pageNumbers, clientUrl) { } class ManualPageView { - constructor() { - this._holderTemplate = views.getTemplate('manual-pager'); - this._navTemplate = views.getTemplate('manual-pager-nav'); - } + constructor(ctx) { + this._hostNode = document.getElementById('content-holder'); - render(ctx) { - const target = document.getElementById('content-holder'); - const source = this._holderTemplate(); - const pageContentHolder = source.querySelector('.page-content-holder'); - const pageHeaderHolder = source.querySelector('.page-header-holder'); - const pageNav = source.querySelector('.page-nav'); + const sourceNode = holderTemplate(); + const pageContentHolderNode + = sourceNode.querySelector('.page-content-holder'); + const pageHeaderHolderNode + = sourceNode.querySelector('.page-header-holder'); + const pageNavNode = sourceNode.querySelector('.page-nav'); const currentPage = ctx.searchQuery.page; - ctx.headerContext.target = pageHeaderHolder; + ctx.headerContext.hostNode = pageHeaderHolderNode; if (ctx.headerRenderer) { - ctx.headerRenderer.render(ctx.headerContext); + ctx.headerRenderer(ctx.headerContext); } + views.replaceContent(this._hostNode, sourceNode); + ctx.requestPage(currentPage).then(response => { Object.assign(ctx.pageContext, response); - ctx.pageContext.target = pageContentHolder; - ctx.pageRenderer.render(ctx.pageContext); + ctx.pageContext.hostNode = pageContentHolderNode; + ctx.pageRenderer(ctx.pageContext); const totalPages = Math.ceil(response.total / response.pageSize); const pageNumbers = _getVisiblePageNumbers(currentPage, totalPages); @@ -95,28 +97,35 @@ class ManualPageView { }); if (response.total) { - views.showView(pageNav, this._navTemplate({ - prevLink: _formatUrl(ctx.clientUrl, currentPage - 1), - nextLink: _formatUrl(ctx.clientUrl, currentPage + 1), - prevLinkActive: currentPage > 1, - nextLinkActive: currentPage < totalPages, - pages: pages, - })); + views.replaceContent( + pageNavNode, + navTemplate({ + prevLink: _formatUrl(ctx.clientUrl, currentPage - 1), + nextLink: _formatUrl(ctx.clientUrl, currentPage + 1), + prevLinkActive: currentPage > 1, + nextLinkActive: currentPage < totalPages, + pages: pages, + })); } - views.listenToMessages(source); - views.showView(target, source); if (response.total <= (currentPage - 1) * response.pageSize) { - events.notify(events.Info, 'No data to show'); + this.showInfo('No data to show'); } }, response => { - views.listenToMessages(source); - views.showView(target, source); - events.notify(events.Error, response.description); + this.showError(response.description); }); } - unrender() { + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } + + showError(message) { + views.showError(this._hostNode, message); + } + + showInfo(message) { + views.showInfo(this._hostNode, message); } } diff --git a/client/js/views/not_found_view.js b/client/js/views/not_found_view.js index 507174c..bb60280 100644 --- a/client/js/views/not_found_view.js +++ b/client/js/views/not_found_view.js @@ -3,15 +3,14 @@ const config = require('../config.js'); const views = require('../util/views.js'); -class NotFoundView { - constructor() { - this._template = views.getTemplate('not-found'); - } +const template = views.getTemplate('not-found'); - render(ctx) { - const target = document.getElementById('content-holder'); - const source = this._template(ctx); - views.showView(target, source); +class NotFoundView { + constructor(path) { + this._hostNode = document.getElementById('content-holder'); + + const sourceNode = template({path: path}); + views.replaceContent(this._hostNode, sourceNode); } } diff --git a/client/js/views/password_reset_view.js b/client/js/views/password_reset_view.js index 0d67a19..73e782d 100644 --- a/client/js/views/password_reset_view.js +++ b/client/js/views/password_reset_view.js @@ -1,31 +1,54 @@ 'use strict'; +const events = require('../events.js'); const views = require('../util/views.js'); -class PasswordResetView { +const template = views.getTemplate('password-reset'); + +class PasswordResetView extends events.EventTarget { constructor() { - this._template = views.getTemplate('password-reset'); + super(); + this._hostNode = document.getElementById('content-holder'); + + views.replaceContent(this._hostNode, template()); + views.decorateValidator(this._formNode); + + this._hostNode.addEventListener('submit', e => { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('submit', { + detail: { + userNameOrEmail: this._userNameOrEmailFieldNode.value, + }, + })); + }); } - render(ctx) { - const target = document.getElementById('content-holder'); - const source = this._template(); + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } - const form = source.querySelector('form'); - const userNameOrEmailField = source.querySelector('#user-name'); + showError(message) { + views.showError(this._hostNode, message); + } - views.decorateValidator(form); + clearMessages() { + views.clearMessages(this._hostNode); + } - form.addEventListener('submit', e => { - e.preventDefault(); - views.clearMessages(target); - views.disableForm(form); - ctx.proceed(userNameOrEmailField.value) - .catch(() => { views.enableForm(form); }); - }); + enableForm() { + views.enableForm(this._formNode); + } - views.listenToMessages(source); - views.showView(target, source); + disableForm() { + views.disableForm(this._formNode); + } + + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _userNameOrEmailFieldNode() { + return this._formNode.querySelector('#user-name'); } } diff --git a/client/js/views/post_view.js b/client/js/views/post_view.js index 9ef86db..6cde228 100644 --- a/client/js/views/post_view.js +++ b/client/js/views/post_view.js @@ -14,20 +14,16 @@ const PostEditSidebarControl = const CommentListControl = require('../controls/comment_list_control.js'); const CommentFormControl = require('../controls/comment_form_control.js'); +const template = views.getTemplate('post'); + class PostView { - constructor() { - this._template = views.getTemplate('post'); - } + constructor(ctx) { + this._hostNode = document.getElementById('content-holder'); - render(ctx) { - const target = document.getElementById('content-holder'); - const source = this._template(ctx); - - const postContainerNode = source.querySelector('.post-container'); - const sidebarNode = source.querySelector('.sidebar'); - - views.listenToMessages(source); - views.showView(target, source); + const sourceNode = template(ctx); + const postContainerNode = sourceNode.querySelector('.post-container'); + const sidebarNode = sourceNode.querySelector('.sidebar'); + views.replaceContent(this._hostNode, sourceNode); const postViewNode = document.body.querySelector('.content-wrapper'); const topNavigationNode = diff --git a/client/js/views/posts_header_view.js b/client/js/views/posts_header_view.js index 349e339..2cee2e4 100644 --- a/client/js/views/posts_header_view.js +++ b/client/js/views/posts_header_view.js @@ -1,33 +1,27 @@ 'use strict'; const router = require('../router.js'); -const settings = require('../settings.js'); +const settings = require('../models/settings.js'); const keyboard = require('../util/keyboard.js'); const misc = require('../util/misc.js'); const views = require('../util/views.js'); const TagAutoCompleteControl = require('../controls/tag_auto_complete_control.js'); +const template = views.getTemplate('posts-header'); + class PostsHeaderView { - constructor() { - this._template = views.getTemplate('posts-header'); - } + constructor(ctx) { + ctx.settings = settings.get(); + this._hostNode = ctx.hostNode; + views.replaceContent(this._hostNode, template(ctx)); - render(ctx) { - ctx.settings = settings.getSettings(); - - const target = ctx.target; - const source = this._template(ctx); - - const form = source.querySelector('form'); - const searchTextInput = form.querySelector('[name=search-text]'); - - if (searchTextInput) { - new TagAutoCompleteControl(searchTextInput); + if (this._queryInputNode) { + new TagAutoCompleteControl(this._queryInputNode); } keyboard.bind('q', () => { - form.querySelector('input').focus(); + this._formNode.querySelector('input').focus(); }); keyboard.bind('p', () => { @@ -38,31 +32,37 @@ class PostsHeaderView { } }); - for (let safetyButton of form.querySelectorAll('.safety')) { + for (let safetyButton of this._formNode.querySelectorAll('.safety')) { safetyButton.addEventListener( 'click', e => this._evtSafetyButtonClick(e, ctx.clientUrl)); } - form.addEventListener( - 'submit', e => this._evtFormSubmit(e, searchTextInput)); + this._formNode.addEventListener( + 'submit', e => this._evtFormSubmit(e, this._queryInputNode)); + } - views.showView(target, source); + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _queryInputNode() { + return this._formNode.querySelector('[name=search-text]'); } _evtSafetyButtonClick(e, url) { e.preventDefault(); e.target.classList.toggle('disabled'); const safety = e.target.getAttribute('data-safety'); - let browsingSettings = settings.getSettings(); + let browsingSettings = settings.get(); browsingSettings.listPosts[safety] = !browsingSettings.listPosts[safety]; - settings.saveSettings(browsingSettings, true); + settings.save(browsingSettings, true); router.show(url.replace(/{page}/, 1)); } - _evtFormSubmit(e, searchTextInput) { + _evtFormSubmit(e, queryInputNode) { e.preventDefault(); - const text = searchTextInput.value; - searchTextInput.blur(); + const text = queryInputNode.value; + queryInputNode.blur(); router.show('/posts/' + misc.formatSearchQuery({text: text})); } } diff --git a/client/js/views/posts_page_view.js b/client/js/views/posts_page_view.js index 4e0d9c8..aeabc02 100644 --- a/client/js/views/posts_page_view.js +++ b/client/js/views/posts_page_view.js @@ -2,15 +2,11 @@ const views = require('../util/views.js'); -class PostsPageView { - constructor() { - this._template = views.getTemplate('posts-page'); - } +const template = views.getTemplate('posts-page'); - render(ctx) { - const target = ctx.target; - const source = this._template(ctx); - views.showView(target, source); +class PostsPageView { + constructor(ctx) { + views.replaceContent(ctx.hostNode, template(ctx)); } } diff --git a/client/js/views/registration_view.js b/client/js/views/registration_view.js index 96e0681..f8d56b3 100644 --- a/client/js/views/registration_view.js +++ b/client/js/views/registration_view.js @@ -1,40 +1,64 @@ 'use strict'; const config = require('../config.js'); +const events = require('../events.js'); const views = require('../util/views.js'); -class RegistrationView { +const template = views.getTemplate('user-registration'); + +class RegistrationView extends events.EventTarget { constructor() { - this._template = views.getTemplate('user-registration'); + super(); + this._hostNode = document.getElementById('content-holder'); + views.replaceContent(this._hostNode, template({ + userNamePattern: config.userNameRegex, + passwordPattern: config.passwordRegex, + })); + views.decorateValidator(this._formNode); + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); } - render(ctx) { - ctx.userNamePattern = config.userNameRegex; - ctx.passwordPattern = config.passwordRegex; + clearMessages() { + views.clearMessages(this._hostNode); + } - const target = document.getElementById('content-holder'); - const source = this._template(ctx); + showError(message) { + views.showError(this._hostNode, message); + } - const form = source.querySelector('form'); - const userNameField = source.querySelector('#user-name'); - const passwordField = source.querySelector('#user-password'); - const emailField = source.querySelector('#user-email'); + enableForm() { + views.enableForm(this._formNode); + } - views.decorateValidator(form); + disableForm() { + views.disableForm(this._formNode); + } - form.addEventListener('submit', e => { - e.preventDefault(); - views.clearMessages(target); - views.disableForm(form); - ctx.register( - userNameField.value, - passwordField.value, - emailField.value) - .always(() => { views.enableForm(form); }); - }); + _evtSubmit(e) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('submit', { + detail: { + name: this._userNameFieldNode.value, + password: this._passwordFieldNode.value, + email: this._emailFieldNode.value, + }, + })); + } - views.listenToMessages(source); - views.showView(target, source); + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _userNameFieldNode() { + return this._formNode.querySelector('#user-name'); + } + + get _passwordFieldNode() { + return this._formNode.querySelector('#user-password'); + } + + get _emailFieldNode() { + return this._formNode.querySelector('#user-email'); } } diff --git a/client/js/views/settings_view.js b/client/js/views/settings_view.js index c448166..c1cbe1d 100644 --- a/client/js/views/settings_view.js +++ b/client/js/views/settings_view.js @@ -1,36 +1,50 @@ 'use strict'; +const events = require('../events.js'); const views = require('../util/views.js'); -class SettingsView { - constructor() { - this._template = views.getTemplate('settings'); +const template = views.getTemplate('settings'); + +class SettingsView extends events.EventTarget { + constructor(ctx) { + super(); + + this._hostNode = document.getElementById('content-holder'); + views.replaceContent( + this._hostNode, template({browsingSettings: ctx.settings})); + views.decorateValidator(this._formNode); + + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); } - render(ctx) { - const target = document.getElementById('content-holder'); - const source = this._template({browsingSettings: ctx.getSettings()}); + clearMessages() { + views.clearMessages(this._hostNode); + } - const form = source.querySelector('form'); - views.decorateValidator(form); + showSuccess(text) { + views.showSuccess(this._hostNode, text); + } - form.addEventListener('submit', e => { - e.preventDefault(); - views.clearMessages(source); - ctx.saveSettings({ - upscaleSmallPosts: - form.querySelector('#upscale-small-posts').checked, - endlessScroll: - form.querySelector('#endless-scroll').checked, - keyboardShortcuts: - form.querySelector('#keyboard-shortcuts').checked, - transparencyGrid: - form.querySelector('#transparency-grid').checked, - }); - }); + _evtSubmit(e) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('change', { + detail: { + settings: { + upscaleSmallPosts: this._formNode.querySelector( + '#upscale-small-posts').checked, + endlessScroll: this._formNode.querySelector( + '#endless-scroll').checked, + keyboardShortcuts: this._formNode.querySelector( + '#keyboard-shortcuts').checked, + transparencyGrid: this._formNode.querySelector( + '#transparency-grid').checked, + }, + }, + })); + } - views.listenToMessages(source); - views.showView(target, source); + get _formNode() { + return this._hostNode.querySelector('form'); } } diff --git a/client/js/views/tag_categories_view.js b/client/js/views/tag_categories_view.js index 2419611..aa72471 100644 --- a/client/js/views/tag_categories_view.js +++ b/client/js/views/tag_categories_view.js @@ -1,32 +1,28 @@ 'use strict'; -const misc = require('../util/misc.js'); const views = require('../util/views.js'); -class TagListHeaderView { - constructor() { - this._template = views.getTemplate('tag-categories'); - } +const template = views.getTemplate('tag-categories'); - render(ctx) { - const target = document.getElementById('content-holder'); - const source = this._template(ctx); +class TagCategoriesView { + constructor(ctx) { + this._hostNode = document.getElementById('content-holder'); + const sourceNode = template(ctx); - const form = source.querySelector('form'); - const newRowTemplate = source.querySelector('.add-template'); - const tableBody = source.querySelector('tbody'); - const addLink = source.querySelector('a.add'); - const saveButton = source.querySelector('button.save'); + const formNode = sourceNode.querySelector('form'); + const newRowTemplate = sourceNode.querySelector('.add-template'); + const tableBodyNode = sourceNode.querySelector('tbody'); + const addLinkNode = sourceNode.querySelector('a.add'); newRowTemplate.parentNode.removeChild(newRowTemplate); - views.decorateValidator(form); + views.decorateValidator(formNode); - for (let row of tableBody.querySelectorAll('tr')) { + for (let row of tableBodyNode.querySelectorAll('tr')) { this._addRowHandlers(row); } - if (addLink) { - addLink.addEventListener('click', e => { + if (addLinkNode) { + addLinkNode.addEventListener('click', e => { e.preventDefault(); let newRow = newRowTemplate.cloneNode(true); tableBody.appendChild(newRow); @@ -34,19 +30,26 @@ class TagListHeaderView { }); } - form.addEventListener('submit', e => { - this._evtSaveButtonClick(e, ctx, target); + formNode.addEventListener('submit', e => { + this._evtSaveButtonClick(e, ctx); }); - views.listenToMessages(source); - views.showView(target, source); + views.replaceContent(this._hostNode, sourceNode); } - _evtSaveButtonClick(e, ctx, target) { + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } + + showError(message) { + views.showError(this._hostNode, message); + } + + _evtSaveButtonClick(e, ctx) { e.preventDefault(); - views.clearMessages(target); - const tableBody = target.querySelector('tbody'); + views.clearMessages(this._hostNode); + const tableBodyNode = this._hostNode.querySelector('tbody'); ctx.getCategories().then(categories => { let existingCategories = {}; @@ -59,7 +62,7 @@ class TagListHeaderView { let removedCategories = []; let changedCategories = []; let allNames = []; - for (let row of tableBody.querySelectorAll('tr')) { + for (let row of tableBodyNode.querySelectorAll('tr')) { let name = row.getAttribute('data-category'); let category = { originalName: name, @@ -127,4 +130,4 @@ class TagListHeaderView { } } -module.exports = TagListHeaderView; +module.exports = TagCategoriesView; diff --git a/client/js/views/tag_delete_view.js b/client/js/views/tag_delete_view.js index 4cceb92..4d27f15 100644 --- a/client/js/views/tag_delete_view.js +++ b/client/js/views/tag_delete_view.js @@ -1,30 +1,52 @@ 'use strict'; +const events = require('../events.js'); const views = require('../util/views.js'); -class TagDeleteView { - constructor() { - this._template = views.getTemplate('tag-delete'); +const template = views.getTemplate('tag-delete'); + +class TagDeleteView extends events.EventTarget { + constructor(ctx) { + super(); + + this._hostNode = ctx.hostNode; + this._tag = ctx.tag; + views.replaceContent(this._hostNode, template(ctx)); + views.decorateValidator(this._formNode); + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); } - render(ctx) { - const target = ctx.target; - const source = this._template(ctx); + clearMessages() { + views.clearMessages(this._hostNode); + } - const form = source.querySelector('form'); + enableForm() { + views.enableForm(this._formNode); + } - views.decorateValidator(form); + disableForm() { + views.disableForm(this._formNode); + } - form.addEventListener('submit', e => { - e.preventDefault(); - views.clearMessages(target); - views.disableForm(form); - ctx.delete(ctx.tag) - .catch(() => { views.enableForm(form); }); - }); + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } - views.listenToMessages(source); - views.showView(target, source); + showError(message) { + views.showError(this._hostNode, message); + } + + _evtSubmit(e) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('submit', { + detail: { + tag: this._tag, + }, + })); + } + + get _formNode() { + return this._hostNode.querySelector('form'); } } diff --git a/client/js/views/tag_merge_view.js b/client/js/views/tag_merge_view.js index b18f951..711716c 100644 --- a/client/js/views/tag_merge_view.js +++ b/client/js/views/tag_merge_view.js @@ -1,40 +1,66 @@ 'use strict'; const config = require('../config.js'); +const events = require('../events.js'); const views = require('../util/views.js'); const TagAutoCompleteControl = require('../controls/tag_auto_complete_control.js'); -class TagMergeView { - constructor() { - this._template = views.getTemplate('tag-merge'); - } +const template = views.getTemplate('tag-merge'); - render(ctx) { +class TagMergeView extends events.EventTarget { + constructor(ctx) { + super(); + + this._tag = ctx.tag; + this._hostNode = ctx.hostNode; ctx.tagNamePattern = config.tagNameRegex; + views.replaceContent(this._hostNode, template(ctx)); - const target = ctx.target; - const source = this._template(ctx); - - const form = source.querySelector('form'); - const otherTagField = source.querySelector('.target input'); - - views.decorateValidator(form); - if (otherTagField) { - new TagAutoCompleteControl(otherTagField); + views.decorateValidator(this._formNode); + if (this._targetTagFieldNode) { + new TagAutoCompleteControl(this._targetTagFieldNode); } - form.addEventListener('submit', e => { + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + } - e.preventDefault(); - views.clearMessages(target); - views.disableForm(form); - ctx.mergeTo(otherTagField.value) - .catch(() => { views.enableForm(form); }); - }); + clearMessages() { + views.clearMessages(this._hostNode); + } - views.listenToMessages(source); - views.showView(target, source); + enableForm() { + views.enableForm(this._formNode); + } + + disableForm() { + views.disableForm(this._formNode); + } + + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } + + showError(message) { + views.showError(this._hostNode, message); + } + + _evtSubmit(e) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('submit', { + detail: { + tag: this._tag, + targetTagName: this._targetTagFieldNode.value, + }, + })); + } + + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _targetTagFieldNode() { + return this._formNode.querySelector('.target input'); } } diff --git a/client/js/views/tag_summary_view.js b/client/js/views/tag_summary_view.js index 5fdcfcf..964512d 100644 --- a/client/js/views/tag_summary_view.js +++ b/client/js/views/tag_summary_view.js @@ -1,54 +1,89 @@ 'use strict'; const config = require('../config.js'); +const events = require('../events.js'); const views = require('../util/views.js'); const TagInputControl = require('../controls/tag_input_control.js'); -function split(str) { +const template = views.getTemplate('tag-summary'); + +function _split(str) { return str.split(/\s+/).filter(s => s); } -class TagSummaryView { - constructor() { - this._template = views.getTemplate('tag-summary'); - } +class TagSummaryView extends events.EventTarget { + constructor(ctx) { + super(); - render(ctx) { + this._tag = ctx.tag; + this._hostNode = ctx.hostNode; const baseRegex = config.tagNameRegex.replace(/[\^\$]/g, ''); ctx.tagNamesPattern = '^((' + baseRegex + ')\\s+)*(' + baseRegex + ')$'; + views.replaceContent(this._hostNode, template(ctx)); - const target = ctx.target; - const source = this._template(ctx); + views.decorateValidator(this._formNode); - const form = source.querySelector('form'); - const namesField = source.querySelector('.names input'); - const categoryField = source.querySelector('.category select'); - const implicationsField = source.querySelector('.implications input'); - const suggestionsField = source.querySelector('.suggestions input'); - - if (implicationsField) { - new TagInputControl(implicationsField); + if (this._implicationsFieldNode) { + new TagInputControl(this._implicationsFieldNode); } - if (suggestionsField) { - new TagInputControl(suggestionsField); + if (this._suggestionsFieldNode) { + new TagInputControl(this._suggestionsFieldNode); } - views.decorateValidator(form); + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + } - form.addEventListener('submit', e => { - e.preventDefault(); - views.clearMessages(target); - views.disableForm(form); - ctx.save({ - names: split(namesField.value), - category: categoryField.value, - implications: split(implicationsField.value), - suggestions: split(suggestionsField.value), - }).always(() => { views.enableForm(form); }); - }); + clearMessages() { + views.clearMessages(this._hostNode); + } - views.listenToMessages(source); - views.showView(target, source); + enableForm() { + views.enableForm(this._formNode); + } + + disableForm() { + views.disableForm(this._formNode); + } + + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } + + showError(message) { + views.showError(this._hostNode, message); + } + + _evtSubmit(e) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('submit', { + detail: { + tag: this._tag, + names: _split(this._namesFieldNode.value), + category: this._categoryFieldNode.value, + implications: _split(this._implicationsFieldNode.value), + suggestions: _split(this._suggestionsFieldNode.value), + }, + })); + } + + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _namesFieldNode() { + return this._formNode.querySelector('.names input'); + } + + get _categoryFieldNode() { + return this._formNode.querySelector('.category select'); + } + + get _implicationsFieldNode() { + return this._formNode.querySelector('.implications input'); + } + + get _suggestionsFieldNode() { + return this._formNode.querySelector('.suggestions input'); } } diff --git a/client/js/views/tag_view.js b/client/js/views/tag_view.js index eea7240..11c5ecc 100644 --- a/client/js/views/tag_view.js +++ b/client/js/views/tag_view.js @@ -1,25 +1,22 @@ 'use strict'; +const events = require('../events.js'); const views = require('../util/views.js'); const TagSummaryView = require('./tag_summary_view.js'); const TagMergeView = require('./tag_merge_view.js'); const TagDeleteView = require('./tag_delete_view.js'); -class TagView { - constructor() { - this._template = views.getTemplate('tag'); - this._summaryView = new TagSummaryView(); - this._mergeView = new TagMergeView(); - this._deleteView = new TagDeleteView(); - } +const template = views.getTemplate('tag'); - render(ctx) { - const target = document.getElementById('content-holder'); - const source = this._template(ctx); +class TagView extends events.EventTarget { + constructor(ctx) { + super(); + this._hostNode = document.getElementById('content-holder'); ctx.section = ctx.section || 'summary'; + views.replaceContent(this._hostNode, template(ctx)); - for (let item of source.querySelectorAll('[data-name]')) { + for (let item of this._hostNode.querySelectorAll('[data-name]')) { if (item.getAttribute('data-name') === ctx.section) { item.className = 'active'; } else { @@ -27,21 +24,47 @@ class TagView { } } - let view = null; + ctx.hostNode = this._hostNode.querySelector('.tag-content-holder'); if (ctx.section == 'merge') { - view = this._mergeView; + this._view = new TagMergeView(ctx); + this._view.addEventListener('submit', e => { + this.dispatchEvent( + new CustomEvent('merge', {detail: e.detail})); + }); } else if (ctx.section == 'delete') { - view = this._deleteView; + this._view = new TagDeleteView(ctx); + this._view.addEventListener('submit', e => { + this.dispatchEvent( + new CustomEvent('delete', {detail: e.detail})); + }); } else { - view = this._summaryView; + this._view = new TagSummaryView(ctx); + this._view.addEventListener('submit', e => { + this.dispatchEvent( + new CustomEvent('change', {detail: e.detail})); + }); } - ctx.target = source.querySelector('.tag-content-holder'); - view.render(ctx); + } - views.listenToMessages(source); - views.showView(target, source); + clearMessages() { + this._view.clearMessages(); + } + + enableForm() { + this._view.enableForm(); + } + + disableForm() { + this._view.disableForm(); + } + + showSuccess(message) { + this._view.showSuccess(message); + } + + showError(message) { + this._view.showError(message); } } module.exports = TagView; - diff --git a/client/js/views/tags_header_view.js b/client/js/views/tags_header_view.js index e175bf5..61ea8d2 100644 --- a/client/js/views/tags_header_view.js +++ b/client/js/views/tags_header_view.js @@ -7,34 +7,39 @@ const views = require('../util/views.js'); const TagAutoCompleteControl = require('../controls/tag_auto_complete_control.js'); +const template = views.getTemplate('tags-header'); + class TagsHeaderView { - constructor() { - this._template = views.getTemplate('tags-header'); - } + constructor(ctx) { + this._hostNode = ctx.hostNode; + views.replaceContent(this._hostNode, template(ctx)); - render(ctx) { - const target = ctx.target; - const source = this._template(ctx); - - const form = source.querySelector('form'); - const searchTextInput = form.querySelector('[name=search-text]'); - - if (searchTextInput) { - new TagAutoCompleteControl(searchTextInput); + if (this._queryInputNode) { + new TagAutoCompleteControl(this._queryInputNode); } keyboard.bind('q', () => { form.querySelector('input').focus(); }); - form.addEventListener('submit', e => { - e.preventDefault(); - const text = searchTextInput.value; - searchTextInput.blur(); - router.show('/tags/' + misc.formatSearchQuery({text: text})); - }); + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + } - views.showView(target, source); + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _queryInputNode() { + return this._hostNode.querySelector('[name=search-text]'); + } + + _evtSubmit(e) { + e.preventDefault(); + this._queryInputNode.blur(); + router.show( + '/tags/' + misc.formatSearchQuery({ + text: this._queryInputNode.value, + })); } } diff --git a/client/js/views/tags_page_view.js b/client/js/views/tags_page_view.js index 26d3630..cd335a4 100644 --- a/client/js/views/tags_page_view.js +++ b/client/js/views/tags_page_view.js @@ -2,15 +2,11 @@ const views = require('../util/views.js'); -class TagsPageView { - constructor() { - this._template = views.getTemplate('tags-page'); - } +const template = views.getTemplate('tags-page'); - render(ctx) { - const target = ctx.target; - const source = this._template(ctx); - views.showView(target, source); +class TagsPageView { + constructor(ctx) { + views.replaceContent(ctx.hostNode, template(ctx)); } } diff --git a/client/js/views/top_navigation_view.js b/client/js/views/top_navigation_view.js index 0ef66a8..be703fc 100644 --- a/client/js/views/top_navigation_view.js +++ b/client/js/views/top_navigation_view.js @@ -2,24 +2,19 @@ const views = require('../util/views.js'); +const template = views.getTemplate('top-navigation'); + class TopNavigationView { constructor() { - this._template = views.getTemplate('top-navigation'); - this._navHolder = document.getElementById('top-navigation-holder'); - this._lastCtx = null; + this._hostNode = document.getElementById('top-navigation-holder'); } render(ctx) { - this._lastCtx = ctx; - const target = this._navHolder; - const source = this._template(ctx); - views.showView(this._navHolder, source); + views.replaceContent(this._hostNode, template(ctx)); } activate(key) { - const allItemNodes = document.querySelectorAll( - '#top-navigation-holder [data-name]'); - for (let itemNode of allItemNodes) { + for (let itemNode of this._hostNode.querySelectorAll('[data-name]')) { itemNode.classList.toggle( 'active', itemNode.getAttribute('data-name') === key); } diff --git a/client/js/views/user_delete_view.js b/client/js/views/user_delete_view.js index 8fd07cd..bdaf9e6 100644 --- a/client/js/views/user_delete_view.js +++ b/client/js/views/user_delete_view.js @@ -1,30 +1,54 @@ 'use strict'; +const events = require('../events.js'); const views = require('../util/views.js'); -class UserDeleteView { - constructor() { - this._template = views.getTemplate('user-delete'); +const template = views.getTemplate('user-delete'); + +class UserDeleteView extends events.EventTarget { + constructor(ctx) { + super(); + + this._user = ctx.user; + this._hostNode = ctx.hostNode; + views.replaceContent(this._hostNode, template(ctx)); + views.decorateValidator(this._formNode); + + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); } - render(ctx) { - const target = ctx.target; - const source = this._template(ctx); + clearMessages() { + views.clearMessages(this._hostNode); + } - const form = source.querySelector('form'); + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } - views.decorateValidator(form); + showError(message) { + views.showError(this._hostNode, message); + } - form.addEventListener('submit', e => { - e.preventDefault(); - views.clearMessages(target); - views.disableForm(form); - ctx.delete() - .catch(() => { views.enableForm(form); }); - }); + enableForm() { + views.enableForm(this._formNode); + } + + disableForm() { + views.disableForm(this._formNode); + } + + _evtSubmit(e) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('submit', { + detail: { + user: this._user, + }, + })); + } + + get _formNode() { + return this._hostNode.querySelector('form'); - views.listenToMessages(source); - views.showView(target, source); } } diff --git a/client/js/views/user_edit_view.js b/client/js/views/user_edit_view.js index ac2ad20..75dfa05 100644 --- a/client/js/views/user_edit_view.js +++ b/client/js/views/user_edit_view.js @@ -1,63 +1,102 @@ 'use strict'; const config = require('../config.js'); +const events = require('../events.js'); const views = require('../util/views.js'); const FileDropperControl = require('../controls/file_dropper_control.js'); -class UserEditView { - constructor() { - this._template = views.getTemplate('user-edit'); - } +const template = views.getTemplate('user-edit'); + +class UserEditView extends events.EventTarget { + constructor(ctx) { + super(); - render(ctx) { ctx.userNamePattern = config.userNameRegex + /|^$/.source; ctx.passwordPattern = config.passwordRegex + /|^$/.source; - const target = ctx.target; - const source = this._template(ctx); + this._user = ctx.user; + this._hostNode = ctx.hostNode; + views.replaceContent(this._hostNode, template(ctx)); + views.decorateValidator(this._formNode); - const form = source.querySelector('form'); - const avatarContentField = source.querySelector('#avatar-content'); - - views.decorateValidator(form); - - let avatarContent = null; - if (avatarContentField) { + this._avatarContent = null; + if (this._avatarContentFieldNode) { new FileDropperControl( - avatarContentField, + this._avatarContentFieldNode, { lock: true, resolve: files => { - source.querySelector( + this._hostNode.querySelector( '[name=avatar-style][value=manual]').checked = true; - avatarContent = files[0]; + this._avatarContent = files[0]; }, }); } - form.addEventListener('submit', e => { - const rankField = source.querySelector('#user-rank'); - const emailField = source.querySelector('#user-email'); - const userNameField = source.querySelector('#user-name'); - const passwordField = source.querySelector('#user-password'); - const avatarStyleField = source.querySelector( - '[name=avatar-style]:checked'); + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + } - e.preventDefault(); - views.clearMessages(target); - views.disableForm(form); - ctx.edit({ - name: userNameField.value, - password: passwordField.value, - email: emailField.value, - rank: rankField.value, - avatarStyle: avatarStyleField.value, - avatarContent: avatarContent}) - .always(() => { views.enableForm(form); }); - }); + clearMessages() { + views.clearMessages(this._hostNode); + } - views.listenToMessages(source); - views.showView(target, source); + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } + + showError(message) { + views.showError(this._hostNode, message); + } + + enableForm() { + views.enableForm(this._formNode); + } + + disableForm() { + views.disableForm(this._formNode); + } + + _evtSubmit(e) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('submit', { + detail: { + user: this._user, + name: this._userNameFieldNode.value, + password: this._passwordFieldNode.value, + email: this._emailFieldNode.value, + rank: this._rankFieldNode.value, + avatarStyle: this._avatarStyleFieldNode.value, + avatarContent: this._avatarContent, + }, + })); + } + + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _rankFieldNode() { + return this._formNode.querySelector('#user-rank'); + } + + get _emailFieldNode() { + return this._formNode.querySelector('#user-email'); + } + + get _userNameFieldNode() { + return this._formNode.querySelector('#user-name'); + } + + get _passwordFieldNode() { + return this._formNode.querySelector('#user-password'); + } + + get _avatarContentFieldNode() { + return this._formNode.querySelector('#avatar-content'); + } + + get _avatarStyleFieldNode() { + return this._formNode.querySelector('[name=avatar-style]:checked'); } } diff --git a/client/js/views/user_summary_view.js b/client/js/views/user_summary_view.js index a5109c8..d8463f3 100644 --- a/client/js/views/user_summary_view.js +++ b/client/js/views/user_summary_view.js @@ -2,16 +2,12 @@ const views = require('../util/views.js'); -class UserSummaryView { - constructor() { - this._template = views.getTemplate('user-summary'); - } +const template = views.getTemplate('user-summary'); - render(ctx) { - const target = ctx.target; - const source = this._template(ctx); - views.listenToMessages(source); - views.showView(target, source); +class UserSummaryView { + constructor(ctx) { + this._hostNode = ctx.hostNode; + views.replaceContent(this._hostNode, template(ctx)); } } diff --git a/client/js/views/user_view.js b/client/js/views/user_view.js index da94ad4..734501d 100644 --- a/client/js/views/user_view.js +++ b/client/js/views/user_view.js @@ -1,25 +1,22 @@ 'use strict'; +const events = require('../events.js'); const views = require('../util/views.js'); const UserDeleteView = require('./user_delete_view.js'); const UserSummaryView = require('./user_summary_view.js'); const UserEditView = require('./user_edit_view.js'); -class UserView { - constructor() { - this._template = views.getTemplate('user'); - this._deleteView = new UserDeleteView(); - this._summaryView = new UserSummaryView(); - this._editView = new UserEditView(); - } +const template = views.getTemplate('user'); - render(ctx) { - const target = document.getElementById('content-holder'); - const source = this._template(ctx); +class UserView extends events.EventTarget { + constructor(ctx) { + super(); + this._hostNode = document.getElementById('content-holder'); ctx.section = ctx.section || 'summary'; + views.replaceContent(this._hostNode, template(ctx)); - for (let item of source.querySelectorAll('[data-name]')) { + for (let item of this._hostNode.querySelectorAll('[data-name]')) { if (item.getAttribute('data-name') === ctx.section) { item.className = 'active'; } else { @@ -27,19 +24,42 @@ class UserView { } } - let view = null; + ctx.hostNode = this._hostNode.querySelector('#user-content-holder'); if (ctx.section == 'edit') { - view = this._editView; + this._view = new UserEditView(ctx); + this._view.addEventListener('submit', e => { + this.dispatchEvent( + new CustomEvent('change', {detail: e.detail})); + }); } else if (ctx.section == 'delete') { - view = this._deleteView; + this._view = new UserDeleteView(ctx); + this._view.addEventListener('submit', e => { + this.dispatchEvent( + new CustomEvent('delete', {detail: e.detail})); + }); } else { - view = this._summaryView; + this._view = new UserSummaryView(ctx); } - ctx.target = source.querySelector('#user-content-holder'); - view.render(ctx); + } - views.listenToMessages(source); - views.showView(target, source); + clearMessages() { + this._view.clearMessages(); + } + + showSuccess(message) { + this._view.showSuccess(message); + } + + showError(message) { + this._view.showError(message); + } + + enableForm() { + this._view.enableForm(); + } + + disableForm() { + this._view.disableForm(); } } diff --git a/client/js/views/users_header_view.js b/client/js/views/users_header_view.js index 601dfd0..2284ba7 100644 --- a/client/js/views/users_header_view.js +++ b/client/js/views/users_header_view.js @@ -5,30 +5,35 @@ const keyboard = require('../util/keyboard.js'); const misc = require('../util/misc.js'); const views = require('../util/views.js'); +const template = views.getTemplate('users-header'); + class UsersHeaderView { - constructor() { - this._template = views.getTemplate('users-header'); - } - - render(ctx) { - const target = ctx.target; - const source = this._template(ctx); - - const form = source.querySelector('form'); + constructor(ctx) { + this._hostNode = ctx.hostNode; + views.replaceContent(this._hostNode, template(ctx)); keyboard.bind('q', () => { - form.querySelector('input').focus(); + this._formNode.querySelector('input').focus(); }); - form.addEventListener('submit', e => { - e.preventDefault(); - const searchTextInput = form.querySelector('[name=search-text]'); - const text = searchTextInput.value; - searchTextInput.blur(); - router.show('/users/' + misc.formatSearchQuery({text: text})); - }); + this._formNode.addEventListener('submit', e => this._evtSubmit(e)); + } - views.showView(target, source); + get _formNode() { + return this._hostNode.querySelector('form'); + } + + get _queryInputNode() { + return this._formNode.querySelector('[name=search-text]'); + } + + _evtSubmit(e) { + e.preventDefault(); + this._queryInputNode.blur(); + router.show( + '/users/' + misc.formatSearchQuery({ + text: this._queryInputNode.value, + })); } } diff --git a/client/js/views/users_page_view.js b/client/js/views/users_page_view.js index c7230f5..f772d4e 100644 --- a/client/js/views/users_page_view.js +++ b/client/js/views/users_page_view.js @@ -2,15 +2,11 @@ const views = require('../util/views.js'); -class UsersPageView { - constructor() { - this._template = views.getTemplate('users-page'); - } +const template = views.getTemplate('users-page'); - render(ctx) { - const target = ctx.target; - const source = this._template(ctx); - views.showView(target, source); +class UsersPageView { + constructor(ctx) { + views.replaceContent(ctx.hostNode, template(ctx)); } }