diff --git a/client/css/users.css b/client/css/users.css
index 81bfe2e..6ba3d5f 100644
--- a/client/css/users.css
+++ b/client/css/users.css
@@ -69,3 +69,6 @@
#user-delete form {
width: 100%;
}
+#user-delete form label {
+ padding: 0;
+}
diff --git a/client/js/controllers/users_controller.js b/client/js/controllers/users_controller.js
index 005627e..d242c1f 100644
--- a/client/js/controllers/users_controller.js
+++ b/client/js/controllers/users_controller.js
@@ -5,6 +5,7 @@ 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 topNavController = require('../controllers/top_nav_controller.js');
const RegistrationView = require('../views/registration_view.js');
const UserView = require('../views/user_view.js');
@@ -58,7 +59,7 @@ class UsersController {
this.user = response.user;
next();
}).catch(response => {
- this.userView.emptyView(this.userView.contentHolder);
+ views.emptyView(document.getElementById('content-holder'));
events.notify(events.Error, response.description);
});
}
diff --git a/client/js/events.js b/client/js/events.js
index 8fb42d4..5c5bdb0 100644
--- a/client/js/events.js
+++ b/client/js/events.js
@@ -2,6 +2,10 @@
let listeners = [];
+function unlisten(messageClass) {
+ listeners[messageClass] = [];
+}
+
function listen(messageClass, handler) {
if (!(messageClass in listeners)) {
listeners[messageClass] = [];
@@ -25,4 +29,5 @@ module.exports = {
notify: notify,
listen: listen,
+ unlisten: unlisten,
};
diff --git a/client/js/util/views.js b/client/js/util/views.js
new file mode 100644
index 0000000..98350f8
--- /dev/null
+++ b/client/js/util/views.js
@@ -0,0 +1,125 @@
+'use strict';
+
+require('../util/polyfill.js');
+const handlebars = require('handlebars');
+const events = require('../events.js');
+const domParser = new DOMParser();
+
+function _messageHandler(target, message, className) {
+ if (!message) {
+ message = 'Unknown message';
+ }
+ const messagesHolder = target.querySelector('.messages');
+ if (!messagesHolder) {
+ alert(message);
+ return;
+ }
+ /* TODO: animate this */
+ const node = document.createElement('div');
+ node.innerHTML = message.replace(/\n/g, '
');
+ node.classList.add('message');
+ node.classList.add(className);
+ messagesHolder.appendChild(node);
+}
+
+
+function listenToMessages(target) {
+ events.unlisten(events.Success);
+ events.unlisten(events.Error);
+ events.listen(
+ events.Success, msg => { _messageHandler(target, msg, 'success'); });
+ events.listen(
+ events.Error, msg => { _messageHandler(target, msg, 'error'); });
+}
+
+function clearMessages(target) {
+ const messagesHolder = target.querySelector('.messages');
+ /* TODO: animate that */
+ while (messagesHolder.lastChild) {
+ messagesHolder.removeChild(messagesHolder.lastChild);
+ }
+}
+
+function htmlToDom(html) {
+ const parsed = domParser.parseFromString(html, 'text/html').body;
+ return parsed.childNodes.length > 1 ?
+ parsed.childNodes :
+ parsed.firstChild;
+}
+
+function getTemplate(templatePath) {
+ const templateElement = document.getElementById(templatePath + '-template');
+ if (!templateElement) {
+ console.error('Missing template: ' + templatePath);
+ return null;
+ }
+ const templateText = templateElement.innerHTML.trim();
+ const templateFactory = handlebars.compile(templateText);
+ return (...args) => {
+ return htmlToDom(templateFactory(...args));
+ };
+}
+
+function decorateValidator(form) {
+ // postpone showing form fields validity until user actually tries
+ // to submit it (seeing red/green form w/o doing anything breaks POLA)
+ const submitButton = form.querySelector('.buttons input');
+ submitButton.addEventListener('click', e => {
+ form.classList.add('show-validation');
+ });
+ form.addEventListener('submit', e => {
+ form.classList.remove('show-validation');
+ });
+}
+
+function disableForm(form) {
+ for (let input of form.querySelectorAll('input')) {
+ input.disabled = true;
+ }
+}
+
+function enableForm(form) {
+ for (let input of form.querySelectorAll('input')) {
+ input.disabled = false;
+ }
+}
+
+function emptyView(target) {
+ const ret = showView(target, htmlToDom('
'));
+ listenToMessages(target);
+ return ret;
+}
+
+function showView(target, source) {
+ return new Promise((resolve, reject) => {
+ let observer = new MutationObserver(mutations => {
+ resolve();
+ observer.disconnect();
+ });
+ observer.observe(target, {childList: true});
+ while (target.lastChild) {
+ target.removeChild(target.lastChild);
+ }
+ if (source instanceof NodeList) {
+ for (let child of source) {
+ target.appendChild(child);
+ }
+ } else if (source instanceof Node) {
+ target.appendChild(source);
+ } else {
+ console.error('Invalid view source', source);
+ }
+ });
+}
+
+module.exports = {
+ htmlToDom: htmlToDom,
+ getTemplate: getTemplate,
+ showView: showView,
+ emptyView: emptyView,
+ enableForm: enableForm,
+ disableForm: disableForm,
+ listenToMessages: listenToMessages,
+ clearMessages: clearMessages,
+ decorateValidator: decorateValidator,
+};
diff --git a/client/js/views/base_view.js b/client/js/views/base_view.js
deleted file mode 100644
index 98ae406..0000000
--- a/client/js/views/base_view.js
+++ /dev/null
@@ -1,115 +0,0 @@
-'use strict';
-
-const handlebars = require('handlebars');
-const events = require('../events.js');
-const contentHolder = document.getElementById('content-holder');
-require('../util/polyfill.js');
-
-function messageHandler(message, className) {
- if (!message) {
- message = 'Unknown message';
- }
- const messagesHolder = contentHolder.querySelector('.messages');
- if (!messagesHolder) {
- alert(message);
- return;
- }
- /* TODO: animate this */
- const node = document.createElement('div');
- node.innerHTML = message.replace(/\n/g, '
');
- node.classList.add('message');
- node.classList.add(className);
- messagesHolder.appendChild(node);
-}
-
-events.listen(events.Success, msg => { messageHandler(msg, 'success'); });
-events.listen(events.Error, msg => { messageHandler(msg, 'error'); });
-
-class BaseView {
- constructor() {
- this.contentHolder = contentHolder;
- this.domParser = new DOMParser();
- }
-
- htmlToDom(html) {
- const parsed = this.domParser.parseFromString(html, 'text/html').body;
- return parsed.childNodes.length > 1 ?
- parsed.childNodes :
- parsed.firstChild;
- }
-
- getTemplate(templatePath) {
- const templateElement = document.getElementById(templatePath);
- if (!templateElement) {
- console.error('Missing template: ' + templatePath);
- return null;
- }
- const templateText = templateElement.innerHTML.trim();
- const templateFactory = handlebars.compile(templateText);
- return (...args) => {
- return this.htmlToDom(templateFactory(...args));
- };
- }
-
- clearMessages() {
- const messagesHolder = this.contentHolder.querySelector('.messages');
- /* TODO: animate that */
- while (messagesHolder.lastChild) {
- messagesHolder.removeChild(messagesHolder.lastChild);
- }
- }
-
- decorateValidator(form) {
- // postpone showing form fields validity until user actually tries
- // to submit it (seeing red/green form w/o doing anything breaks POLA)
- const submitButton = form.querySelector('.buttons input');
- submitButton.addEventListener('click', e => {
- form.classList.add('show-validation');
- });
- form.addEventListener('submit', e => {
- form.classList.remove('show-validation');
- });
- }
-
- disableForm(form) {
- for (let input of form.querySelectorAll('input')) {
- input.disabled = true;
- }
- }
-
- enableForm(form) {
- for (let input of form.querySelectorAll('input')) {
- input.disabled = false;
- }
- }
-
- emptyView(target) {
- return this.showView(
- target,
- this.htmlToDom(''));
- }
-
- showView(target, source) {
- return new Promise((resolve, reject) => {
- let observer = new MutationObserver(mutations => {
- resolve();
- observer.disconnect();
- });
- observer.observe(target, {childList: true});
- while (target.lastChild) {
- target.removeChild(target.lastChild);
- }
- if (source instanceof NodeList) {
- for (let child of source) {
- target.appendChild(child);
- }
- } else if (source instanceof Node) {
- target.appendChild(source);
- } else {
- console.error('Invalid view source', source);
- }
- });
- }
-}
-
-module.exports = BaseView;
diff --git a/client/js/views/help_view.js b/client/js/views/help_view.js
index d3f198c..833e548 100644
--- a/client/js/views/help_view.js
+++ b/client/js/views/help_view.js
@@ -1,31 +1,30 @@
'use strict';
const config = require('../config.js');
-const BaseView = require('./base_view.js');
+const views = require('../util/views.js');
-class HelpView extends BaseView {
+class HelpView {
constructor() {
- super();
- this.template = this.getTemplate('help-template');
+ this.template = views.getTemplate('help');
this.sectionTemplates = {};
const sectionKeys = ['about', 'keyboard', 'search', 'comments', 'tos'];
for (let section of sectionKeys) {
- const templateName = 'help-' + section + '-template';
- this.sectionTemplates[section] = this.getTemplate(templateName);
+ const templateName = 'help-' + section;
+ this.sectionTemplates[section] = views.getTemplate(templateName);
}
}
render(ctx) {
- const target = this.contentHolder;
+ const target = document.getElementById('content-holder');
const source = this.template();
ctx.section = ctx.section || 'about';
if (!(ctx.section in this.sectionTemplates)) {
- this.emptyView(this.contentHolder);
+ views.emptyView(target);
return;
}
- this.showView(
+ views.showView(
source.querySelector('.content'),
this.sectionTemplates[ctx.section]({
name: config.name,
@@ -40,7 +39,8 @@ class HelpView extends BaseView {
}
}
- this.showView(target, source);
+ views.listenToMessages(target);
+ views.showView(target, source);
}
}
diff --git a/client/js/views/home_view.js b/client/js/views/home_view.js
index 341982a..b27fb14 100644
--- a/client/js/views/home_view.js
+++ b/client/js/views/home_view.js
@@ -1,22 +1,23 @@
'use strict';
const config = require('../config.js');
-const BaseView = require('./base_view.js');
+const views = require('../util/views.js');
-class HomeView extends BaseView {
+class HomeView {
constructor() {
- super();
- this.template = this.getTemplate('home-template');
+ this.template = views.getTemplate('home');
}
render(ctx) {
- const target = this.contentHolder;
+ const target = document.getElementById('content-holder');
const source = this.template({
name: config.name,
version: config.meta.version,
buildDate: config.meta.buildDate,
});
- this.showView(target, source);
+
+ views.listenToMessages(target);
+ views.showView(target, source);
}
}
diff --git a/client/js/views/login_view.js b/client/js/views/login_view.js
index f0e90c0..9a1763c 100644
--- a/client/js/views/login_view.js
+++ b/client/js/views/login_view.js
@@ -1,17 +1,15 @@
'use strict';
const config = require('../config.js');
-const events = require('../events.js');
-const BaseView = require('./base_view.js');
+const views = require('../util/views.js');
-class LoginView extends BaseView {
+class LoginView {
constructor() {
- super();
- this.template = this.getTemplate('login-template');
+ this.template = views.getTemplate('login');
}
render(ctx) {
- const target = this.contentHolder;
+ const target = document.getElementById('content-holder');
const source = this.template({canSendMails: config.canSendMails});
const form = source.querySelector('form');
@@ -19,22 +17,23 @@ class LoginView extends BaseView {
const passwordField = source.querySelector('#user-password');
const rememberUserField = source.querySelector('#remember-user');
- this.decorateValidator(form);
+ views.decorateValidator(form);
userNameField.setAttribute('pattern', config.userNameRegex);
passwordField.setAttribute('pattern', config.passwordRegex);
form.addEventListener('submit', e => {
e.preventDefault();
- this.clearMessages();
- this.disableForm(form);
+ views.clearMessages(target);
+ views.disableForm(form);
ctx.login(
userNameField.value,
passwordField.value,
rememberUserField.checked)
- .always(() => { this.enableForm(form); });
+ .always(() => { views.enableForm(form); });
});
- this.showView(target, source);
+ views.listenToMessages(target);
+ views.showView(target, source);
}
}
diff --git a/client/js/views/password_reset_view.js b/client/js/views/password_reset_view.js
index 12729bc..14f0737 100644
--- a/client/js/views/password_reset_view.js
+++ b/client/js/views/password_reset_view.js
@@ -1,32 +1,31 @@
'use strict';
-const events = require('../events.js');
-const BaseView = require('./base_view.js');
+const views = require('../util/views.js');
-class PasswordResetView extends BaseView {
+class PasswordResetView {
constructor() {
- super();
- this.template = this.getTemplate('password-reset-template');
+ this.template = views.getTemplate('password-reset');
}
render(ctx) {
- const target = this.contentHolder;
+ const target = document.getElementById('content-holder');
const source = this.template();
const form = source.querySelector('form');
const userNameOrEmailField = source.querySelector('#user-name');
- this.decorateValidator(form);
+ views.decorateValidator(form);
form.addEventListener('submit', e => {
e.preventDefault();
- this.clearMessages();
- this.disableForm(form);
+ views.clearMessages(target);
+ views.disableForm(form);
ctx.proceed(userNameOrEmailField.value)
- .catch(() => { this.enableForm(form); });
+ .catch(() => { views.enableForm(form); });
});
- this.showView(target, source);
+ views.listenToMessages(target);
+ views.showView(target, source);
}
}
diff --git a/client/js/views/registration_view.js b/client/js/views/registration_view.js
index 7c58ab5..55ad0a1 100644
--- a/client/js/views/registration_view.js
+++ b/client/js/views/registration_view.js
@@ -1,17 +1,15 @@
'use strict';
const config = require('../config.js');
-const events = require('../events.js');
-const BaseView = require('./base_view.js');
+const views = require('../util/views.js');
-class RegistrationView extends BaseView {
+class RegistrationView {
constructor() {
- super();
- this.template = this.getTemplate('user-registration-template');
+ this.template = views.getTemplate('user-registration');
}
render(ctx) {
- const target = this.contentHolder;
+ const target = document.getElementById('content-holder');
const source = this.template();
const form = source.querySelector('form');
@@ -19,22 +17,23 @@ class RegistrationView extends BaseView {
const passwordField = source.querySelector('#user-password');
const emailField = source.querySelector('#user-email');
- this.decorateValidator(form);
+ views.decorateValidator(form);
userNameField.setAttribute('pattern', config.userNameRegex);
passwordField.setAttribute('pattern', config.passwordRegex);
form.addEventListener('submit', e => {
e.preventDefault();
- this.clearMessages();
- this.disableForm(form);
+ views.clearMessages(target);
+ views.disableForm(form);
ctx.register(
userNameField.value,
passwordField.value,
emailField.value)
- .always(() => { this.enableForm(form); });
+ .always(() => { views.enableForm(form); });
});
- this.showView(target, source);
+ views.listenToMessages(target);
+ views.showView(target, source);
}
}
diff --git a/client/js/views/top_nav_view.js b/client/js/views/top_nav_view.js
index 7e9c271..3210564 100644
--- a/client/js/views/top_nav_view.js
+++ b/client/js/views/top_nav_view.js
@@ -1,11 +1,10 @@
'use strict';
-const BaseView = require('./base_view.js');
+const views = require('../util/views.js');
-class TopNavView extends BaseView {
+class TopNavView {
constructor() {
- super();
- this.template = this.getTemplate('top-nav-template');
+ this.template = views.getTemplate('top-nav');
this.navHolder = document.getElementById('top-nav-holder');
}
@@ -21,7 +20,7 @@ class TopNavView extends BaseView {
'$1');
}
- this.showView(this.navHolder, source);
+ views.showView(this.navHolder, source);
}
activate(itemName) {
diff --git a/client/js/views/user_deletion_view.js b/client/js/views/user_deletion_view.js
index 3861685..20c950f 100644
--- a/client/js/views/user_deletion_view.js
+++ b/client/js/views/user_deletion_view.js
@@ -1,11 +1,10 @@
'use strict';
-const BaseView = require('./base_view.js');
+const views = require('../util/views.js');
-class UserDeletionView extends BaseView {
+class UserDeletionView {
constructor() {
- super();
- this.template = this.getTemplate('user-deletion-template');
+ this.template = views.getTemplate('user-deletion');
}
render(ctx) {
@@ -14,16 +13,17 @@ class UserDeletionView extends BaseView {
const form = source.querySelector('form');
- this.decorateValidator(form);
+ views.decorateValidator(form);
form.addEventListener('submit', e => {
e.preventDefault();
- this.clearMessages();
- this.disableForm(form);
+ views.clearMessages(target);
+ views.disableForm(form);
ctx.delete();
});
- this.showView(target, source);
+ views.listenToMessages(target);
+ views.showView(target, source);
}
}
diff --git a/client/js/views/user_edit_view.js b/client/js/views/user_edit_view.js
index 9d5c3ae..21ac7ba 100644
--- a/client/js/views/user_edit_view.js
+++ b/client/js/views/user_edit_view.js
@@ -1,12 +1,11 @@
'use strict';
const config = require('../config.js');
-const BaseView = require('./base_view.js');
+const views = require('../util/views.js');
-class UserEditView extends BaseView {
+class UserEditView {
constructor() {
- super();
- this.template = this.getTemplate('user-edit-template');
+ this.template = views.getTemplate('user-edit');
}
render(ctx) {
@@ -19,7 +18,7 @@ class UserEditView extends BaseView {
const userNameField = source.querySelector('#user-name');
const passwordField = source.querySelector('#user-password');
- this.decorateValidator(form);
+ views.decorateValidator(form);
if (userNameField) {
userNameField.setAttribute(
@@ -41,17 +40,18 @@ class UserEditView extends BaseView {
form.addEventListener('submit', e => {
e.preventDefault();
- this.clearMessages();
- this.disableForm(form);
+ views.clearMessages(target);
+ views.disableForm(form);
ctx.edit(
userNameField.value,
passwordField.value,
emailField.value,
rankField.value)
- .always(() => { this.enableForm(form); });
+ .always(() => { views.enableForm(form); });
});
- this.showView(target, source);
+ views.listenToMessages(target);
+ views.showView(target, source);
}
}
diff --git a/client/js/views/user_summary_view.js b/client/js/views/user_summary_view.js
index ef3029a..9369376 100644
--- a/client/js/views/user_summary_view.js
+++ b/client/js/views/user_summary_view.js
@@ -1,17 +1,17 @@
'use strict';
-const BaseView = require('./base_view.js');
+const views = require('../util/views.js');
-class UserSummaryView extends BaseView {
+class UserSummaryView {
constructor() {
- super();
- this.template = this.getTemplate('user-summary-template');
+ this.template = views.getTemplate('user-summary');
}
render(ctx) {
const target = ctx.target;
const source = this.template(ctx);
- this.showView(target, source);
+ views.listenToMessages(target);
+ views.showView(target, source);
}
}
diff --git a/client/js/views/user_view.js b/client/js/views/user_view.js
index cb38fc1..c547549 100644
--- a/client/js/views/user_view.js
+++ b/client/js/views/user_view.js
@@ -1,21 +1,20 @@
'use strict';
-const BaseView = require('./base_view.js');
+const views = require('../util/views.js');
const UserDeletionView = require('./user_deletion_view.js');
const UserSummaryView = require('./user_summary_view.js');
const UserEditView = require('./user_edit_view.js');
-class UserView extends BaseView {
+class UserView {
constructor() {
- super();
- this.template = this.getTemplate('user-template');
+ this.template = views.getTemplate('user');
this.deletionView = new UserDeletionView();
this.summaryView = new UserSummaryView();
this.editView = new UserEditView();
}
render(ctx) {
- const target = this.contentHolder;
+ const target = document.getElementById('content-holder');
const source = this.template(ctx);
ctx.section = ctx.section || 'summary';
@@ -39,7 +38,8 @@ class UserView extends BaseView {
ctx.target = source.querySelector('#user-content-holder');
view.render(ctx);
- this.showView(target, source);
+ views.listenToMessages(target);
+ views.showView(target, source);
}
}