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