client/top-nav: trying out actual mvc

This commit is contained in:
rr- 2016-06-13 22:34:39 +02:00
parent e93af8b577
commit 0f1e234a5d
22 changed files with 240 additions and 174 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -9,7 +9,7 @@
<link rel='shortcut icon' type='image/png' href='/img/favicon.png'/>
</head>
<body>
<div id='top-nav-holder'></div>
<div id='top-navigation-holder'></div>
<div id='content-holder'></div>
<script type='text/javascript' src='/js/vendor.min.js'></script>
<script type='text/javascript' src='/js/app.min.js'></script>

View file

@ -1,14 +1,14 @@
<nav id='top-nav' class='buttons'>
<ul><!--
--><% for (let [key, item] of ctx.items) { %><!--
<nav id='top-navigation' class='buttons'><!--
--><ul><!--
--><% for (let item of ctx.items) { %><!--
--><% if (item.available) { %><!--
--><li data-name='<%= key %>'><!--
--><li data-name='<%= item.key %>'><!--
--><a href='<%= item.url %>' accesskey='<%= item.accessKey %>'><!--
--><% if (item.imageUrl) { print(ctx.makeThumbnail(item.imageUrl)); } %><!--
--><span class='text'><%= ctx.makeAccessKey(item.name, item.accessKey) %></span><!--
--><span class='text'><%= ctx.makeAccessKey(item.title, item.accessKey) %></span><!--
--></a><!--
--></li><!--
--><% } %><!--
--><% } %><!--
--></ul>
</nav>
--></ul><!--
--></nav>

View file

@ -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);

View file

@ -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,

View file

@ -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,

View file

@ -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('');
}
}

View file

@ -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});
}
}

View file

@ -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=` +

View file

@ -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),

View file

@ -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,

View file

@ -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, '<i class=\'fa fa-cog\'></i>', '/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();

View file

@ -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();

View file

@ -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,

View file

@ -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,
'<i class=\'fa fa-cog\'></i>',
'/settings'));
return ret;
}
module.exports = _makeTopNavigation();

View file

@ -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,
];
});

View file

@ -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;

View file

@ -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;