client/general: refactor all the things

- Move controls to the "controls/" directory
- Make controls interface look similar to each other
- Prefix "private" methods and attributes with underscore
This commit is contained in:
rr- 2016-05-20 21:35:12 +02:00
parent c88dfd228a
commit 69fe8ec31a
41 changed files with 633 additions and 617 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(
'<div class="autocomplete"><ul></ul></div>');
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;

View file

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

View file

@ -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('<div class="tag-input"></div>');
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) {

View file

@ -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(
'<div class="autocomplete"><ul></ul></div>');
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;

View file

@ -4,7 +4,7 @@ const views = require('../util/views.js');
class EmptyView {
constructor() {
this.template = () => {
this._template = () => {
return views.htmlToDom(
'<div class="wrapper"><div class="messages"></div></div>');
};
@ -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);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 {
'<span class="access-key" data-accesskey="$1">$1</span>');
}
views.showView(this.navHolder, source);
views.showView(this._navHolder, source);
}
activate(itemName) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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