diff --git a/client/css/colors.styl b/client/css/colors.styl index 1a5996e..5c2ad11 100644 --- a/client/css/colors.styl +++ b/client/css/colors.styl @@ -1,6 +1,6 @@ $main-color = #24AADD $window-color = white -$top-nav-color = #F5F5F5 +$top-navigation-color = #F5F5F5 $text-color = #111 $inactive-link-color = #888 $line-color = #DDD diff --git a/client/css/comments.styl b/client/css/comments.styl index 81b168e..1ec0cd5 100644 --- a/client/css/comments.styl +++ b/client/css/comments.styl @@ -75,7 +75,7 @@ line-height: 16pt vertical-align: middle margin-bottom: 0.5em - background: $top-nav-color + background: $top-navigation-color padding: 0.2em 0.5em .date, .score-container, .edit, .delete diff --git a/client/css/main.styl b/client/css/main.styl index b08d81e..60d985b 100644 --- a/client/css/main.styl +++ b/client/css/main.styl @@ -68,7 +68,7 @@ form .fa-question-circle-o >*:first-child, form h1 margin-top: 0 >.content-wrapper:not(.transparent) - background: $top-nav-color + background: $top-navigation-color padding: 2vw hr @@ -113,8 +113,8 @@ nav background: $focused-tab-background-color outline: 0 - top-nav - background: $top-nav-color + top-navigation + background: $top-navigation-color margin: 0 ul display: block @@ -207,7 +207,7 @@ a .access-key top: 50% right: 0 height: 3px - background: $top-nav-color + background: $top-navigation-color z-index: 1 span position: relative diff --git a/client/css/tags.styl b/client/css/tags.styl index ff98ae6..84954f8 100644 --- a/client/css/tags.styl +++ b/client/css/tags.styl @@ -7,11 +7,11 @@ text-align: left line-height: 1.3em tr:hover td - background: $top-nav-color + background: $top-navigation-color th, td padding: 0.1em 0.5em th - background: $top-nav-color + background: $top-navigation-color .names width: 28% .implications diff --git a/client/css/users.styl b/client/css/users.styl index 49bc384..3cd1df9 100644 --- a/client/css/users.styl +++ b/client/css/users.styl @@ -80,7 +80,7 @@ margin: 0 0.5em 1em 0.5em padding: 0.75em vertical-align: top - background: $top-nav-color + background: $top-navigation-color text-align: left .wrapper display: flex diff --git a/client/html/index.htm b/client/html/index.htm index 0c2ca61..8eeb8b8 100644 --- a/client/html/index.htm +++ b/client/html/index.htm @@ -9,7 +9,7 @@
- + diff --git a/client/html/top_nav.tpl b/client/html/top_navigation.tpl similarity index 62% rename from client/html/top_nav.tpl rename to client/html/top_navigation.tpl index 860b5f8..02f6702 100644 --- a/client/html/top_nav.tpl +++ b/client/html/top_navigation.tpl @@ -1,14 +1,14 @@ - + --> diff --git a/client/js/controllers/auth_controller.js b/client/js/controllers/auth_controller.js index b8ca289..b1a52b2 100644 --- a/client/js/controllers/auth_controller.js +++ b/client/js/controllers/auth_controller.js @@ -3,7 +3,7 @@ const router = require('../router.js'); const api = require('../api.js'); const events = require('../events.js'); -const topNavController = require('../controllers/top_nav_controller.js'); +const TopNavigation = require('../models/top_navigation.js'); const LoginView = require('../views/login_view.js'); const PasswordResetView = require('../views/password_reset_view.js'); @@ -32,7 +32,7 @@ class AuthController { _loginRoute() { api.forget(); - topNavController.activate('login'); + TopNavigation.activate('login'); this._loginView.render({ login: (name, password, doRemember) => { return new Promise((resolve, reject) => { @@ -58,7 +58,7 @@ class AuthController { } _passwordResetRoute() { - topNavController.activate('login'); + TopNavigation.activate('login'); this._passwordResetView.render({ proceed: (...args) => { return this._passwordReset(...args); diff --git a/client/js/controllers/comments_controller.js b/client/js/controllers/comments_controller.js index 5244a3b..6ab819b 100644 --- a/client/js/controllers/comments_controller.js +++ b/client/js/controllers/comments_controller.js @@ -3,8 +3,8 @@ const api = require('../api.js'); const router = require('../router.js'); const misc = require('../util/misc.js'); -const topNavController = require('../controllers/top_nav_controller.js'); const pageController = require('../controllers/page_controller.js'); +const TopNavigation = require('../models/top_navigation.js'); const CommentsPageView = require('../views/comments_page_view.js'); const EmptyView = require('../views/empty_view.js'); @@ -18,7 +18,7 @@ class CommentsController { } _listCommentsRoute(ctx) { - topNavController.activate('comments'); + TopNavigation.activate('comments'); pageController.run({ searchQuery: ctx.searchQuery, diff --git a/client/js/controllers/help_controller.js b/client/js/controllers/help_controller.js index 73c1896..9c199a6 100644 --- a/client/js/controllers/help_controller.js +++ b/client/js/controllers/help_controller.js @@ -1,7 +1,7 @@ 'use strict'; const router = require('../router.js'); -const topNavController = require('../controllers/top_nav_controller.js'); +const TopNavigation = require('../models/top_navigation.js'); const HelpView = require('../views/help_view.js'); class HelpController { @@ -24,7 +24,7 @@ class HelpController { } _showHelpRoute(section, subsection) { - topNavController.activate('help'); + TopNavigation.activate('help'); this._helpView.render({ section: section, subsection: subsection, diff --git a/client/js/controllers/history_controller.js b/client/js/controllers/history_controller.js index 0b7dc85..1793fe8 100644 --- a/client/js/controllers/history_controller.js +++ b/client/js/controllers/history_controller.js @@ -1,7 +1,7 @@ 'use strict'; const router = require('../router.js'); -const topNavController = require('../controllers/top_nav_controller.js'); +const TopNavigation = require('../models/top_navigation.js'); class HistoryController { registerRoutes() { @@ -11,7 +11,7 @@ class HistoryController { } _listHistoryRoute() { - topNavController.activate(''); + TopNavigation.activate(''); } } diff --git a/client/js/controllers/home_controller.js b/client/js/controllers/home_controller.js index 76987b9..11151c3 100644 --- a/client/js/controllers/home_controller.js +++ b/client/js/controllers/home_controller.js @@ -3,7 +3,7 @@ const router = require('../router.js'); const api = require('../api.js'); const events = require('../events.js'); -const topNavController = require('../controllers/top_nav_controller.js'); +const TopNavigation = require('../models/top_navigation.js'); const HomeView = require('../views/home_view.js'); const NotFoundView = require('../views/not_found_view.js'); @@ -23,7 +23,7 @@ class HomeController { } _indexRoute() { - topNavController.activate('home'); + TopNavigation.activate('home'); api.get('/info') .then(response => { @@ -45,7 +45,7 @@ class HomeController { } _notFoundRoute(ctx) { - topNavController.activate(''); + TopNavigation.activate(''); this._notFoundView.render({path: ctx.canonicalPath}); } } diff --git a/client/js/controllers/posts_controller.js b/client/js/controllers/posts_controller.js index 4dbdae2..2bd0f44 100644 --- a/client/js/controllers/posts_controller.js +++ b/client/js/controllers/posts_controller.js @@ -5,8 +5,8 @@ const api = require('../api.js'); const settings = require('../settings.js'); const events = require('../events.js'); const misc = require('../util/misc.js'); -const topNavController = require('../controllers/top_nav_controller.js'); const pageController = require('../controllers/page_controller.js'); +const TopNavigation = require('../models/top_navigation.js'); const PostsHeaderView = require('../views/posts_header_view.js'); const PostsPageView = require('../views/posts_page_view.js'); const PostView = require('../views/post_view.js'); @@ -37,12 +37,12 @@ class PostsController { } _uploadPostsRoute() { - topNavController.activate('upload'); + TopNavigation.activate('upload'); this._emptyView.render(); } _listPostsRoute(ctx) { - topNavController.activate('posts'); + TopNavigation.activate('posts'); pageController.run({ searchQuery: ctx.searchQuery, @@ -67,7 +67,7 @@ class PostsController { } _showPostRoute(id, editMode) { - topNavController.activate('posts'); + TopNavigation.activate('posts'); Promise.all([ api.get('/post/' + id), api.get(`/post/${id}/around?fields=id&query=` + diff --git a/client/js/controllers/settings_controller.js b/client/js/controllers/settings_controller.js index d7bcd7e..360b480 100644 --- a/client/js/controllers/settings_controller.js +++ b/client/js/controllers/settings_controller.js @@ -2,7 +2,7 @@ const router = require('../router.js'); const settings = require('../settings.js'); -const topNavController = require('../controllers/top_nav_controller.js'); +const TopNavigation = require('../models/top_navigation.js'); const SettingsView = require('../views/settings_view.js'); class SettingsController { @@ -15,7 +15,7 @@ class SettingsController { } _settingsRoute() { - topNavController.activate('settings'); + TopNavigation.activate('settings'); this._settingsView.render({ getSettings: () => settings.getSettings(), saveSettings: newSettings => settings.saveSettings(newSettings), diff --git a/client/js/controllers/tags_controller.js b/client/js/controllers/tags_controller.js index a5fa50d..fef4977 100644 --- a/client/js/controllers/tags_controller.js +++ b/client/js/controllers/tags_controller.js @@ -5,8 +5,8 @@ const api = require('../api.js'); const tags = require('../tags.js'); const events = require('../events.js'); const misc = require('../util/misc.js'); -const topNavController = require('../controllers/top_nav_controller.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'); @@ -114,7 +114,7 @@ class TagsController { } _show(tag, section) { - topNavController.activate('tags'); + TopNavigation.activate('tags'); const categories = {}; for (let category of tags.getAllCategories()) { categories[category.name] = category.name; @@ -174,7 +174,7 @@ class TagsController { } _tagCategoriesRoute(ctx, next) { - topNavController.activate('tags'); + TopNavigation.activate('tags'); api.get('/tag-categories/').then(response => { this._tagCategoriesView.render({ tagCategories: response.results, @@ -201,7 +201,7 @@ class TagsController { } _listTagsRoute(ctx, next) { - topNavController.activate('tags'); + TopNavigation.activate('tags'); pageController.run({ searchQuery: ctx.searchQuery, diff --git a/client/js/controllers/top_nav_controller.js b/client/js/controllers/top_nav_controller.js deleted file mode 100644 index 1548558..0000000 --- a/client/js/controllers/top_nav_controller.js +++ /dev/null @@ -1,94 +0,0 @@ -'use strict'; - -const api = require('../api.js'); -const events = require('../events.js'); -const TopNavView = require('../views/top_nav_view.js'); - -function _createNavigationItemMap() { - const ret = new Map(); - ret.set('home', new NavigationItem('H', 'Home', '/')); - ret.set('posts', new NavigationItem('P', 'Posts', '/posts')); - ret.set('upload', new NavigationItem('U', 'Upload', '/upload')); - ret.set('comments', new NavigationItem('C', 'Comments', '/comments')); - ret.set('tags', new NavigationItem('T', 'Tags', '/tags')); - ret.set('users', new NavigationItem('S', 'Users', '/users')); - ret.set('account', new NavigationItem('A', 'Account', '/user/{me}')); - ret.set('register', new NavigationItem('R', 'Register', '/register')); - ret.set('login', new NavigationItem('L', 'Log in', '/login')); - ret.set('logout', new NavigationItem('O', 'Logout', '/logout')); - ret.set('help', new NavigationItem('E', 'Help', '/help')); - ret.set( - 'settings', - new NavigationItem(null, '', '/settings')); - return ret; -} - -class NavigationItem { - constructor(accessKey, name, url) { - this.accessKey = accessKey; - this.name = name; - this.url = url; - this.available = true; - this.imageUrl = null; - } -} - -class TopNavController { - constructor() { - this._topNavView = new TopNavView(); - this._activeItem = null; - this._items = _createNavigationItemMap(); - - const rerender = () => { - this._updateVisibility(); - this._topNavView.render({ - items: this._items, - activeItem: this._activeItem}); - this._topNavView.activate(this._activeItem); - }; - - events.listen( - events.Authentication, - () => { rerender(); return true; }); - rerender(); - } - - _updateVisibility() { - this._items.get('account').url = '/user/' + api.userName; - this._items.get('account').imageUrl = api.user ? - api.user.avatarUrl : null; - - for (let [key, item] of this._items) { - item.available = true; - } - if (!api.hasPrivilege('posts:list')) { - this._items.get('posts').available = false; - } - if (!api.hasPrivilege('posts:create')) { - this._items.get('upload').available = false; - } - if (!api.hasPrivilege('comments:list')) { - this._items.get('comments').available = false; - } - if (!api.hasPrivilege('tags:list')) { - this._items.get('tags').available = false; - } - if (!api.hasPrivilege('users:list')) { - this._items.get('users').available = false; - } - if (api.isLoggedIn()) { - this._items.get('register').available = false; - this._items.get('login').available = false; - } else { - this._items.get('account').available = false; - this._items.get('logout').available = false; - } - } - - activate(itemName) { - this._activeItem = itemName; - this._topNavView.activate(this._activeItem); - } -} - -module.exports = new TopNavController(); diff --git a/client/js/controllers/top_navigation_controller.js b/client/js/controllers/top_navigation_controller.js new file mode 100644 index 0000000..6b0d47f --- /dev/null +++ b/client/js/controllers/top_navigation_controller.js @@ -0,0 +1,70 @@ +'use strict'; + +const api = require('../api.js'); +const events = require('../events.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( + 'activate', e => this._evtActivate(e)); + + events.listen( + events.Authentication, + () => { + this._render(); + return true; + }); + + this._render(); + } + + _evtActivate(e) { + this._topNavigationView.activate(e.key); + } + + _updateNavigationFromPrivileges() { + TopNavigation.get('account').url = '/user/' + api.userName; + TopNavigation.get('account').imageUrl = + api.user ? api.user.avatarUrl : null; + + TopNavigation.showAll(); + if (!api.hasPrivilege('posts:list')) { + TopNavigation.hide('posts'); + } + if (!api.hasPrivilege('posts:create')) { + TopNavigation.hide('upload'); + } + if (!api.hasPrivilege('comments:list')) { + TopNavigation.hide('comments'); + } + if (!api.hasPrivilege('tags:list')) { + TopNavigation.hide('tags'); + } + if (!api.hasPrivilege('users:list')) { + TopNavigation.hide('users'); + } + if (api.isLoggedIn()) { + TopNavigation.hide('register'); + TopNavigation.hide('login'); + } else { + TopNavigation.hide('account'); + TopNavigation.hide('logout'); + } + } + + _render() { + this._updateNavigationFromPrivileges(); + console.log(TopNavigation.getAll()); + this._topNavigationView.render({ + items: TopNavigation.getAll(), + }); + this._topNavigationView.activate( + TopNavigation.activeItem ? TopNavigation.activeItem.key : ''); + }; +} + +module.exports = new TopNavigationController(); diff --git a/client/js/controllers/users_controller.js b/client/js/controllers/users_controller.js index ecfc41f..32b311d 100644 --- a/client/js/controllers/users_controller.js +++ b/client/js/controllers/users_controller.js @@ -6,8 +6,8 @@ const config = require('../config.js'); const events = require('../events.js'); const misc = require('../util/misc.js'); const views = require('../util/views.js'); -const topNavController = require('../controllers/top_nav_controller.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'); @@ -65,7 +65,7 @@ class UsersController { } _listUsersRoute(ctx, next) { - topNavController.activate('users'); + TopNavigation.activate('users'); pageController.run({ searchQuery: ctx.searchQuery, @@ -87,7 +87,7 @@ class UsersController { } _createUserRoute(ctx, next) { - topNavController.activate('register'); + TopNavigation.activate('register'); this._registrationView.render({ register: (...args) => { return this._register(...args); @@ -237,9 +237,9 @@ class UsersController { } if (isLoggedIn) { - topNavController.activate('account'); + TopNavigation.activate('account'); } else { - topNavController.activate('users'); + TopNavigation.activate('users'); } this._userView.render({ user: user, diff --git a/client/js/models/top_navigation.js b/client/js/models/top_navigation.js new file mode 100644 index 0000000..b8caeba --- /dev/null +++ b/client/js/models/top_navigation.js @@ -0,0 +1,93 @@ +'use strict'; + +const events = require('../events.js'); + +class TopNavigationItem { + constructor(accessKey, title, url, available, imageUrl) { + this.accessKey = accessKey; + this.title = title; + this.url = url; + this.available = available === undefined ? true : available; + this.imageUrl = imageUrl === undefined ? null : imageUrl; + this.key = null; + } +}; + +class TopNavigation extends events.EventTarget { + constructor() { + super(); + this.activeItem = null; + this._keyToItem = new Map(); + this._items = []; + } + + getAll() { + return this._items; + } + + get(key) { + if (!this._keyToItem.has(key)) { + throw `An item with key ${key} does not exist.`; + } + return this._keyToItem.get(key); + } + + add(key, item) { + item.key = key; + if (this._keyToItem.has(key)) { + throw `An item with key ${key} was already added.`; + } + this._keyToItem.set(key, item); + this._items.push(item); + } + + 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); + } + + showAll() { + for (let item of this._items) { + item.available = true; + } + } + + show(key) { + this.get(key).available = true; + } + + hide(key) { + this.get(key).available = false; + } +}; + +function _makeTopNavigation() { + const ret = new TopNavigation(); + ret.add('home', new TopNavigationItem('H', 'Home', '/')); + ret.add('posts', new TopNavigationItem('P', 'Posts', '/posts')); + ret.add('upload', new TopNavigationItem('U', 'Upload', '/upload')); + ret.add('comments', new TopNavigationItem('C', 'Comments', '/comments')); + ret.add('tags', new TopNavigationItem('T', 'Tags', '/tags')); + ret.add('users', new TopNavigationItem('S', 'Users', '/users')); + ret.add('account', new TopNavigationItem('A', 'Account', '/user/{me}')); + ret.add('register', new TopNavigationItem('R', 'Register', '/register')); + ret.add('login', new TopNavigationItem('L', 'Log in', '/login')); + ret.add('logout', new TopNavigationItem('O', 'Logout', '/logout')); + ret.add('help', new TopNavigationItem('E', 'Help', '/help')); + ret.add( + 'settings', + new TopNavigationItem( + null, + '', + '/settings')); + return ret; +} + +module.exports = _makeTopNavigation(); diff --git a/client/js/views/post_view.js b/client/js/views/post_view.js index ce57c61..9ef86db 100644 --- a/client/js/views/post_view.js +++ b/client/js/views/post_view.js @@ -29,12 +29,13 @@ class PostView { views.listenToMessages(source); views.showView(target, source); - const topNavNode = document.body.querySelector('#top-nav'); const postViewNode = document.body.querySelector('.content-wrapper'); + const topNavigationNode = + document.body.querySelector('#top-navigation'); const margin = ( postViewNode.getBoundingClientRect().top - - topNavNode.getBoundingClientRect().height); + topNavigationNode.getBoundingClientRect().height); this._postContentControl = new PostContentControl( postContainerNode, @@ -45,7 +46,7 @@ class PostView { postContainerNode.getBoundingClientRect().left - margin, window.innerHeight - - topNavNode.getBoundingClientRect().height - + topNavigationNode.getBoundingClientRect().height - margin * 2, ]; }); diff --git a/client/js/views/top_nav_view.js b/client/js/views/top_nav_view.js deleted file mode 100644 index 78defb1..0000000 --- a/client/js/views/top_nav_view.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -const views = require('../util/views.js'); - -class TopNavView { - constructor() { - this._template = views.getTemplate('top-nav'); - this._navHolder = document.getElementById('top-nav-holder'); - this._lastCtx = null; - } - - render(ctx) { - this._lastCtx = ctx; - const target = this._navHolder; - const source = this._template(ctx); - views.showView(this._navHolder, source); - } - - activate(itemName) { - const allItemsSelector = '#top-nav-holder [data-name]'; - const currentItemSelector = - '#top-nav-holder [data-name="' + itemName + '"]'; - for (let item of document.querySelectorAll(allItemsSelector)) { - item.className = ''; - } - const currentItem = document.querySelectorAll(currentItemSelector); - if (currentItem.length > 0) { - currentItem[0].className = 'active'; - } - } -} - -module.exports = TopNavView; diff --git a/client/js/views/top_navigation_view.js b/client/js/views/top_navigation_view.js new file mode 100644 index 0000000..0ef66a8 --- /dev/null +++ b/client/js/views/top_navigation_view.js @@ -0,0 +1,29 @@ +'use strict'; + +const views = require('../util/views.js'); + +class TopNavigationView { + constructor() { + this._template = views.getTemplate('top-navigation'); + this._navHolder = document.getElementById('top-navigation-holder'); + this._lastCtx = null; + } + + render(ctx) { + this._lastCtx = ctx; + const target = this._navHolder; + const source = this._template(ctx); + views.showView(this._navHolder, source); + } + + activate(key) { + const allItemNodes = document.querySelectorAll( + '#top-navigation-holder [data-name]'); + for (let itemNode of allItemNodes) { + itemNode.classList.toggle( + 'active', itemNode.getAttribute('data-name') === key); + } + } +} + +module.exports = TopNavigationView;