diff --git a/client/js/api.js b/client/js/api.js
index b23daa59..f5cadfaa 100644
--- a/client/js/api.js
+++ b/client/js/api.js
@@ -51,7 +51,7 @@ class Api {
}
_process(url, requestFactory, data, files) {
- const fullUrl = this.getFullUrl(url);
+ const fullUrl = this._getFullUrl(url);
return new Promise((resolve, reject) => {
nprogress.start();
let req = requestFactory(fullUrl);
@@ -161,7 +161,7 @@ class Api {
}
}
- getFullUrl(url) {
+ _getFullUrl(url) {
return (config.apiUrl + '/' + url).replace(/([^:])\/+/g, '$1/');
}
}
diff --git a/client/js/controllers/auth_controller.js b/client/js/controllers/auth_controller.js
index 01c01431..4470ae0f 100644
--- a/client/js/controllers/auth_controller.js
+++ b/client/js/controllers/auth_controller.js
@@ -9,24 +9,24 @@ const PasswordResetView = require('../views/password_reset_view.js');
class AuthController {
constructor() {
- this.loginView = new LoginView();
- this.passwordResetView = new PasswordResetView();
+ this._loginView = new LoginView();
+ this._passwordResetView = new PasswordResetView();
}
registerRoutes() {
page(/\/password-reset\/([^:]+):([^:]+)$/,
(ctx, next) => {
- this.passwordResetFinishRoute(ctx.params[0], ctx.params[1]);
+ this._passwordResetFinishRoute(ctx.params[0], ctx.params[1]);
});
- page('/password-reset', (ctx, next) => { this.passwordResetRoute(); });
- page('/login', (ctx, next) => { this.loginRoute(); });
- page('/logout', (ctx, next) => { this.logoutRoute(); });
+ page('/password-reset', (ctx, next) => { this._passwordResetRoute(); });
+ page('/login', (ctx, next) => { this._loginRoute(); });
+ page('/logout', (ctx, next) => { this._logoutRoute(); });
}
- loginRoute() {
+ _loginRoute() {
api.forget();
topNavController.activate('login');
- this.loginView.render({
+ this._loginView.render({
login: (name, password, doRemember) => {
return new Promise((resolve, reject) => {
api.forget();
@@ -43,22 +43,22 @@ class AuthController {
}});
}
- logoutRoute() {
+ _logoutRoute() {
api.forget();
api.logout();
page('/');
events.notify(events.Success, 'Logged out');
}
- passwordResetRoute() {
+ _passwordResetRoute() {
topNavController.activate('login');
- this.passwordResetView.render({
+ this._passwordResetView.render({
proceed: (...args) => {
return this._passwordReset(...args);
}});
}
- passwordResetFinishRoute(name, token) {
+ _passwordResetFinishRoute(name, token) {
api.forget();
api.logout();
api.post('/password-reset/' + name, {token: token})
diff --git a/client/js/controllers/comments_controller.js b/client/js/controllers/comments_controller.js
index 1b20fcab..11ed6395 100644
--- a/client/js/controllers/comments_controller.js
+++ b/client/js/controllers/comments_controller.js
@@ -5,10 +5,10 @@ const topNavController = require('../controllers/top_nav_controller.js');
class CommentsController {
registerRoutes() {
- page('/comments', (ctx, next) => { this.listCommentsRoute(); });
+ page('/comments', (ctx, next) => { this._listCommentsRoute(); });
}
- listCommentsRoute() {
+ _listCommentsRoute() {
topNavController.activate('comments');
}
}
diff --git a/client/js/controllers/help_controller.js b/client/js/controllers/help_controller.js
index 797d2aca..6c352c33 100644
--- a/client/js/controllers/help_controller.js
+++ b/client/js/controllers/help_controller.js
@@ -6,24 +6,24 @@ const HelpView = require('../views/help_view.js');
class HelpController {
constructor() {
- this.helpView = new HelpView();
+ this._helpView = new HelpView();
}
registerRoutes() {
- page('/help', () => { this.showHelpRoute(); });
+ page('/help', () => { this._showHelpRoute(); });
page(
'/help/:section',
- (ctx, next) => { this.showHelpRoute(ctx.params.section); });
+ (ctx, next) => { this._showHelpRoute(ctx.params.section); });
page(
'/help/:section/:subsection',
(ctx, next) => {
- this.showHelpRoute(ctx.params.section, ctx.params.subsection);
+ this._showHelpRoute(ctx.params.section, ctx.params.subsection);
});
}
- showHelpRoute(section, subsection) {
+ _showHelpRoute(section, subsection) {
topNavController.activate('help');
- this.helpView.render({
+ this._helpView.render({
section: section,
subsection: subsection,
});
diff --git a/client/js/controllers/history_controller.js b/client/js/controllers/history_controller.js
index 8c0915e6..fdd85622 100644
--- a/client/js/controllers/history_controller.js
+++ b/client/js/controllers/history_controller.js
@@ -5,10 +5,10 @@ const topNavController = require('../controllers/top_nav_controller.js');
class HistoryController {
registerRoutes() {
- page('/history', (ctx, next) => { this.showHistoryRoute(); });
+ page('/history', (ctx, next) => { this._listHistoryRoute(); });
}
- listHistoryRoute() {
+ _listHistoryRoute() {
topNavController.activate('');
}
}
diff --git a/client/js/controllers/home_controller.js b/client/js/controllers/home_controller.js
index 32144e29..1e839c4e 100644
--- a/client/js/controllers/home_controller.js
+++ b/client/js/controllers/home_controller.js
@@ -6,20 +6,20 @@ const HomeView = require('../views/home_view.js');
class HomeController {
constructor() {
- this.homeView = new HomeView();
+ this._homeView = new HomeView();
}
registerRoutes() {
- page('/', (ctx, next) => { this.indexRoute(); });
- page('*', (ctx, next) => { this.notFoundRoute(); });
+ page('/', (ctx, next) => { this._indexRoute(); });
+ page('*', (ctx, next) => { this._notFoundRoute(); });
}
- indexRoute() {
+ _indexRoute() {
topNavController.activate('home');
- this.homeView.render({});
+ this._homeView.render({});
}
- notFoundRoute() {
+ _notFoundRoute() {
topNavController.activate('');
}
}
diff --git a/client/js/controllers/page_controller.js b/client/js/controllers/page_controller.js
index 7b589a37..ee4f4f19 100644
--- a/client/js/controllers/page_controller.js
+++ b/client/js/controllers/page_controller.js
@@ -8,27 +8,27 @@ const ManualPageView = require('../views/manual_page_view.js');
class PageController {
constructor() {
events.listen(events.SettingsChange, () => {
- this.update();
+ this._update();
return true;
});
- this.update();
+ this._update();
}
- update() {
+ _update() {
if (settings.getSettings().endlessScroll) {
- this.pageView = new EndlessPageView();
+ this._pageView = new EndlessPageView();
} else {
- this.pageView = new ManualPageView();
+ this._pageView = new ManualPageView();
}
}
run(ctx) {
- this.pageView.unrender();
- this.pageView.render(ctx);
+ this._pageView.unrender();
+ this._pageView.render(ctx);
}
stop() {
- this.pageView.unrender();
+ this._pageView.unrender();
}
}
diff --git a/client/js/controllers/posts_controller.js b/client/js/controllers/posts_controller.js
index f55cdef5..18cbc84a 100644
--- a/client/js/controllers/posts_controller.js
+++ b/client/js/controllers/posts_controller.js
@@ -5,29 +5,29 @@ const topNavController = require('../controllers/top_nav_controller.js');
class PostsController {
registerRoutes() {
- page('/upload', (ctx, next) => { this.uploadPostsRoute(); });
- page('/posts', (ctx, next) => { this.listPostsRoute(); });
+ page('/upload', (ctx, next) => { this._uploadPostsRoute(); });
+ page('/posts', (ctx, next) => { this._listPostsRoute(); });
page(
'/post/:id',
- (ctx, next) => { this.showPostRoute(ctx.params.id); });
+ (ctx, next) => { this._showPostRoute(ctx.params.id); });
page(
'/post/:id/edit',
- (ctx, next) => { this.editPostRoute(ctx.params.id); });
+ (ctx, next) => { this._editPostRoute(ctx.params.id); });
}
- uploadPostsRoute() {
+ _uploadPostsRoute() {
topNavController.activate('upload');
}
- listPostsRoute() {
+ _listPostsRoute() {
topNavController.activate('posts');
}
- showPostRoute(id) {
+ _showPostRoute(id) {
topNavController.activate('posts');
}
- editPostRoute(id) {
+ _editPostRoute(id) {
topNavController.activate('posts');
}
}
diff --git a/client/js/controllers/settings_controller.js b/client/js/controllers/settings_controller.js
index 777bf3ae..96da12c9 100644
--- a/client/js/controllers/settings_controller.js
+++ b/client/js/controllers/settings_controller.js
@@ -7,16 +7,16 @@ const SettingsView = require('../views/settings_view.js');
class SettingsController {
constructor() {
- this.settingsView = new SettingsView();
+ this._settingsView = new SettingsView();
}
registerRoutes() {
- page('/settings', (ctx, next) => { this.settingsRoute(); });
+ page('/settings', (ctx, next) => { this._settingsRoute(); });
}
- settingsRoute() {
+ _settingsRoute() {
topNavController.activate('settings');
- this.settingsView.render({
+ 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 c096c163..91330ba4 100644
--- a/client/js/controllers/tags_controller.js
+++ b/client/js/controllers/tags_controller.js
@@ -15,31 +15,31 @@ 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();
+ this._tagView = new TagView();
+ this._tagsHeaderView = new TagsHeaderView();
+ this._tagsPageView = new TagsPageView();
+ this._tagCategoriesView = new TagCategoriesView();
+ this._emptyView = new EmptyView();
}
registerRoutes() {
- page('/tag-categories', () => { this.tagCategoriesRoute(); });
+ page('/tag-categories', () => { this._tagCategoriesRoute(); });
page(
'/tag/:name',
- (ctx, next) => { this.loadTagRoute(ctx, next); },
- (ctx, next) => { this.showTagRoute(ctx, next); });
+ (ctx, next) => { this._loadTagRoute(ctx, next); },
+ (ctx, next) => { this._showTagRoute(ctx, next); });
page(
'/tag/:name/merge',
- (ctx, next) => { this.loadTagRoute(ctx, next); },
- (ctx, next) => { this.mergeTagRoute(ctx, next); });
+ (ctx, next) => { this._loadTagRoute(ctx, next); },
+ (ctx, next) => { this._mergeTagRoute(ctx, next); });
page(
'/tag/:name/delete',
- (ctx, next) => { this.loadTagRoute(ctx, next); },
- (ctx, next) => { this.deleteTagRoute(ctx, next); });
+ (ctx, next) => { this._loadTagRoute(ctx, next); },
+ (ctx, next) => { this._deleteTagRoute(ctx, next); });
page(
'/tags/:query?',
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
- (ctx, next) => { this.listTagsRoute(ctx, next); });
+ (ctx, next) => { this._listTagsRoute(ctx, next); });
}
_saveTagCategories(addedCategories, changedCategories, removedCategories) {
@@ -64,34 +64,35 @@ class TagsController {
});
}
- loadTagRoute(ctx, next) {
+ _loadTagRoute(ctx, next) {
if (ctx.state.tag) {
next();
- } else if (this.tag && this.tag.names == ctx.params.names) {
- ctx.state.tag = this.tag;
+ } 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.tag;
ctx.save();
- this.tag = response.tag;
+ this._cachedTag = response.tag;
next();
}, response => {
- this.emptyView.render();
+ this._emptyView.render();
events.notify(events.Error, response.description);
});
}
}
- showTagRoute(ctx, next) {
+ _showTagRoute(ctx, next) {
this._show(ctx.state.tag, 'summary');
}
- mergeTagRoute(ctx, next) {
+ _mergeTagRoute(ctx, next) {
this._show(ctx.state.tag, 'merge');
}
- deleteTagRoute(ctx, next) {
+ _deleteTagRoute(ctx, next) {
this._show(ctx.state.tag, 'delete');
}
@@ -101,7 +102,7 @@ class TagsController {
for (let category of tags.getAllCategories()) {
categories[category.name] = category.name;
}
- this.tagView.render({
+ this._tagView.render({
tag: tag,
section: section,
canEditNames: api.hasPrivilege('tags:edit:names'),
@@ -152,10 +153,10 @@ class TagsController {
});
}
- tagCategoriesRoute(ctx, next) {
+ _tagCategoriesRoute(ctx, next) {
topNavController.activate('tags');
api.get('/tag-categories/').then(response => {
- this.tagCategoriesView.render({
+ this._tagCategoriesView.render({
tagCategories: response.results,
canEditName: api.hasPrivilege('tagCategories:edit:name'),
canEditColor: api.hasPrivilege('tagCategories:edit:color'),
@@ -173,12 +174,12 @@ class TagsController {
}
});
}, response => {
- this.emptyView.render();
+ this._emptyView.render();
events.notify(events.Error, response.description);
});
}
- listTagsRoute(ctx, next) {
+ _listTagsRoute(ctx, next) {
topNavController.activate('tags');
pageController.run({
@@ -192,8 +193,8 @@ class TagsController {
clientUrl: '/tags/' + misc.formatSearchQuery({
text: ctx.searchQuery.text, page: '{page}'}),
searchQuery: ctx.searchQuery,
- headerRenderer: this.tagsHeaderView,
- pageRenderer: this.tagsPageView,
+ headerRenderer: this._tagsHeaderView,
+ pageRenderer: this._tagsPageView,
canEditTagCategories: api.hasPrivilege('tagCategories:edit'),
});
}
diff --git a/client/js/controllers/top_nav_controller.js b/client/js/controllers/top_nav_controller.js
index cab1a4d5..f030770e 100644
--- a/client/js/controllers/top_nav_controller.js
+++ b/client/js/controllers/top_nav_controller.js
@@ -16,10 +16,10 @@ class NavigationItem {
class TopNavController {
constructor() {
- this.topNavView = new TopNavView();
- this.activeItem = null;
+ this._topNavView = new TopNavView();
+ this._activeItem = null;
- this.items = {
+ this._items = {
'home': new NavigationItem('H', 'Home', '/'),
'posts': new NavigationItem('P', 'Posts', '/posts'),
'upload': new NavigationItem('U', 'Upload', '/upload'),
@@ -36,11 +36,11 @@ class TopNavController {
};
const rerender = () => {
- this.updateVisibility();
- this.topNavView.render({
- items: this.items,
- activeItem: this.activeItem});
- this.topNavView.activate(this.activeItem);
+ this._updateVisibility();
+ this._topNavView.render({
+ items: this._items,
+ activeItem: this._activeItem});
+ this._topNavView.activate(this._activeItem);
};
events.listen(
@@ -49,41 +49,41 @@ class TopNavController {
rerender();
}
- updateVisibility() {
- this.items.account.url = '/user/' + api.userName;
- this.items.account.imageUrl = api.user ? api.user.avatarUrl : null;
+ _updateVisibility() {
+ this._items.account.url = '/user/' + api.userName;
+ this._items.account.imageUrl = api.user ? api.user.avatarUrl : null;
- const b = Object.keys(this.items);
+ const b = Object.keys(this._items);
for (let key of b) {
- this.items[key].available = true;
+ this._items[key].available = true;
}
if (!api.hasPrivilege('posts:list')) {
- this.items.posts.available = false;
+ this._items.posts.available = false;
}
if (!api.hasPrivilege('posts:create')) {
- this.items.upload.available = false;
+ this._items.upload.available = false;
}
if (!api.hasPrivilege('comments:list')) {
- this.items.comments.available = false;
+ this._items.comments.available = false;
}
if (!api.hasPrivilege('tags:list')) {
- this.items.tags.available = false;
+ this._items.tags.available = false;
}
if (!api.hasPrivilege('users:list')) {
- this.items.users.available = false;
+ this._items.users.available = false;
}
if (api.isLoggedIn()) {
- this.items.register.available = false;
- this.items.login.available = false;
+ this._items.register.available = false;
+ this._items.login.available = false;
} else {
- this.items.account.available = false;
- this.items.logout.available = false;
+ this._items.account.available = false;
+ this._items.logout.available = false;
}
}
activate(itemName) {
- this.activeItem = itemName;
- this.topNavView.activate(this.activeItem);
+ this._activeItem = itemName;
+ this._topNavView.activate(this._activeItem);
}
}
diff --git a/client/js/controllers/users_controller.js b/client/js/controllers/users_controller.js
index 529f4903..33c8620a 100644
--- a/client/js/controllers/users_controller.js
+++ b/client/js/controllers/users_controller.js
@@ -26,42 +26,42 @@ const rankNames = {
class UsersController {
constructor() {
- this.registrationView = new RegistrationView();
- this.userView = new UserView();
- this.usersHeaderView = new UsersHeaderView();
- this.usersPageView = new UsersPageView();
- this.emptyView = new EmptyView();
+ this._registrationView = new RegistrationView();
+ this._userView = new UserView();
+ this._usersHeaderView = new UsersHeaderView();
+ this._usersPageView = new UsersPageView();
+ this._emptyView = new EmptyView();
}
registerRoutes() {
- page('/register', () => { this.createUserRoute(); });
+ page('/register', () => { this._createUserRoute(); });
page(
'/users/:query?',
(ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
- (ctx, next) => { this.listUsersRoute(ctx, next); });
+ (ctx, next) => { this._listUsersRoute(ctx, next); });
page(
'/user/:name',
- (ctx, next) => { this.loadUserRoute(ctx, next); },
- (ctx, next) => { this.showUserRoute(ctx, next); });
+ (ctx, next) => { this._loadUserRoute(ctx, next); },
+ (ctx, next) => { this._showUserRoute(ctx, next); });
page(
'/user/:name/edit',
- (ctx, next) => { this.loadUserRoute(ctx, next); },
- (ctx, next) => { this.editUserRoute(ctx, next); });
+ (ctx, next) => { this._loadUserRoute(ctx, next); },
+ (ctx, next) => { this._editUserRoute(ctx, next); });
page(
'/user/:name/delete',
- (ctx, next) => { this.loadUserRoute(ctx, next); },
- (ctx, next) => { this.deleteUserRoute(ctx, next); });
+ (ctx, next) => { this._loadUserRoute(ctx, next); },
+ (ctx, next) => { this._deleteUserRoute(ctx, next); });
page.exit(/\/users\/.*/, (ctx, next) => {
pageController.stop();
next();
});
page.exit(/\/user\/.*/, (ctx, next) => {
- this.user = null;
+ this._cachedUser = null;
next();
});
}
- listUsersRoute(ctx, next) {
+ _listUsersRoute(ctx, next) {
topNavController.activate('users');
pageController.run({
@@ -75,48 +75,48 @@ class UsersController {
clientUrl: '/users/' + misc.formatSearchQuery({
text: ctx.searchQuery.text, page: '{page}'}),
searchQuery: ctx.searchQuery,
- headerRenderer: this.usersHeaderView,
- pageRenderer: this.usersPageView,
+ headerRenderer: this._usersHeaderView,
+ pageRenderer: this._usersPageView,
});
}
- createUserRoute() {
+ _createUserRoute() {
topNavController.activate('register');
- this.registrationView.render({
+ this._registrationView.render({
register: (...args) => {
return this._register(...args);
}});
}
- loadUserRoute(ctx, next) {
+ _loadUserRoute(ctx, next) {
if (ctx.state.user) {
next();
- } else if (this.user && this.user.name == ctx.params.name) {
- ctx.state.user = this.user;
+ } 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.user.rankName = rankNames[response.user.rank];
ctx.state.user = response.user;
ctx.save();
- this.user = response.user;
+ this._cachedUser = response.user;
next();
}, response => {
- this.emptyView.render();
+ this._emptyView.render();
events.notify(events.Error, response.description);
});
}
}
- showUserRoute(ctx, next) {
+ _showUserRoute(ctx, next) {
this._show(ctx.state.user, 'summary');
}
- editUserRoute(ctx, next) {
+ _editUserRoute(ctx, next) {
this._show(ctx.state.user, 'edit');
}
- deleteUserRoute(ctx, next) {
+ _deleteUserRoute(ctx, next) {
this._show(ctx.state.user, 'delete');
}
@@ -171,7 +171,7 @@ class UsersController {
return new Promise((resolve, reject) => {
api.put('/user/' + user.name, data, files)
.then(response => {
- this.user = response.user;
+ this._cachedUser = response.user;
return isLoggedIn ?
api.login(
data.name || api.userName,
@@ -236,7 +236,7 @@ class UsersController {
} else {
topNavController.activate('users');
}
- this.userView.render({
+ this._userView.render({
user: user,
section: section,
isLoggedIn: isLoggedIn,
diff --git a/client/js/controls/auto_complete_control.js b/client/js/controls/auto_complete_control.js
new file mode 100644
index 00000000..75327593
--- /dev/null
+++ b/client/js/controls/auto_complete_control.js
@@ -0,0 +1,297 @@
+'use strict';
+
+const lodash = require('lodash');
+const views = require('../util/views.js');
+
+const KEY_TAB = 9;
+const KEY_RETURN = 13;
+const KEY_DELETE = 46;
+const KEY_ESCAPE = 27;
+const KEY_UP = 38;
+const KEY_DOWN = 40;
+
+function _getSelectionStart(input) {
+ if ('selectionStart' in input) {
+ return input.selectionStart;
+ }
+ if (document.selection) {
+ input.focus();
+ const sel = document.selection.createRange();
+ const selLen = document.selection.createRange().text.length;
+ sel.moveStart('character', -input.value.length);
+ return sel.text.length - selLen;
+ }
+ return 0;
+}
+
+class AutoCompleteControl {
+ constructor(sourceInputNode, options) {
+ this._sourceInputNode = sourceInputNode;
+ this._options = lodash.extend({}, {
+ verticalShift: 2,
+ source: null,
+ maxResults: 15,
+ getTextToFind: () => {
+ const value = sourceInputNode.value;
+ const start = _getSelectionStart(sourceInputNode);
+ return value.substring(0, start).replace(/.*\s+/, '');
+ },
+ confirm: text => {
+ const start = _getSelectionStart(sourceInputNode);
+ let prefix = '';
+ let suffix = sourceInputNode.value.substring(start);
+ let middle = sourceInputNode.value.substring(0, start);
+ const index = middle.lastIndexOf(' ');
+ if (index !== -1) {
+ prefix = sourceInputNode.value.substring(0, index + 1);
+ middle = sourceInputNode.value.substring(index + 1);
+ }
+ sourceInputNode.value = prefix +
+ this._results[this._activeResult].value +
+ ' ' +
+ suffix.trimLeft();
+ sourceInputNode.focus();
+ },
+ delete: text => {
+ },
+ getMatches: null,
+ }, options);
+
+ this._showTimeout = null;
+ this._results = [];
+ this._activeResult = -1;
+
+ this._mutationObserver = new MutationObserver(
+ mutations => {
+ for (let mutation of mutations) {
+ for (let node of mutation.removedNodes) {
+ if (node.contains(this._sourceInputNode)) {
+ this._uninstall();
+ return;
+ }
+ }
+ }
+ });
+
+ this._install();
+ }
+
+ hide() {
+ window.clearTimeout(this._showTimeout);
+ this._suggestionDiv.style.display = 'none';
+ this._isVisible = false;
+ }
+
+ _show() {
+ this._suggestionDiv.style.display = 'block';
+ this._isVisible = true;
+ }
+
+ _showOrHide() {
+ const textToFind = this._options.getTextToFind();
+ if (!textToFind || !textToFind.length) {
+ this.hide();
+ } else {
+ this._updateResults(textToFind);
+ this._refreshList();
+ }
+ }
+
+ _install() {
+ if (!this._sourceInputNode) {
+ throw new Error('Input element was not found');
+ }
+ if (this._sourceInputNode.getAttribute('data-autocomplete')) {
+ throw new Error(
+ 'Autocompletion was already added for this element');
+ }
+ this._sourceInputNode.setAttribute('data-autocomplete', true);
+ this._sourceInputNode.setAttribute('autocomplete', 'off');
+
+ this._mutationObserver.observe(
+ document.body, {childList: true, subtree: true});
+ this._sourceInputNode.addEventListener(
+ 'keydown', e => this._evtKeyDown(e));
+ this._sourceInputNode.addEventListener(
+ 'blur', e => this._evtBlur(e));
+
+ this._suggestionDiv = views.htmlToDom(
+ '
');
+ this._suggestionList = this._suggestionDiv.querySelector('ul');
+ document.body.appendChild(this._suggestionDiv);
+ }
+
+ _uninstall() {
+ window.clearTimeout(this._showTimeout);
+ this._mutationObserver.disconnect();
+ document.body.removeChild(this._suggestionDiv);
+ }
+
+ _evtKeyDown(e) {
+ const key = e.which;
+ const shift = e.shiftKey;
+ let func = null;
+ if (this._isVisible) {
+ if (key === KEY_ESCAPE) {
+ func = this.hide;
+ } else if (key === KEY_TAB && shift) {
+ func = () => { this._selectPrevious(); };
+ } else if (key === KEY_TAB && !shift) {
+ func = () => { this._selectNext(); };
+ } else if (key === KEY_UP) {
+ func = () => { this._selectPrevious(); };
+ } else if (key === KEY_DOWN) {
+ func = () => { this._selectNext(); };
+ } else if (key === KEY_RETURN && this._activeResult >= 0) {
+ func = () => {
+ this._options.confirm(this._getActiveSuggestion());
+ this.hide();
+ };
+ } else if (key === KEY_DELETE && this._activeResult >= 0) {
+ func = () => {
+ this._options.delete(this._getActiveSuggestion());
+ this.hide();
+ };
+ }
+ }
+
+ if (func !== null) {
+ e.preventDefault();
+ e.stopPropagation();
+ e.stopImmediatePropagation();
+ func();
+ } else {
+ window.clearTimeout(this._showTimeout);
+ this._showTimeout = window.setTimeout(
+ () => { this._showOrHide(); }, 250);
+ }
+ }
+
+ _evtBlur(e) {
+ window.clearTimeout(this._showTimeout);
+ window.setTimeout(() => { this.hide(); }, 50);
+ }
+
+ _getActiveSuggestion() {
+ if (this._activeResult === -1) {
+ return null;
+ }
+ return this._results[this._activeResult].value;
+ }
+
+ _selectPrevious() {
+ this._select(this._activeResult === -1 ?
+ this._results.length - 1 :
+ this._activeResult - 1);
+ }
+
+ _selectNext() {
+ this._select(this._activeResult === -1 ? 0 : this._activeResult + 1);
+ }
+
+ _select(newActiveResult) {
+ this._activeResult =
+ newActiveResult.between(0, this._results.length - 1, true) ?
+ newActiveResult :
+ -1;
+ this._refreshActiveResult();
+ }
+
+ _updateResults(textToFind) {
+ const oldResults = this._results.slice();
+ this._results =
+ this._options.getMatches(textToFind)
+ .slice(0, this._options.maxResults);
+ if (!lodash.isEqual(oldResults, this._results)) {
+ this._activeResult = -1;
+ }
+ }
+
+ _refreshList() {
+ if (this._results.length === 0) {
+ this.hide();
+ return;
+ }
+
+ while (this._suggestionList.firstChild) {
+ this._suggestionList.removeChild(this._suggestionList.firstChild);
+ }
+ lodash.each(
+ this._results,
+ (resultItem, resultIndex) => {
+ const listItem = document.createElement('li');
+ const link = document.createElement('a');
+ link.href = '#';
+ link.innerHTML = resultItem.caption;
+ link.setAttribute('data-key', resultItem.value);
+ link.addEventListener(
+ 'mouseenter',
+ e => {
+ e.preventDefault();
+ this._activeResult = resultIndex;
+ this._refreshActiveResult();
+ });
+ link.addEventListener(
+ 'mousedown',
+ e => {
+ e.preventDefault();
+ this._activeResult = resultIndex;
+ this._options.confirm(this._getActiveSuggestion());
+ this.hide();
+ });
+ listItem.appendChild(link);
+ this._suggestionList.appendChild(listItem);
+ });
+ this._refreshActiveResult();
+
+ // display the suggestions offscreen to get the height
+ this._suggestionDiv.style.left = '-9999px';
+ this._suggestionDiv.style.top = '-9999px';
+ this._show();
+ const verticalShift = this._options.verticalShift;
+ const inputRect = this._sourceInputNode.getBoundingClientRect();
+ const bodyRect = document.body.getBoundingClientRect();
+ const viewPortHeight = bodyRect.bottom - bodyRect.top;
+ let listRect = this._suggestionDiv.getBoundingClientRect();
+
+ // choose where to view the suggestions: if there's more space above
+ // the input - draw the suggestions above it, otherwise below
+ const direction =
+ inputRect.top + inputRect.height / 2 < viewPortHeight / 2 ? 1 : -1;
+
+ let x = inputRect.left - bodyRect.left;
+ let y = direction == 1 ?
+ inputRect.bottom - bodyRect.top - verticalShift :
+ inputRect.top - bodyRect.top - listRect.height + verticalShift;
+
+ // remove offscreen items until whole suggestion list can fit on the
+ // screen
+ while ((y < 0 || y + listRect.height > viewPortHeight) &&
+ this._suggestionList.childNodes.length) {
+ this._suggestionList.removeChild(this._suggestionList.lastChild);
+ const prevHeight = listRect.height;
+ listRect = this._suggestionDiv.getBoundingClientRect();
+ const heightDelta = prevHeight - listRect.height;
+ if (direction == -1) {
+ y += heightDelta;
+ }
+ }
+
+ this._suggestionDiv.style.left = x + 'px';
+ this._suggestionDiv.style.top = y + 'px';
+ }
+
+ _refreshActiveResult() {
+ let activeItem = this._suggestionList.querySelector('li.active');
+ if (activeItem) {
+ activeItem.classList.remove('active');
+ }
+ if (this._activeResult >= 0) {
+ const allItems = this._suggestionList.querySelectorAll('li');
+ activeItem = allItems[this._activeResult];
+ activeItem.classList.add('active');
+ }
+ }
+};
+
+module.exports = AutoCompleteControl;
diff --git a/client/js/controls/file_dropper_control.js b/client/js/controls/file_dropper_control.js
new file mode 100644
index 00000000..c5b53606
--- /dev/null
+++ b/client/js/controls/file_dropper_control.js
@@ -0,0 +1,76 @@
+'use strict';
+
+const views = require('../util/views.js');
+
+class FileDropperControl {
+ constructor(target, options) {
+ this._options = options;
+ this._template = views.getTemplate('file-dropper');
+ const source = this._template({
+ allowMultiple: this._options.allowMultiple,
+ id: 'file-' + Math.random().toString(36).substring(7),
+ });
+
+ this._dropperNode = source.querySelector('.file-dropper');
+ this._fileInputNode = source.querySelector('input');
+ this._fileInputNode.style.display = 'none';
+ this._fileInputNode.multiple = this._options._allowMultiple || false;
+
+ this._counter = 0;
+ this._dropperNode.addEventListener(
+ 'dragenter', e => this._evtDragEnter(e));
+ this._dropperNode.addEventListener(
+ 'dragleave', e => this._evtDragLeave(e));
+ this._dropperNode.addEventListener(
+ 'dragover', e => this._evtDragOver(e));
+ this._dropperNode.addEventListener(
+ 'drop', e => this._evtDrop(e));
+ this._fileInputNode.addEventListener(
+ 'change', e => this._evtFileChange(e));
+
+ views.showView(target, source);
+ }
+
+ _resolve(files) {
+ files = Array.from(files);
+ if (this._options.lock) {
+ this._dropperNode.innerText =
+ files.map(file => file.name).join(', ');
+ }
+ this._options.resolve(files);
+ };
+
+ _evtFileChange(e) {
+ this._resolve(e.target.files);
+ }
+
+ _evtDragEnter(e) {
+ this._dropperNode.classList.add('active');
+ counter++;
+ }
+
+ _evtDragLeave(e) {
+ this._counter--;
+ if (this._counter === 0) {
+ this._dropperNode.classList.remove('active');
+ }
+ }
+
+ _evtDragOver(e) {
+ e.preventDefault();
+ }
+
+ _evtDrop(e) {
+ e.preventDefault();
+ this._dropperNode.classList.remove('active');
+ if (!e.dataTransfer.files.length) {
+ window.alert('Only files are supported.');
+ }
+ if (!this._options.allowMultiple && e.dataTransfer.files.length > 1) {
+ window.alert('Cannot select multiple files.');
+ }
+ this._resolve(e.dataTransfer.files);
+ }
+}
+
+module.exports = FileDropperControl;
diff --git a/client/js/views/tag_auto_complete_control.js b/client/js/controls/tag_auto_complete_control.js
similarity index 100%
rename from client/js/views/tag_auto_complete_control.js
rename to client/js/controls/tag_auto_complete_control.js
diff --git a/client/js/views/tag_input_control.js b/client/js/controls/tag_input_control.js
similarity index 97%
rename from client/js/views/tag_input_control.js
rename to client/js/controls/tag_input_control.js
index f608efa6..2412879d 100644
--- a/client/js/views/tag_input_control.js
+++ b/client/js/controls/tag_input_control.js
@@ -20,9 +20,12 @@ class TagInputControl {
this.readOnly = sourceInputNode.readOnly;
this._autoCompleteControls = [];
-
this._sourceInputNode = sourceInputNode;
+ this._install();
+ }
+
+ _install() {
// set up main edit area
this._editAreaNode = views.htmlToDom('');
this._editAreaNode.autocorrect = false;
@@ -43,12 +46,12 @@ class TagInputControl {
this._editAreaNode.appendChild(this._tailWrapperNode);
// add existing tags
- this.addMultipleTags(sourceInputNode.value);
+ this.addMultipleTags(this._sourceInputNode.value);
// show
- sourceInputNode.style.display = 'none';
- sourceInputNode.parentNode.insertBefore(
- this._editAreaNode, sourceInputNode.nextSibling);
+ this._sourceInputNode.style.display = 'none';
+ this._sourceInputNode.parentNode.insertBefore(
+ this._editAreaNode, this._sourceInputNode.nextSibling);
}
addMultipleTags(text, sourceNode) {
diff --git a/client/js/views/auto_complete_control.js b/client/js/views/auto_complete_control.js
deleted file mode 100644
index 5300412c..00000000
--- a/client/js/views/auto_complete_control.js
+++ /dev/null
@@ -1,298 +0,0 @@
-'use strict';
-
-const lodash = require('lodash');
-const views = require('../util/views.js');
-
-const KEY_TAB = 9;
-const KEY_RETURN = 13;
-const KEY_DELETE = 46;
-const KEY_ESCAPE = 27;
-const KEY_UP = 38;
-const KEY_DOWN = 40;
-
-function getSelectionStart(input) {
- if ('selectionStart' in input) {
- return input.selectionStart;
- }
- if (document.selection) {
- input.focus();
- const sel = document.selection.createRange();
- const selLen = document.selection.createRange().text.length;
- sel.moveStart('character', -input.value.length);
- return sel.text.length - selLen;
- }
- return 0;
-}
-
-class AutoCompleteControl {
- constructor(input, options) {
- this.input = input;
- this.options = lodash.extend({}, {
- verticalShift: 2,
- source: null,
- maxResults: 15,
- getTextToFind: () => {
- const value = this.input.value;
- const start = getSelectionStart(this.input);
- return value.substring(0, start).replace(/.*\s+/, '');
- },
- confirm: text => {
- const start = getSelectionStart(this.input);
- let prefix = '';
- let suffix = this.input.value.substring(start);
- let middle = this.input.value.substring(0, start);
- const index = middle.lastIndexOf(' ');
- if (index !== -1) {
- prefix = this.input.value.substring(0, index + 1);
- middle = this.input.value.substring(index + 1);
- }
- this.input.value = prefix +
- this.results[this.activeResult].value +
- ' ' +
- suffix.trimLeft();
- this.input.focus();
- },
- delete: text => {
- },
- getMatches: null,
- }, options);
-
- this.showTimeout = null;
- this.results = [];
- this.activeResult = -1;
-
- this.mutationObserver = new MutationObserver(
- mutations => {
- for (let mutation of mutations) {
- for (let node of mutation.removedNodes) {
- if (node.contains(input)) {
- this.uninstall();
- return;
- }
- }
- }
- });
-
- this.install();
- }
-
- uninstall() {
- window.clearTimeout(this.showTimeout);
- this.mutationObserver.disconnect();
- document.body.removeChild(this.suggestionDiv);
- }
-
- install() {
- if (!this.input) {
- throw new Error('Input element was not found');
- }
- if (this.input.getAttribute('data-autocomplete')) {
- throw new Error(
- 'Autocompletion was already added for this element');
- }
- this.input.setAttribute('data-autocomplete', true);
- this.input.setAttribute('autocomplete', 'off');
-
- this.mutationObserver.observe(
- document.body, {childList: true, subtree: true});
-
- this.input.addEventListener(
- 'keydown',
- e => {
- const key = e.which;
- const shift = e.shiftKey;
- let func = null;
- if (this.isVisible) {
- if (key === KEY_ESCAPE) {
- func = this.hide;
- } else if (key === KEY_TAB && shift) {
- func = () => { this.selectPrevious(); };
- } else if (key === KEY_TAB && !shift) {
- func = () => { this.selectNext(); };
- } else if (key === KEY_UP) {
- func = () => { this.selectPrevious(); };
- } else if (key === KEY_DOWN) {
- func = () => { this.selectNext(); };
- } else if (key === KEY_RETURN && this.activeResult >= 0) {
- func = () => {
- this.options.confirm(this.getActiveSuggestion());
- this.hide();
- };
- } else if (key === KEY_DELETE && this.activeResult >= 0) {
- func = () => {
- this.options.delete(this.getActiveSuggestion());
- this.hide();
- };
- }
- }
-
- if (func !== null) {
- e.preventDefault();
- e.stopPropagation();
- e.stopImmediatePropagation();
- func();
- } else {
- window.clearTimeout(this.showTimeout);
- this.showTimeout = window.setTimeout(
- () => { this.showOrHide(); },
- 250);
- }
- });
-
- this.input.addEventListener(
- 'blur',
- e => {
- window.clearTimeout(this.showTimeout);
- window.setTimeout(() => { this.hide(); }, 50);
- });
-
- this.suggestionDiv = views.htmlToDom(
- '');
- this.suggestionList = this.suggestionDiv.querySelector('ul');
- document.body.appendChild(this.suggestionDiv);
- }
-
- getActiveSuggestion() {
- if (this.activeResult === -1) {
- return null;
- }
- return this.results[this.activeResult].value;
- }
-
- showOrHide() {
- const textToFind = this.options.getTextToFind();
- if (!textToFind || !textToFind.length) {
- this.hide();
- } else {
- this.updateResults(textToFind);
- this.refreshList();
- }
- }
-
- show() {
- this.suggestionDiv.style.display = 'block';
- this.isVisible = true;
- }
-
- hide() {
- window.clearTimeout(this.showTimeout);
- this.suggestionDiv.style.display = 'none';
- this.isVisible = false;
- }
-
- selectPrevious() {
- this.select(this.activeResult === -1 ?
- this.results.length - 1 :
- this.activeResult - 1);
- }
-
- selectNext() {
- this.select(this.activeResult === -1 ? 0 : this.activeResult + 1);
- }
-
- select(newActiveResult) {
- this.activeResult =
- newActiveResult.between(0, this.results.length - 1, true) ?
- newActiveResult :
- -1;
- this.refreshActiveResult();
- }
-
- updateResults(textToFind) {
- const oldResults = this.results.slice();
- this.results =
- this.options.getMatches(textToFind)
- .slice(0, this.options.maxResults);
- if (!lodash.isEqual(oldResults, this.results)) {
- this.activeResult = -1;
- }
- }
-
- refreshList() {
- if (this.results.length === 0) {
- this.hide();
- return;
- }
-
- while (this.suggestionList.firstChild) {
- this.suggestionList.removeChild(this.suggestionList.firstChild);
- }
- lodash.each(
- this.results,
- (resultItem, resultIndex) => {
- const listItem = document.createElement('li');
- const link = document.createElement('a');
- link.href = '#';
- link.innerHTML = resultItem.caption;
- link.setAttribute('data-key', resultItem.value);
- link.addEventListener(
- 'mouseenter',
- e => {
- e.preventDefault();
- this.activeResult = resultIndex;
- this.refreshActiveResult();
- });
- link.addEventListener(
- 'mousedown',
- e => {
- e.preventDefault();
- this.activeResult = resultIndex;
- this.options.confirm(this.getActiveSuggestion());
- this.hide();
- });
- listItem.appendChild(link);
- this.suggestionList.appendChild(listItem);
- });
- this.refreshActiveResult();
-
- // display the suggestions offscreen to get the height
- this.suggestionDiv.style.left = '-9999px';
- this.suggestionDiv.style.top = '-9999px';
- this.show();
- const verticalShift = this.options.verticalShift;
- const inputRect = this.input.getBoundingClientRect();
- const bodyRect = document.body.getBoundingClientRect();
- const viewPortHeight = bodyRect.bottom - bodyRect.top;
- let listRect = this.suggestionDiv.getBoundingClientRect();
-
- // choose where to view the suggestions: if there's more space above
- // the input - draw the suggestions above it, otherwise below
- const direction =
- inputRect.top + inputRect.height / 2 < viewPortHeight / 2 ? 1 : -1;
-
- let x = inputRect.left - bodyRect.left;
- let y = direction == 1 ?
- inputRect.bottom - bodyRect.top - verticalShift :
- inputRect.top - bodyRect.top - listRect.height + verticalShift;
-
- // remove offscreen items until whole suggestion list can fit on the
- // screen
- while ((y < 0 || y + listRect.height > viewPortHeight) &&
- this.suggestionList.childNodes.length) {
- this.suggestionList.removeChild(this.suggestionList.lastChild);
- const prevHeight = listRect.height;
- listRect = this.suggestionDiv.getBoundingClientRect();
- const heightDelta = prevHeight - listRect.height;
- if (direction == -1) {
- y += heightDelta;
- }
- }
-
- this.suggestionDiv.style.left = x + 'px';
- this.suggestionDiv.style.top = y + 'px';
- }
-
- refreshActiveResult() {
- let activeItem = this.suggestionList.querySelector('li.active');
- if (activeItem) {
- activeItem.classList.remove('active');
- }
- if (this.activeResult >= 0) {
- const allItems = this.suggestionList.querySelectorAll('li');
- activeItem = allItems[this.activeResult];
- activeItem.classList.add('active');
- }
- }
-};
-
-module.exports = AutoCompleteControl;
diff --git a/client/js/views/empty_view.js b/client/js/views/empty_view.js
index 48c06fd8..39268659 100644
--- a/client/js/views/empty_view.js
+++ b/client/js/views/empty_view.js
@@ -4,7 +4,7 @@ const views = require('../util/views.js');
class EmptyView {
constructor() {
- this.template = () => {
+ this._template = () => {
return views.htmlToDom(
'');
};
@@ -12,7 +12,7 @@ class EmptyView {
render(ctx) {
const target = document.getElementById('content-holder');
- const source = this.template();
+ const source = this._template();
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 784d58cb..c4e36463 100644
--- a/client/js/views/endless_page_view.js
+++ b/client/js/views/endless_page_view.js
@@ -6,19 +6,19 @@ const views = require('../util/views.js');
class EndlessPageView {
constructor() {
- this.holderTemplate = views.getTemplate('endless-pager');
- this.pageTemplate = views.getTemplate('endless-pager-page');
+ 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 source = this._holderTemplate();
const pageHeaderHolder = source.querySelector('.page-header-holder');
const pagesHolder = source.querySelector('.pages-holder');
views.listenToMessages(source);
views.showView(target, source);
- this.active = true;
- this.working = 0;
+ this._active = true;
+ this._working = 0;
let headerRendererCtx = ctx;
headerRendererCtx.target = pageHeaderHolder;
@@ -31,8 +31,8 @@ class EndlessPageView {
this.totalPages = null;
this.currentPage = null;
- this.updater = () => {
- if (this.working) {
+ this._updater = () => {
+ if (this._working) {
return;
}
@@ -66,46 +66,46 @@ class EndlessPageView {
document.documentElement.clientHeight;
if (this.minPageShown > 1 && window.scrollY - threshold < 0) {
- this.loadPage(pagesHolder, ctx, this.minPageShown - 1, false)
- .then(() => this.updater());
+ this._loadPage(pagesHolder, ctx, this.minPageShown - 1, false)
+ .then(() => this._updater());
} else if (this.maxPageShown < this.totalPages &&
window.scrollY + threshold > scrollHeight) {
- this.loadPage(pagesHolder, ctx, this.maxPageShown + 1, true)
- .then(() => this.updater());
+ this._loadPage(pagesHolder, ctx, this.maxPageShown + 1, true)
+ .then(() => this._updater());
}
};
- this.loadPage(pagesHolder, ctx, ctx.searchQuery.page, true)
+ this._loadPage(pagesHolder, ctx, ctx.searchQuery.page, true)
.then(pageNode => {
if (ctx.searchQuery.page > 1) {
window.scroll(0, pageNode.getBoundingClientRect().top);
}
- this.updater();
+ this._updater();
});
- window.addEventListener('scroll', this.updater, true);
- window.addEventListener('unload', this.scrollToTop, true);
+ window.addEventListener('scroll', this._updater, true);
+ window.addEventListener('unload', this._scrollToTop, true);
}
unrender() {
- this.active = false;
- window.removeEventListener('scroll', this.updater, true);
- window.removeEventListener('unload', this.scrollToTop, true);
+ this._active = false;
+ window.removeEventListener('scroll', this._updater, true);
+ window.removeEventListener('unload', this._scrollToTop, true);
}
- scrollToTop() {
+ _scrollToTop() {
window.scroll(0, 0);
}
- loadPage(pagesHolder, ctx, pageNumber, append) {
- this.working++;
+ _loadPage(pagesHolder, ctx, pageNumber, append) {
+ this._working++;
return ctx.requestPage(pageNumber).then(response => {
- if (!this.active) {
- this.working--;
+ if (!this._active) {
+ this._working--;
return Promise.reject();
}
this.totalPages = Math.ceil(response.total / response.pageSize);
if (response.total) {
- const pageNode = this.pageTemplate({
+ const pageNode = this._pageTemplate({
page: pageNumber,
totalPages: this.totalPages,
});
@@ -137,17 +137,17 @@ class EndlessPageView {
window.scrollX,
window.scrollY + pageNode.offsetHeight);
}
- this.working--;
+ this._working--;
return Promise.resolve(pageNode);
}
if (response.total <= (pageNumber - 1) * response.pageSize) {
events.notify(events.Info, 'No data to show');
}
- this.working--;
+ this._working--;
return Promise.reject();
}, response => {
events.notify(events.Error, response.description);
- this.working--;
+ this._working--;
return Promise.reject();
});
}
diff --git a/client/js/views/file_dropper_control.js b/client/js/views/file_dropper_control.js
deleted file mode 100644
index 45b753ae..00000000
--- a/client/js/views/file_dropper_control.js
+++ /dev/null
@@ -1,67 +0,0 @@
-'use strict';
-
-const views = require('../util/views.js');
-
-class FileDropperControl {
- constructor() {
- this.template = views.getTemplate('file-dropper');
- }
-
- render(ctx) {
- const target = ctx.target;
- const source = this.template({
- allowMultiple: ctx.allowMultiple,
- id: 'file-' + Math.random().toString(36).substring(7),
- });
-
- const dropper = source.querySelector('.file-dropper');
- const fileInput = source.querySelector('input');
- fileInput.style.display = 'none';
- fileInput.multiple = ctx.allowMultiple || false;
-
- const resolve = files => {
- files = Array.from(files);
- if (ctx.lock) {
- dropper.innerText = files.map(file => file.name).join(', ');
- }
- ctx.resolve(files);
- };
-
- let counter = 0;
- dropper.addEventListener('dragenter', e => {
- dropper.classList.add('active');
- counter++;
- });
-
- dropper.addEventListener('dragleave', e => {
- counter--;
- if (counter === 0) {
- dropper.classList.remove('active');
- }
- });
-
- dropper.addEventListener('dragover', e => {
- e.preventDefault();
- });
-
- dropper.addEventListener('drop', e => {
- dropper.classList.remove('active');
- e.preventDefault();
- if (!e.dataTransfer.files.length) {
- window.alert('Only files are supported.');
- }
- if (!ctx.allowMultiple && e.dataTransfer.files.length > 1) {
- window.alert('Cannot select multiple files.');
- }
- resolve(e.dataTransfer.files);
- });
-
- fileInput.addEventListener('change', e => {
- resolve(e.target.files);
- });
-
- views.showView(target, source);
- }
-}
-
-module.exports = FileDropperControl;
diff --git a/client/js/views/help_view.js b/client/js/views/help_view.js
index b4ff2785..ffa4afda 100644
--- a/client/js/views/help_view.js
+++ b/client/js/views/help_view.js
@@ -5,14 +5,14 @@ const views = require('../util/views.js');
class HelpView {
constructor() {
- this.template = views.getTemplate('help');
- this.sectionTemplates = {};
+ 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._sectionTemplates[section] = views.getTemplate(templateName);
}
- this.subsectionTemplates = {
+ this._subsectionTemplates = {
'search': {
'default': views.getTemplate('help-search-general'),
'posts': views.getTemplate('help-search-posts'),
@@ -24,23 +24,23 @@ class HelpView {
render(ctx) {
const target = document.getElementById('content-holder');
- const source = this.template();
+ const source = this._template();
ctx.section = ctx.section || 'about';
- if (ctx.section in this.sectionTemplates) {
+ if (ctx.section in this._sectionTemplates) {
views.showView(
source.querySelector('.content'),
- this.sectionTemplates[ctx.section]({
+ this._sectionTemplates[ctx.section]({
name: config.name,
}));
}
ctx.subsection = ctx.subsection || 'default';
- if (ctx.section in this.subsectionTemplates &&
- ctx.subsection in this.subsectionTemplates[ctx.section]) {
+ if (ctx.section in this._subsectionTemplates &&
+ ctx.subsection in this._subsectionTemplates[ctx.section]) {
views.showView(
source.querySelector('.subcontent'),
- this.subsectionTemplates[ctx.section][ctx.subsection]({
+ this._subsectionTemplates[ctx.section][ctx.subsection]({
name: config.name,
}));
}
diff --git a/client/js/views/home_view.js b/client/js/views/home_view.js
index c0d5df15..cb19b195 100644
--- a/client/js/views/home_view.js
+++ b/client/js/views/home_view.js
@@ -5,12 +5,12 @@ const views = require('../util/views.js');
class HomeView {
constructor() {
- this.template = views.getTemplate('home');
+ this._template = views.getTemplate('home');
}
render(ctx) {
const target = document.getElementById('content-holder');
- const source = this.template({
+ const source = this._template({
name: config.name,
version: config.meta.version,
buildDate: config.meta.buildDate,
diff --git a/client/js/views/login_view.js b/client/js/views/login_view.js
index 112cdab5..ab07093d 100644
--- a/client/js/views/login_view.js
+++ b/client/js/views/login_view.js
@@ -5,12 +5,12 @@ const views = require('../util/views.js');
class LoginView {
constructor() {
- this.template = views.getTemplate('login');
+ this._template = views.getTemplate('login');
}
render(ctx) {
const target = document.getElementById('content-holder');
- const source = this.template({
+ const source = this._template({
userNamePattern: config.userNameRegex,
passwordPattern: config.passwordRegex,
canSendMails: config.canSendMails,
diff --git a/client/js/views/manual_page_view.js b/client/js/views/manual_page_view.js
index 53f0c4b0..9e176f8f 100644
--- a/client/js/views/manual_page_view.js
+++ b/client/js/views/manual_page_view.js
@@ -6,13 +6,13 @@ const keyboard = require('../util/keyboard.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
-function removeConsecutiveDuplicates(a) {
+function _removeConsecutiveDuplicates(a) {
return a.filter((item, pos, ary) => {
return !pos || item != ary[pos - 1];
});
}
-function getVisiblePageNumbers(currentPage, totalPages) {
+function _getVisiblePageNumbers(currentPage, totalPages) {
const threshold = 2;
let pagesVisible = [];
for (let i = 1; i <= threshold; i++) {
@@ -30,11 +30,11 @@ function getVisiblePageNumbers(currentPage, totalPages) {
return item >= 1 && item <= totalPages;
});
pagesVisible = pagesVisible.sort((a, b) => { return a - b; });
- pagesVisible = removeConsecutiveDuplicates(pagesVisible);
+ pagesVisible = _removeConsecutiveDuplicates(pagesVisible);
return pagesVisible;
}
-function getPages(currentPage, pageNumbers, clientUrl) {
+function _getPages(currentPage, pageNumbers, clientUrl) {
const pages = [];
let lastPage = 0;
for (let page of pageNumbers) {
@@ -53,13 +53,13 @@ function getPages(currentPage, pageNumbers, clientUrl) {
class ManualPageView {
constructor() {
- this.holderTemplate = views.getTemplate('manual-pager');
- this.navTemplate = views.getTemplate('manual-pager-nav');
+ this._holderTemplate = views.getTemplate('manual-pager');
+ this._navTemplate = views.getTemplate('manual-pager-nav');
}
render(ctx) {
const target = document.getElementById('content-holder');
- const source = this.holderTemplate();
+ const source = this._holderTemplate();
const pageContentHolder = source.querySelector('.page-content-holder');
const pageHeaderHolder = source.querySelector('.page-header-holder');
const pageNav = source.querySelector('.page-nav');
@@ -75,8 +75,8 @@ class ManualPageView {
ctx.pageRenderer.render(pageRendererCtx);
const totalPages = Math.ceil(response.total / response.pageSize);
- const pageNumbers = getVisiblePageNumbers(currentPage, totalPages);
- const pages = getPages(currentPage, pageNumbers, ctx.clientUrl);
+ const pageNumbers = _getVisiblePageNumbers(currentPage, totalPages);
+ const pages = _getPages(currentPage, pageNumbers, ctx.clientUrl);
keyboard.bind(['a', 'left'], () => {
if (currentPage > 1) {
@@ -90,7 +90,7 @@ class ManualPageView {
});
if (response.total) {
- views.showView(pageNav, this.navTemplate({
+ views.showView(pageNav, this._navTemplate({
prevLink: ctx.clientUrl.format({page: currentPage - 1}),
nextLink: ctx.clientUrl.format({page: currentPage + 1}),
prevLinkActive: currentPage > 1,
diff --git a/client/js/views/password_reset_view.js b/client/js/views/password_reset_view.js
index 67043c5e..0d67a193 100644
--- a/client/js/views/password_reset_view.js
+++ b/client/js/views/password_reset_view.js
@@ -4,12 +4,12 @@ const views = require('../util/views.js');
class PasswordResetView {
constructor() {
- this.template = views.getTemplate('password-reset');
+ this._template = views.getTemplate('password-reset');
}
render(ctx) {
const target = document.getElementById('content-holder');
- const source = this.template();
+ const source = this._template();
const form = source.querySelector('form');
const userNameOrEmailField = source.querySelector('#user-name');
diff --git a/client/js/views/registration_view.js b/client/js/views/registration_view.js
index bcd8c749..96e0681a 100644
--- a/client/js/views/registration_view.js
+++ b/client/js/views/registration_view.js
@@ -5,7 +5,7 @@ const views = require('../util/views.js');
class RegistrationView {
constructor() {
- this.template = views.getTemplate('user-registration');
+ this._template = views.getTemplate('user-registration');
}
render(ctx) {
@@ -13,7 +13,7 @@ class RegistrationView {
ctx.passwordPattern = config.passwordRegex;
const target = document.getElementById('content-holder');
- const source = this.template(ctx);
+ const source = this._template(ctx);
const form = source.querySelector('form');
const userNameField = source.querySelector('#user-name');
diff --git a/client/js/views/settings_view.js b/client/js/views/settings_view.js
index 6809ee8b..c5487b1d 100644
--- a/client/js/views/settings_view.js
+++ b/client/js/views/settings_view.js
@@ -4,12 +4,12 @@ const views = require('../util/views.js');
class SettingsView {
constructor() {
- this.template = views.getTemplate('settings');
+ this._template = views.getTemplate('settings');
}
render(ctx) {
const target = document.getElementById('content-holder');
- const source = this.template({browsingSettings: ctx.getSettings()});
+ const source = this._template({browsingSettings: ctx.getSettings()});
const form = source.querySelector('form');
views.decorateValidator(form);
diff --git a/client/js/views/tag_categories_view.js b/client/js/views/tag_categories_view.js
index 7dd71158..30e101ea 100644
--- a/client/js/views/tag_categories_view.js
+++ b/client/js/views/tag_categories_view.js
@@ -5,7 +5,7 @@ const views = require('../util/views.js');
class TagListHeaderView {
constructor() {
- this.template = views.getTemplate('tag-categories');
+ this._template = views.getTemplate('tag-categories');
}
_saveButtonClickHandler(e, ctx, target) {
@@ -73,7 +73,7 @@ class TagListHeaderView {
render(ctx) {
const target = document.getElementById('content-holder');
- const source = this.template(ctx);
+ const source = this._template(ctx);
const form = source.querySelector('form');
const newRowTemplate = source.querySelector('.add-template');
diff --git a/client/js/views/tag_delete_view.js b/client/js/views/tag_delete_view.js
index e3771d38..4cceb92e 100644
--- a/client/js/views/tag_delete_view.js
+++ b/client/js/views/tag_delete_view.js
@@ -4,12 +4,12 @@ const views = require('../util/views.js');
class TagDeleteView {
constructor() {
- this.template = views.getTemplate('tag-delete');
+ this._template = views.getTemplate('tag-delete');
}
render(ctx) {
const target = ctx.target;
- const source = this.template(ctx);
+ const source = this._template(ctx);
const form = source.querySelector('form');
diff --git a/client/js/views/tag_merge_view.js b/client/js/views/tag_merge_view.js
index ee40c45f..b18f951c 100644
--- a/client/js/views/tag_merge_view.js
+++ b/client/js/views/tag_merge_view.js
@@ -2,18 +2,19 @@
const config = require('../config.js');
const views = require('../util/views.js');
-const TagAutoCompleteControl = require('./tag_auto_complete_control.js');
+const TagAutoCompleteControl =
+ require('../controls/tag_auto_complete_control.js');
class TagMergeView {
constructor() {
- this.template = views.getTemplate('tag-merge');
+ this._template = views.getTemplate('tag-merge');
}
render(ctx) {
ctx.tagNamePattern = config.tagNameRegex;
const target = ctx.target;
- const source = this.template(ctx);
+ const source = this._template(ctx);
const form = source.querySelector('form');
const otherTagField = source.querySelector('.target input');
diff --git a/client/js/views/tag_summary_view.js b/client/js/views/tag_summary_view.js
index 8b5b19dc..5fdcfcff 100644
--- a/client/js/views/tag_summary_view.js
+++ b/client/js/views/tag_summary_view.js
@@ -2,7 +2,7 @@
const config = require('../config.js');
const views = require('../util/views.js');
-const TagInputControl = require('./tag_input_control.js');
+const TagInputControl = require('../controls/tag_input_control.js');
function split(str) {
return str.split(/\s+/).filter(s => s);
@@ -10,7 +10,7 @@ function split(str) {
class TagSummaryView {
constructor() {
- this.template = views.getTemplate('tag-summary');
+ this._template = views.getTemplate('tag-summary');
}
render(ctx) {
@@ -18,7 +18,7 @@ class TagSummaryView {
ctx.tagNamesPattern = '^((' + baseRegex + ')\\s+)*(' + baseRegex + ')$';
const target = ctx.target;
- const source = this.template(ctx);
+ const source = this._template(ctx);
const form = source.querySelector('form');
const namesField = source.querySelector('.names input');
diff --git a/client/js/views/tag_view.js b/client/js/views/tag_view.js
index e022a87d..eea72403 100644
--- a/client/js/views/tag_view.js
+++ b/client/js/views/tag_view.js
@@ -7,15 +7,15 @@ 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();
+ this._template = views.getTemplate('tag');
+ this._summaryView = new TagSummaryView();
+ this._mergeView = new TagMergeView();
+ this._deleteView = new TagDeleteView();
}
render(ctx) {
const target = document.getElementById('content-holder');
- const source = this.template(ctx);
+ const source = this._template(ctx);
ctx.section = ctx.section || 'summary';
@@ -29,11 +29,11 @@ class TagView {
let view = null;
if (ctx.section == 'merge') {
- view = this.mergeView;
+ view = this._mergeView;
} else if (ctx.section == 'delete') {
- view = this.deleteView;
+ view = this._deleteView;
} else {
- view = this.summaryView;
+ view = this._summaryView;
}
ctx.target = source.querySelector('.tag-content-holder');
view.render(ctx);
diff --git a/client/js/views/tags_header_view.js b/client/js/views/tags_header_view.js
index 565f7de0..ac4dbf5e 100644
--- a/client/js/views/tags_header_view.js
+++ b/client/js/views/tags_header_view.js
@@ -4,16 +4,17 @@ const page = require('page');
const keyboard = require('../util/keyboard.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
-const TagAutoCompleteControl = require('./tag_auto_complete_control.js');
+const TagAutoCompleteControl =
+ require('../controls/tag_auto_complete_control.js');
class TagsHeaderView {
constructor() {
- this.template = views.getTemplate('tags-header');
+ this._template = views.getTemplate('tags-header');
}
render(ctx) {
const target = ctx.target;
- const source = this.template(ctx);
+ const source = this._template(ctx);
const form = source.querySelector('form');
const searchTextInput = form.querySelector('[name=search-text]');
diff --git a/client/js/views/tags_page_view.js b/client/js/views/tags_page_view.js
index 26c0f28b..26d36307 100644
--- a/client/js/views/tags_page_view.js
+++ b/client/js/views/tags_page_view.js
@@ -4,12 +4,12 @@ const views = require('../util/views.js');
class TagsPageView {
constructor() {
- this.template = views.getTemplate('tags-page');
+ this._template = views.getTemplate('tags-page');
}
render(ctx) {
const target = ctx.target;
- const source = this.template(ctx);
+ const source = this._template(ctx);
views.showView(target, source);
}
}
diff --git a/client/js/views/top_nav_view.js b/client/js/views/top_nav_view.js
index 9a1d2796..22973ff0 100644
--- a/client/js/views/top_nav_view.js
+++ b/client/js/views/top_nav_view.js
@@ -4,13 +4,13 @@ const views = require('../util/views.js');
class TopNavView {
constructor() {
- this.template = views.getTemplate('top-nav');
- this.navHolder = document.getElementById('top-nav-holder');
+ this._template = views.getTemplate('top-nav');
+ this._navHolder = document.getElementById('top-nav-holder');
}
render(ctx) {
- const target = this.navHolder;
- const source = this.template(ctx);
+ const target = this._navHolder;
+ const source = this._template(ctx);
for (let link of source.querySelectorAll('a')) {
const regex = new RegExp(
@@ -21,7 +21,7 @@ class TopNavView {
'$1');
}
- views.showView(this.navHolder, source);
+ views.showView(this._navHolder, source);
}
activate(itemName) {
diff --git a/client/js/views/user_delete_view.js b/client/js/views/user_delete_view.js
index c9e2be7d..8fd07cdc 100644
--- a/client/js/views/user_delete_view.js
+++ b/client/js/views/user_delete_view.js
@@ -4,12 +4,12 @@ const views = require('../util/views.js');
class UserDeleteView {
constructor() {
- this.template = views.getTemplate('user-delete');
+ this._template = views.getTemplate('user-delete');
}
render(ctx) {
const target = ctx.target;
- const source = this.template(ctx);
+ const source = this._template(ctx);
const form = source.querySelector('form');
diff --git a/client/js/views/user_edit_view.js b/client/js/views/user_edit_view.js
index 0f9d858d..ac2ad203 100644
--- a/client/js/views/user_edit_view.js
+++ b/client/js/views/user_edit_view.js
@@ -2,12 +2,11 @@
const config = require('../config.js');
const views = require('../util/views.js');
-const FileDropperControl = require('./file_dropper_control.js');
+const FileDropperControl = require('../controls/file_dropper_control.js');
class UserEditView {
constructor() {
- this.template = views.getTemplate('user-edit');
- this.fileDropperControl = new FileDropperControl();
+ this._template = views.getTemplate('user-edit');
}
render(ctx) {
@@ -15,7 +14,7 @@ class UserEditView {
ctx.passwordPattern = config.passwordRegex + /|^$/.source;
const target = ctx.target;
- const source = this.template(ctx);
+ const source = this._template(ctx);
const form = source.querySelector('form');
const avatarContentField = source.querySelector('#avatar-content');
@@ -23,15 +22,18 @@ class UserEditView {
views.decorateValidator(form);
let avatarContent = null;
- this.fileDropperControl.render({
- target: avatarContentField,
- lock: true,
- resolve: files => {
- source.querySelector(
- '[name=avatar-style][value=manual]').checked = true;
- avatarContent = files[0];
- },
- });
+ if (avatarContentField) {
+ new FileDropperControl(
+ avatarContentField,
+ {
+ lock: true,
+ resolve: files => {
+ source.querySelector(
+ '[name=avatar-style][value=manual]').checked = true;
+ avatarContent = files[0];
+ },
+ });
+ }
form.addEventListener('submit', e => {
const rankField = source.querySelector('#user-rank');
diff --git a/client/js/views/user_summary_view.js b/client/js/views/user_summary_view.js
index c740cce7..a5109c82 100644
--- a/client/js/views/user_summary_view.js
+++ b/client/js/views/user_summary_view.js
@@ -4,12 +4,12 @@ const views = require('../util/views.js');
class UserSummaryView {
constructor() {
- this.template = views.getTemplate('user-summary');
+ this._template = views.getTemplate('user-summary');
}
render(ctx) {
const target = ctx.target;
- const source = this.template(ctx);
+ const source = this._template(ctx);
views.listenToMessages(source);
views.showView(target, source);
}
diff --git a/client/js/views/user_view.js b/client/js/views/user_view.js
index aac2e089..da94ad42 100644
--- a/client/js/views/user_view.js
+++ b/client/js/views/user_view.js
@@ -7,15 +7,15 @@ 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();
+ this._template = views.getTemplate('user');
+ this._deleteView = new UserDeleteView();
+ this._summaryView = new UserSummaryView();
+ this._editView = new UserEditView();
}
render(ctx) {
const target = document.getElementById('content-holder');
- const source = this.template(ctx);
+ const source = this._template(ctx);
ctx.section = ctx.section || 'summary';
@@ -29,11 +29,11 @@ class UserView {
let view = null;
if (ctx.section == 'edit') {
- view = this.editView;
+ view = this._editView;
} else if (ctx.section == 'delete') {
- view = this.deleteView;
+ view = this._deleteView;
} else {
- view = this.summaryView;
+ view = this._summaryView;
}
ctx.target = source.querySelector('#user-content-holder');
view.render(ctx);
diff --git a/client/js/views/users_header_view.js b/client/js/views/users_header_view.js
index d2f4e6cf..1f60e971 100644
--- a/client/js/views/users_header_view.js
+++ b/client/js/views/users_header_view.js
@@ -7,12 +7,12 @@ const views = require('../util/views.js');
class UsersHeaderView {
constructor() {
- this.template = views.getTemplate('users-header');
+ this._template = views.getTemplate('users-header');
}
render(ctx) {
const target = ctx.target;
- const source = this.template(ctx);
+ const source = this._template(ctx);
const form = source.querySelector('form');
diff --git a/client/js/views/users_page_view.js b/client/js/views/users_page_view.js
index 1e609aff..c7230f5f 100644
--- a/client/js/views/users_page_view.js
+++ b/client/js/views/users_page_view.js
@@ -4,12 +4,12 @@ const views = require('../util/views.js');
class UsersPageView {
constructor() {
- this.template = views.getTemplate('users-page');
+ this._template = views.getTemplate('users-page');
}
render(ctx) {
const target = ctx.target;
- const source = this.template(ctx);
+ const source = this._template(ctx);
views.showView(target, source);
}
}