From 2a4241641c80fec036143e7335eb2dad63fad476 Mon Sep 17 00:00:00 2001 From: rr- Date: Wed, 11 May 2016 21:29:57 +0200 Subject: [PATCH] client/events: improve event dispatching This commit introduces timer-less retry system: 1. Any change to URL is going to stop listening to any messages. 2. If a message is sent and there's no handler that could pick it up, the message gets enqueued. 3. The message is sent again to the first handler that attaches itself to given event type. While in theory this is full of holes (no control over the first handler), in practice, it works quite well. Additionally, views.listenToMessages was attaching to completely wrong DOM node; this commit fixes this as well. --- client/html/endless_pager.hbs | 2 +- client/js/controllers/page_controller.js | 1 + client/js/controllers/tags_controller.js | 2 +- client/js/controllers/top_nav_controller.js | 4 +- client/js/controllers/users_controller.js | 36 ++++--- client/js/events.js | 40 +++++--- client/js/main.js | 6 ++ client/js/tags.js | 4 +- client/js/util/views.js | 105 +++++++++++--------- client/js/views/empty_view.js | 2 +- client/js/views/endless_page_view.js | 2 +- client/js/views/help_view.js | 2 +- client/js/views/home_view.js | 2 +- client/js/views/login_view.js | 2 +- client/js/views/manual_page_view.js | 4 +- client/js/views/password_reset_view.js | 2 +- client/js/views/registration_view.js | 2 +- client/js/views/settings_view.js | 2 +- client/js/views/tag_categories_view.js | 2 +- client/js/views/user_delete_view.js | 5 +- client/js/views/user_edit_view.js | 2 +- client/js/views/user_summary_view.js | 2 +- client/js/views/user_view.js | 2 +- 23 files changed, 133 insertions(+), 100 deletions(-) diff --git a/client/html/endless_pager.hbs b/client/html/endless_pager.hbs index a47cd96..6870f9a 100644 --- a/client/html/endless_pager.hbs +++ b/client/html/endless_pager.hbs @@ -1,5 +1,5 @@
-
+
diff --git a/client/js/controllers/page_controller.js b/client/js/controllers/page_controller.js index 8c75c83..7b589a3 100644 --- a/client/js/controllers/page_controller.js +++ b/client/js/controllers/page_controller.js @@ -9,6 +9,7 @@ class PageController { constructor() { events.listen(events.SettingsChange, () => { this.update(); + return true; }); this.update(); } diff --git a/client/js/controllers/tags_controller.js b/client/js/controllers/tags_controller.js index 931a32c..b49bffd 100644 --- a/client/js/controllers/tags_controller.js +++ b/client/js/controllers/tags_controller.js @@ -40,7 +40,7 @@ class TagsController { Promise.all(promises).then( () => { events.notify(events.TagsChange); - events.notify(events.Success, 'Changes saved successfully'); + events.notify(events.Success, 'Changes saved.'); }, response => { events.notify(events.Error, response.description); diff --git a/client/js/controllers/top_nav_controller.js b/client/js/controllers/top_nav_controller.js index 51d3002..cab1a4d 100644 --- a/client/js/controllers/top_nav_controller.js +++ b/client/js/controllers/top_nav_controller.js @@ -43,7 +43,9 @@ class TopNavController { this.topNavView.activate(this.activeItem); }; - events.listen(events.Authentication, rerender); + events.listen( + events.Authentication, + () => { rerender(); return true; }); rerender(); } diff --git a/client/js/controllers/users_controller.js b/client/js/controllers/users_controller.js index c824eb3..529f490 100644 --- a/client/js/controllers/users_controller.js +++ b/client/js/controllers/users_controller.js @@ -195,25 +195,23 @@ class UsersController { _delete(user) { const isLoggedIn = api.isLoggedIn(user); - return new Promise((resolve, reject) => { - api.delete('/user/' + user.name) - .then(response => { - if (isLoggedIn) { - api.forget(); - api.logout(); - } - resolve(); - if (api.hasPrivilege('users:list')) { - page('/users'); - } else { - page('/'); - } - events.notify(events.Success, 'Account deleted'); - }, response => { - reject(); - events.notify(events.Error, response.description); - }); - }); + return api.delete('/user/' + user.name) + .then(response => { + if (isLoggedIn) { + api.forget(); + api.logout(); + } + if (api.hasPrivilege('users:list')) { + page('/users'); + } else { + page('/'); + } + events.notify(events.Success, 'Account deleted.'); + return Promise.resolve(); + }, response => { + events.notify(events.Error, response.description); + return Promise.reject(); + }); } _show(user, section) { diff --git a/client/js/events.js b/client/js/events.js index 0493c3b..0dd010a 100644 --- a/client/js/events.js +++ b/client/js/events.js @@ -1,34 +1,48 @@ 'use strict'; -let listeners = []; +let pendingMessages = new Map(); +let listeners = new Map(); function unlisten(messageClass) { - listeners[messageClass] = []; + listeners.set(messageClass, []); } function listen(messageClass, handler) { - if (!(messageClass in listeners)) { - listeners[messageClass] = []; + if (pendingMessages.has(messageClass)) { + let newPendingMessages = []; + for (let message of pendingMessages.get(messageClass)) { + if (!handler(message)) { + newPendingMessages.push(message); + } + } + pendingMessages.set(messageClass, newPendingMessages); } - listeners[messageClass].push(handler); + if (!listeners.has(messageClass)) { + listeners.set(messageClass, []); + } + listeners.get(messageClass).push(handler); } function notify(messageClass, message) { - if (!(messageClass in listeners)) { + 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[messageClass]) { + for (let handler of listeners.get(messageClass)) { handler(message); } } module.exports = { - Success: 1, - Error: 2, - Info: 3, - Authentication: 4, - SettingsChange: 5, - TagsChange: 6, + Success: 'success', + Error: 'error', + Info: 'info', + Authentication: 'auth', + SettingsChange: 'settings-change', + TagsChange: 'tags-change', notify: notify, listen: listen, diff --git a/client/js/main.js b/client/js/main.js index 6fb92c3..1636e96 100644 --- a/client/js/main.js +++ b/client/js/main.js @@ -29,10 +29,16 @@ controllers.push(require('./controllers/home_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(); } +page.exit((ctx, next) => { + views.unlistenToMessages(); + next(); +}); + const api = require('./api.js'); Promise.all([tags.refreshExport(), api.loginFromCookies()]) .then(() => { diff --git a/client/js/tags.js b/client/js/tags.js index 5b1c3b0..3098104 100644 --- a/client/js/tags.js +++ b/client/js/tags.js @@ -60,7 +60,9 @@ function getExport() { return _export || {}; } -events.listen(events.TagsChange, refreshExport); +events.listen( + events.TagsChange, + () => { refreshExport(); return true; }); module.exports = { getExport: getExport, diff --git a/client/js/util/views.js b/client/js/util/views.js index 8dd4115..5f61a49 100644 --- a/client/js/util/views.js +++ b/client/js/util/views.js @@ -37,31 +37,31 @@ function makeThumbnail(url) { function makeRadio(options) { return makeVoidElement( - 'input', - { - id: options.id, - name: options.name, - value: options.value, - type: 'radio', - checked: options.selectedValue === options.value, - required: options.required, - }) + - _makeLabel(options, {class: 'radio'}); + 'input', + { + id: options.id, + name: options.name, + value: options.value, + type: 'radio', + checked: options.selectedValue === options.value, + required: options.required, + }) + + _makeLabel(options, {class: 'radio'}); } function makeCheckbox(options) { return makeVoidElement( - 'input', - { - id: options.id, - name: options.name, - value: options.value, - type: 'checkbox', - checked: options.checked !== undefined ? - options.checked : false, - required: options.required, - }) + - _makeLabel(options, {class: 'checkbox'}); + 'input', + { + id: options.id, + name: options.name, + value: options.value, + type: 'checkbox', + checked: options.checked !== undefined ? + options.checked : false, + required: options.required, + }) + + _makeLabel(options, {class: 'checkbox'}); } function makeSelect(options) { @@ -143,26 +143,6 @@ function makeFlexboxAlign(options) { .map(() => '
  • ').join(''); } -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); - const wrapper = document.createElement('div'); - wrapper.classList.add('message-wrapper'); - wrapper.appendChild(node); - messagesHolder.appendChild(wrapper); -} - function _serializeElement(name, attributes) { return [name] .concat(Object.keys(attributes).map(key => { @@ -186,16 +166,44 @@ function makeVoidElement(name, attributes) { return '<{0}/>'.format(_serializeElement(name, attributes)); } -function listenToMessages(target) { +function _messageHandler(target, message, className) { + if (!message) { + message = 'Unknown message'; + } + const messagesHolder = target.querySelector('.messages'); + if (!messagesHolder) { + return false; + } + /* TODO: animate this */ + const node = document.createElement('div'); + node.innerHTML = message.replace(/\n/g, '
    '); + node.classList.add('message'); + node.classList.add(className); + const wrapper = document.createElement('div'); + wrapper.classList.add('message-wrapper'); + wrapper.appendChild(node); + messagesHolder.appendChild(wrapper); + return true; +} + +function unlistenToMessages() { events.unlisten(events.Success); events.unlisten(events.Error); events.unlisten(events.Info); - events.listen( - events.Success, msg => { _messageHandler(target, msg, 'success'); }); - events.listen( - events.Error, msg => { _messageHandler(target, msg, 'error'); }); - events.listen( - events.Info, msg => { _messageHandler(target, msg, 'info'); }); +} + +function listenToMessages(target) { + unlistenToMessages(); + const listen = (eventType, className) => { + events.listen( + eventType, + msg => { + return _messageHandler(target, msg, className); + }); + }; + listen(events.Success, 'success'); + listen(events.Error, 'error'); + listen(events.Info, 'info'); } function clearMessages(target) { @@ -314,6 +322,7 @@ module.exports = { enableForm: enableForm, disableForm: disableForm, listenToMessages: listenToMessages, + unlistenToMessages: unlistenToMessages, clearMessages: clearMessages, decorateValidator: decorateValidator, makeVoidElement: makeVoidElement, diff --git a/client/js/views/empty_view.js b/client/js/views/empty_view.js index d4e36b6..2142fb8 100644 --- a/client/js/views/empty_view.js +++ b/client/js/views/empty_view.js @@ -10,7 +10,7 @@ class EmptyView { render(ctx) { const target = document.getElementById('content-holder'); const source = this.template; - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); } } diff --git a/client/js/views/endless_page_view.js b/client/js/views/endless_page_view.js index d961a0f..784d58c 100644 --- a/client/js/views/endless_page_view.js +++ b/client/js/views/endless_page_view.js @@ -15,7 +15,7 @@ class EndlessPageView { const source = this.holderTemplate(); const pageHeaderHolder = source.querySelector('.page-header-holder'); const pagesHolder = source.querySelector('.pages-holder'); - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); this.active = true; this.working = 0; diff --git a/client/js/views/help_view.js b/client/js/views/help_view.js index f43a1a6..b4ff278 100644 --- a/client/js/views/help_view.js +++ b/client/js/views/help_view.js @@ -61,7 +61,7 @@ class HelpView { } } - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); views.scrollToHash(); diff --git a/client/js/views/home_view.js b/client/js/views/home_view.js index b27fb14..c0d5df1 100644 --- a/client/js/views/home_view.js +++ b/client/js/views/home_view.js @@ -16,7 +16,7 @@ class HomeView { buildDate: config.meta.buildDate, }); - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); } } diff --git a/client/js/views/login_view.js b/client/js/views/login_view.js index 10ba67a..112cdab 100644 --- a/client/js/views/login_view.js +++ b/client/js/views/login_view.js @@ -36,7 +36,7 @@ class LoginView { .always(() => { views.enableForm(form); }); }); - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); } } diff --git a/client/js/views/manual_page_view.js b/client/js/views/manual_page_view.js index a74729c..53f0c4b 100644 --- a/client/js/views/manual_page_view.js +++ b/client/js/views/manual_page_view.js @@ -99,13 +99,13 @@ class ManualPageView { })); } - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); if (response.total <= (currentPage - 1) * response.pageSize) { events.notify(events.Info, 'No data to show'); } }, response => { - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); events.notify(events.Error, response.description); }); diff --git a/client/js/views/password_reset_view.js b/client/js/views/password_reset_view.js index 14f0737..67043c5 100644 --- a/client/js/views/password_reset_view.js +++ b/client/js/views/password_reset_view.js @@ -24,7 +24,7 @@ class PasswordResetView { .catch(() => { views.enableForm(form); }); }); - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); } } diff --git a/client/js/views/registration_view.js b/client/js/views/registration_view.js index 4a3178f..bcd8c74 100644 --- a/client/js/views/registration_view.js +++ b/client/js/views/registration_view.js @@ -33,7 +33,7 @@ class RegistrationView { .always(() => { views.enableForm(form); }); }); - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); } } diff --git a/client/js/views/settings_view.js b/client/js/views/settings_view.js index 19bf591..6809ee8 100644 --- a/client/js/views/settings_view.js +++ b/client/js/views/settings_view.js @@ -25,7 +25,7 @@ class SettingsView { }); }); - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); } } diff --git a/client/js/views/tag_categories_view.js b/client/js/views/tag_categories_view.js index f00c86f..7dd7115 100644 --- a/client/js/views/tag_categories_view.js +++ b/client/js/views/tag_categories_view.js @@ -101,7 +101,7 @@ class TagListHeaderView { this._saveButtonClickHandler(e, ctx, target); }); - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); } } diff --git a/client/js/views/user_delete_view.js b/client/js/views/user_delete_view.js index 740c205..c9e2be7 100644 --- a/client/js/views/user_delete_view.js +++ b/client/js/views/user_delete_view.js @@ -19,10 +19,11 @@ class UserDeleteView { e.preventDefault(); views.clearMessages(target); views.disableForm(form); - ctx.delete(); + ctx.delete() + .catch(() => { views.enableForm(form); }); }); - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); } } diff --git a/client/js/views/user_edit_view.js b/client/js/views/user_edit_view.js index bb5eb9c..0f9d858 100644 --- a/client/js/views/user_edit_view.js +++ b/client/js/views/user_edit_view.js @@ -54,7 +54,7 @@ class UserEditView { .always(() => { views.enableForm(form); }); }); - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); } } diff --git a/client/js/views/user_summary_view.js b/client/js/views/user_summary_view.js index 9369376..c740cce 100644 --- a/client/js/views/user_summary_view.js +++ b/client/js/views/user_summary_view.js @@ -10,7 +10,7 @@ class UserSummaryView { render(ctx) { const target = ctx.target; const source = this.template(ctx); - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); } } diff --git a/client/js/views/user_view.js b/client/js/views/user_view.js index 7cc8533..aac2e08 100644 --- a/client/js/views/user_view.js +++ b/client/js/views/user_view.js @@ -38,7 +38,7 @@ class UserView { ctx.target = source.querySelector('#user-content-holder'); view.render(ctx); - views.listenToMessages(target); + views.listenToMessages(source); views.showView(target, source); } }