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