client+server: implement code autoformatting using prettier and black

This commit is contained in:
Shyam Sunder 2020-06-05 18:03:37 -04:00
parent c06aaa63af
commit 57193b5715
312 changed files with 15512 additions and 12825 deletions

View file

@ -1,50 +1,91 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: stable
hooks:
- id: black
files: server/szurubooru/
language_version: python3.8
- repo: https://github.com/timothycrosley/isort
rev: '4.3.21-2'
hooks:
- id: isort
files: server/szurubooru/
exclude: server/szurubooru/migrations/env.py
additional_dependencies:
- toml
- repo: https://github.com/prettier/prettier
rev: '2.0.5'
hooks:
- id: prettier
files: client/js/
exclude: client/js/.gitignore
args: ['--config', 'client/.prettierrc.yml']
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v7.1.0
hooks:
- id: eslint
files: client/js/
args: ['--fix']
additional_dependencies:
- eslint-config-prettier
- repo: https://gitlab.com/pycqa/flake8
rev: '3.8.2'
hooks:
- id: flake8
files: server/szurubooru/
additional_dependencies:
- flake8-print
args: ['--config=server/setup.cfg']
- flake8-print
args: ['--config=server/.flake8']
- repo: local
hooks:
- id: pytest
name: pytest
entry: >-
bash -c
'docker build -f server/Dockerfile.test -t $(git rev-parse --short HEAD)-test server/
&& docker run --rm -t $(git rev-parse --short HEAD)-test szurubooru/
&& docker rmi --no-prune $(git rev-parse --short HEAD)-test'
language: system
types: [python]
files: server/szurubooru/
pass_filenames: false
- id: docker-build-client
name: Test building the client in Docker
entry: bash -c 'docker build -t szurubooru-client:$(git rev-parse --short HEAD) client/'
name: Docker - build client
entry: bash -c 'docker build client/'
language: system
types: [file]
files: client/
pass_filenames: false
- id: docker-build-server
name: Test building the server in Docker
entry: bash -c 'docker build -t szurubooru-server:$(git rev-parse --short HEAD) server/'
name: Docker - build server
entry: bash -c 'docker build server/'
language: system
types: [file]
files: server/
pass_filenames: false
- id: pytest
name: pytest
entry: bash -c 'docker run --rm -t $(docker build -f server/Dockerfile.test -q server/) szurubooru/'
language: system
types: [python]
files: server/szurubooru/
exclude: server/szurubooru/migrations/
pass_filenames: false
- id: pytest-cov
name: pytest
entry: bash -c 'docker run --rm -t $(docker build -f server/Dockerfile.test -q server/) --cov-report=term-missing:skip-covered --cov=szurubooru szurubooru/'
language: system
types: [python]
files: server/szurubooru/
exclude: server/szurubooru/migrations/
pass_filenames: false
verbose: true
stages: [manual]
fail_fast: true
exclude: LICENSE.md

View file

@ -2,7 +2,7 @@ env:
browser: true
commonjs: true
es6: true
extends: 'eslint:recommended'
extends: 'prettier'
globals:
Atomics: readonly
SharedArrayBuffer: readonly
@ -10,284 +10,3 @@ ignorePatterns:
- build.js
parserOptions:
ecmaVersion: 11
rules:
accessor-pairs: error
array-bracket-newline: error
array-bracket-spacing:
- error
- never
array-callback-return: error
array-element-newline: 'off'
arrow-body-style: 'off'
arrow-parens:
- error
- as-needed
arrow-spacing:
- error
- after: true
before: true
block-scoped-var: error
block-spacing: error
brace-style:
- error
- 1tbs
callback-return: 'off'
camelcase: error
class-methods-use-this: 'off'
comma-dangle: 'off'
comma-spacing:
- error
- after: true
before: false
comma-style:
- error
- last
complexity: 'off'
computed-property-spacing:
- error
- never
consistent-return: 'off'
consistent-this: 'off'
curly: error
default-case: error
default-case-last: error
default-param-last: error
dot-location:
- error
- property
dot-notation:
- error
- allowKeywords: true
eol-last: error
eqeqeq: error
func-call-spacing: error
func-name-matching: error
func-names: error
func-style:
- error
- declaration
- allowArrowFunctions: true
function-call-argument-newline:
- error
- consistent
function-paren-newline: 'off'
generator-star-spacing: error
global-require: 'off'
grouped-accessor-pairs: 'off'
guard-for-in: error
handle-callback-err: error
id-blacklist: error
id-length: 'off'
id-match: error
implicit-arrow-linebreak:
- error
- beside
indent:
- error
- 4
indent-legacy: 'off'
init-declarations: error
jsx-quotes: error
key-spacing: error
keyword-spacing:
- error
- after: true
before: true
line-comment-position: 'off'
linebreak-style:
- error
- unix
lines-around-comment: error
lines-around-directive: error
lines-between-class-members:
- error
- always
max-classes-per-file: 'off'
max-depth: error
max-len: 'off'
max-lines: 'off'
max-lines-per-function: 'off'
max-nested-callbacks: error
max-params: 'off'
max-statements: 'off'
max-statements-per-line: error
multiline-comment-style:
- error
- separate-lines
multiline-ternary: 'off'
new-cap: error
new-parens: error
newline-after-var: 'off'
newline-before-return: 'off'
newline-per-chained-call: 'off'
no-alert: 'off'
no-array-constructor: error
no-await-in-loop: error
no-bitwise: 'off'
no-buffer-constructor: 'off'
no-caller: error
no-catch-shadow: error
no-confusing-arrow: error
no-console: error
no-constructor-return: error
no-continue: 'off'
no-div-regex: 'off'
no-duplicate-imports: error
no-else-return: 'off'
no-empty-function: 'off'
no-eq-null: error
no-eval: error
no-extend-native: error
no-extra-bind: error
no-extra-label: error
no-extra-parens: 'off'
no-floating-decimal: error
no-implicit-globals: error
no-implied-eval: error
no-inline-comments: 'off'
no-invalid-this: error
no-iterator: error
no-label-var: error
no-labels: error
no-lone-blocks: error
no-lonely-if: error
no-loop-func: 'off'
no-loss-of-precision: error
no-magic-numbers: 'off'
no-mixed-operators: error
no-mixed-requires: error
no-multi-assign: error
no-multi-spaces:
- error
- ignoreEOLComments: true
no-multi-str: error
no-multiple-empty-lines: error
no-native-reassign: error
no-negated-condition: 'off'
no-negated-in-lhs: error
no-nested-ternary: error
no-new: 'off'
no-new-func: error
no-new-object: error
no-new-require: error
no-new-wrappers: error
no-octal-escape: error
no-param-reassign: 'off'
no-path-concat: error
no-plusplus: 'off'
no-process-env: error
no-process-exit: error
no-proto: error
no-restricted-exports: error
no-restricted-globals: error
no-restricted-imports: error
no-restricted-modules: error
no-restricted-properties: error
no-restricted-syntax: error
no-return-assign: error
no-return-await: error
no-script-url: error
no-self-compare: error
no-sequences: error
no-shadow: 'off'
no-spaced-func: error
no-sync: error
no-tabs: error
no-template-curly-in-string: error
no-ternary: 'off'
no-throw-literal: 'off'
no-trailing-spaces: error
no-undef-init: error
no-undefined: 'off'
no-underscore-dangle: 'off'
no-unmodified-loop-condition: error
no-unneeded-ternary: error
no-unused-expressions: error
no-unused-vars: 'off'
no-use-before-define: 'off'
no-useless-backreference: error
no-useless-call: error
no-useless-computed-key: error
no-useless-concat: error
no-useless-constructor: error
no-useless-escape: 'off'
no-useless-rename: error
no-useless-return: error
no-var: 'off'
no-void: error
no-warning-comments: warn
no-whitespace-before-property: error
nonblock-statement-body-position: error
object-curly-newline: error
object-curly-spacing:
- error
- never
object-shorthand: 'off'
one-var: 'off'
one-var-declaration-per-line: error
operator-assignment:
- error
- always
operator-linebreak: 'off'
padded-blocks: 'off'
padding-line-between-statements: error
prefer-arrow-callback: error
prefer-const: 'off'
prefer-destructuring: 'off'
prefer-exponentiation-operator: 'off'
prefer-named-capture-group: 'off'
prefer-numeric-literals: error
prefer-object-spread: 'off'
prefer-promise-reject-errors: 'off'
prefer-reflect: 'off'
prefer-regex-literals: warn
prefer-rest-params: 'off'
prefer-spread: 'off'
prefer-template: 'off'
quote-props: 'off'
quotes: 'off'
radix:
- error
- as-needed
require-atomic-updates: error
require-await: error
require-jsdoc: 'off'
require-unicode-regexp: 'off'
rest-spread-spacing: error
semi: 'off'
semi-spacing:
- error
- after: true
before: false
semi-style:
- error
- last
sort-imports: error
sort-keys: 'off'
sort-vars: error
space-before-blocks: error
space-before-function-paren: 'off'
space-in-parens:
- error
- never
space-infix-ops: error
space-unary-ops: error
spaced-comment:
- error
- always
strict: error
switch-colon-spacing: error
symbol-description: error
template-curly-spacing:
- error
- never
template-tag-spacing: error
unicode-bom:
- error
- never
valid-jsdoc: error
vars-on-top: error
wrap-iife: error
wrap-regex: error
yield-star-spacing: error
yoda: 'off'

4
client/.prettierrc.yml Normal file
View file

@ -0,0 +1,4 @@
parser: babel
printWidth: 79
tabWidth: 4
quoteProps: consistent

View file

@ -1,10 +1,10 @@
'use strict';
"use strict";
const cookies = require('js-cookie');
const request = require('superagent');
const events = require('./events.js');
const progress = require('./util/progress.js');
const uri = require('./util/uri.js');
const cookies = require("js-cookie");
const request = require("superagent");
const events = require("./events.js");
const progress = require("./util/progress.js");
const uri = require("./util/uri.js");
let fileTokens = {};
let remoteConfig = null;
@ -18,22 +18,22 @@ class Api extends events.EventTarget {
this.token = null;
this.cache = {};
this.allRanks = [
'anonymous',
'restricted',
'regular',
'power',
'moderator',
'administrator',
'nobody',
"anonymous",
"restricted",
"regular",
"power",
"moderator",
"administrator",
"nobody",
];
this.rankNames = new Map([
['anonymous', 'Anonymous'],
['restricted', 'Restricted user'],
['regular', 'Regular user'],
['power', 'Power user'],
['moderator', 'Moderator'],
['administrator', 'Administrator'],
['nobody', 'Nobody'],
["anonymous", "Anonymous"],
["restricted", "Restricted user"],
["regular", "Regular user"],
["power", "Power user"],
["moderator", "Moderator"],
["administrator", "Administrator"],
["nobody", "Nobody"],
]);
}
@ -43,11 +43,12 @@ class Api extends events.EventTarget {
resolve(this.cache[url]);
});
}
return this._wrappedRequest(url, request.get, {}, {}, options)
.then(response => {
return this._wrappedRequest(url, request.get, {}, {}, options).then(
(response) => {
this.cache[url] = response;
return Promise.resolve(response);
});
}
);
}
post(url, data, files, options) {
@ -67,10 +68,9 @@ class Api extends events.EventTarget {
fetchConfig() {
if (remoteConfig === null) {
return this.get(uri.formatApiLink('info'))
.then(response => {
remoteConfig = response.config;
});
return this.get(uri.formatApiLink("info")).then((response) => {
remoteConfig = response.config;
});
} else {
return Promise.resolve();
}
@ -115,7 +115,8 @@ class Api extends events.EventTarget {
continue;
}
const rankIndex = this.allRanks.indexOf(
remoteConfig.privileges[p]);
remoteConfig.privileges[p]
);
if (minViableRank === null || rankIndex < minViableRank) {
minViableRank = rankIndex;
}
@ -123,17 +124,16 @@ class Api extends events.EventTarget {
if (minViableRank === null) {
throw `Bad privilege name: ${lookup}`;
}
let myRank = this.user !== null ?
this.allRanks.indexOf(this.user.rank) :
0;
let myRank =
this.user !== null ? this.allRanks.indexOf(this.user.rank) : 0;
return myRank >= minViableRank;
}
loginFromCookies() {
const auth = cookies.getJSON('auth');
return auth && auth.user && auth.token ?
this.loginWithToken(auth.user, auth.token, true) :
Promise.resolve();
const auth = cookies.getJSON("auth");
return auth && auth.user && auth.token
? this.loginWithToken(auth.user, auth.token, true)
: Promise.resolve();
}
loginWithToken(userName, token, doRemember) {
@ -141,63 +141,74 @@ class Api extends events.EventTarget {
return new Promise((resolve, reject) => {
this.userName = userName;
this.token = token;
this.get('/user/' + userName + '?bump-login=true')
.then(response => {
this.get("/user/" + userName + "?bump-login=true").then(
(response) => {
const options = {};
if (doRemember) {
options.expires = 365;
}
cookies.set(
'auth',
{'user': userName, 'token': token},
options);
"auth",
{ user: userName, token: token },
options
);
this.user = response;
resolve();
this.dispatchEvent(new CustomEvent('login'));
}, error => {
this.dispatchEvent(new CustomEvent("login"));
},
(error) => {
reject(error);
this.logout();
});
}
);
});
}
createToken(userName, options) {
let userTokenRequest = {
enabled: true,
note: 'Web Login Token'
note: "Web Login Token",
};
if (typeof options.expires !== 'undefined') {
userTokenRequest.expirationTime = new Date().addDays(options.expires).toISOString()
if (typeof options.expires !== "undefined") {
userTokenRequest.expirationTime = new Date()
.addDays(options.expires)
.toISOString();
}
return new Promise((resolve, reject) => {
this.post('/user-token/' + userName, userTokenRequest)
.then(response => {
this.post("/user-token/" + userName, userTokenRequest).then(
(response) => {
cookies.set(
'auth',
{'user': userName, 'token': response.token},
options);
"auth",
{ user: userName, token: response.token },
options
);
this.userName = userName;
this.token = response.token;
this.userPassword = null;
}, error => {
},
(error) => {
reject(error);
});
}
);
});
}
deleteToken(userName, userToken) {
return new Promise((resolve, reject) => {
this.delete('/user-token/' + userName + '/' + userToken, {})
.then(response => {
this.delete("/user-token/" + userName + "/" + userToken, {}).then(
(response) => {
const options = {};
cookies.set(
'auth',
{'user': userName, 'token': null},
options);
"auth",
{ user: userName, token: null },
options
);
resolve();
}, error => {
},
(error) => {
reject(error);
});
}
);
});
}
@ -206,8 +217,8 @@ class Api extends events.EventTarget {
return new Promise((resolve, reject) => {
this.userName = userName;
this.userPassword = userPassword;
this.get('/user/' + userName + '?bump-login=true')
.then(response => {
this.get("/user/" + userName + "?bump-login=true").then(
(response) => {
const options = {};
if (doRemember) {
options.expires = 365;
@ -215,22 +226,26 @@ class Api extends events.EventTarget {
this.createToken(this.userName, options);
this.user = response;
resolve();
this.dispatchEvent(new CustomEvent('login'));
}, error => {
this.dispatchEvent(new CustomEvent("login"));
},
(error) => {
reject(error);
this.logout();
});
}
);
});
}
logout() {
let self = this;
this.deleteToken(this.userName, this.token)
.then(response => {
this.deleteToken(this.userName, this.token).then(
(response) => {
self._logout();
}, error => {
},
(error) => {
self._logout();
});
}
);
}
_logout() {
@ -238,17 +253,19 @@ class Api extends events.EventTarget {
this.userName = null;
this.userPassword = null;
this.token = null;
this.dispatchEvent(new CustomEvent('logout'));
this.dispatchEvent(new CustomEvent("logout"));
}
forget() {
cookies.remove('auth');
cookies.remove("auth");
}
isLoggedIn(user) {
if (user) {
return this.userName !== null &&
this.userName.toLowerCase() === user.name.toLowerCase();
return (
this.userName !== null &&
this.userName.toLowerCase() === user.name.toLowerCase()
);
} else {
return this.userName !== null;
}
@ -259,8 +276,7 @@ class Api extends events.EventTarget {
}
_getFullUrl(url) {
const fullUrl =
('api/' + url).replace(/([^:])\/+/g, '$1/');
const fullUrl = ("api/" + url).replace(/([^:])\/+/g, "$1/");
const matches = fullUrl.match(/^([^?]*)\??(.*)$/);
const baseUrl = matches[1];
const request = matches[2];
@ -285,7 +301,7 @@ class Api extends events.EventTarget {
const file = files[key];
const fileId = this._getFileId(file);
if (fileTokens[fileId]) {
data[key + 'Token'] = fileTokens[fileId];
data[key + "Token"] = fileTokens[fileId];
} else {
promise = promise
.then(() => {
@ -293,33 +309,40 @@ class Api extends events.EventTarget {
abortFunction = () => uploadPromise.abort();
return uploadPromise;
})
.then(token => {
.then((token) => {
abortFunction = () => {};
fileTokens[fileId] = token;
data[key + 'Token'] = token;
data[key + "Token"] = token;
return Promise.resolve();
});
}
}
}
promise = promise.then(
() => {
promise = promise
.then(() => {
let requestPromise = this._rawRequest(
url, requestFactory, data, {}, options);
url,
requestFactory,
data,
{},
options
);
abortFunction = () => requestPromise.abort();
return requestPromise;
})
.catch(error => {
if (error.response && error.response.name ===
'MissingOrExpiredRequiredFileError') {
.catch((error) => {
if (
error.response &&
error.response.name === "MissingOrExpiredRequiredFileError"
) {
for (let key of Object.keys(files)) {
const file = files[key];
const fileId = this._getFileId(file);
fileTokens[fileId] = null;
}
error.message =
'The uploaded file has expired; ' +
'please resend the form to reupload.';
"The uploaded file has expired; " +
"please resend the form to reupload.";
}
return Promise.reject(error);
});
@ -331,13 +354,17 @@ class Api extends events.EventTarget {
let abortFunction = () => {};
let returnedPromise = new Promise((resolve, reject) => {
let uploadPromise = this._rawRequest(
'uploads', request.post, {}, {content: file}, options);
"uploads",
request.post,
{},
{ content: file },
options
);
abortFunction = () => uploadPromise.abort();
return uploadPromise.then(
response => {
abortFunction = () => {};
return resolve(response.token);
}, reject);
return uploadPromise.then((response) => {
abortFunction = () => {};
return resolve(response.token);
}, reject);
});
returnedPromise.abort = () => abortFunction();
return returnedPromise;
@ -352,7 +379,7 @@ class Api extends events.EventTarget {
let returnedPromise = new Promise((resolve, reject) => {
let req = requestFactory(fullUrl);
req.set('Accept', 'application/json');
req.set("Accept", "application/json");
if (query) {
req.query(query);
@ -362,7 +389,7 @@ class Api extends events.EventTarget {
for (let key of Object.keys(files)) {
const value = files[key];
if (value.constructor === String) {
data[key + 'Url'] = value;
data[key + "Url"] = value;
} else {
req.attach(key, value || new Blob());
}
@ -371,9 +398,9 @@ class Api extends events.EventTarget {
if (data) {
if (files && Object.keys(files).length) {
req.attach('metadata', new Blob([JSON.stringify(data)]));
req.attach("metadata", new Blob([JSON.stringify(data)]));
} else {
req.set('Content-Type', 'application/json');
req.set("Content-Type", "application/json");
req.send(data);
}
}
@ -382,19 +409,28 @@ class Api extends events.EventTarget {
if (this.userName && this.token) {
req.auth = null;
// eslint-disable-next-line no-undef
req.set('Authorization', 'Token ' + new Buffer(
this.userName + ":" + this.token).toString('base64'))
req.set(
"Authorization",
"Token " +
new Buffer(
this.userName + ":" + this.token
).toString("base64")
);
} else if (this.userName && this.userPassword) {
req.auth(
this.userName,
encodeURIComponent(this.userPassword)
.replace(/%([0-9A-F]{2})/g, (match, p1) => {
return String.fromCharCode('0x' + p1);
}));
encodeURIComponent(this.userPassword).replace(
/%([0-9A-F]{2})/g,
(match, p1) => {
return String.fromCharCode("0x" + p1);
}
)
);
}
} catch (e) {
reject(
new Error('Authentication error (malformed credentials)'));
new Error("Authentication error (malformed credentials)")
);
}
if (!options.noProgress) {
@ -402,10 +438,11 @@ class Api extends events.EventTarget {
}
abortFunction = () => {
req.abort(); // does *NOT* call the callback passed in .end()
req.abort(); // does *NOT* call the callback passed in .end()
progress.done();
reject(
new Error('The request was aborted due to user cancel.'));
new Error("The request was aborted due to user cancel.")
);
};
req.end((error, response) => {
@ -414,7 +451,8 @@ class Api extends events.EventTarget {
if (error) {
if (response && response.body) {
error = new Error(
response.body.description || 'Unknown error');
response.body.description || "Unknown error"
);
error.response = response.body;
}
reject(error);

View file

@ -1,4 +1,4 @@
'use strict';
"use strict";
const config = require('./.config.autogen.json');
const config = require("./.config.autogen.json");
module.exports = config;

View file

@ -1,38 +1,40 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const tags = require('../tags.js');
const pools = require('../pools.js');
const uri = require('../util/uri.js');
const topNavigation = require('../models/top_navigation.js');
const LoginView = require('../views/login_view.js');
const router = require("../router.js");
const api = require("../api.js");
const tags = require("../tags.js");
const pools = require("../pools.js");
const uri = require("../util/uri.js");
const topNavigation = require("../models/top_navigation.js");
const LoginView = require("../views/login_view.js");
class LoginController {
constructor() {
api.forget();
topNavigation.activate('login');
topNavigation.setTitle('Login');
topNavigation.activate("login");
topNavigation.setTitle("Login");
this._loginView = new LoginView();
this._loginView.addEventListener('submit', e => this._evtLogin(e));
this._loginView.addEventListener("submit", (e) => this._evtLogin(e));
}
_evtLogin(e) {
this._loginView.clearMessages();
this._loginView.disableForm();
api.forget();
api.login(e.detail.name, e.detail.password, e.detail.remember)
.then(() => {
api.login(e.detail.name, e.detail.password, e.detail.remember).then(
() => {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('Logged in');
ctx.controller.showSuccess("Logged in");
// reload tag category color map, this is required when `tag_categories:list` has a permission other than anonymous
tags.refreshCategoryColorMap();
pools.refreshCategoryColorMap();
}, error => {
},
(error) => {
this._loginView.showError(error.message);
this._loginView.enableForm();
});
}
);
}
}
@ -41,15 +43,15 @@ class LogoutController {
api.forget();
api.logout();
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('Logged out');
ctx.controller.showSuccess("Logged out");
}
}
module.exports = router => {
router.enter(['login'], (ctx, next) => {
module.exports = (router) => {
router.enter(["login"], (ctx, next) => {
ctx.controller = new LoginController();
});
router.enter(['logout'], (ctx, next) => {
router.enter(["logout"], (ctx, next) => {
ctx.controller = new LogoutController();
});
};

View file

@ -1,19 +1,19 @@
'use strict';
"use strict";
const api = require('../api.js');
const topNavigation = require('../models/top_navigation.js');
const EmptyView = require('../views/empty_view.js');
const api = require("../api.js");
const topNavigation = require("../models/top_navigation.js");
const EmptyView = require("../views/empty_view.js");
class BasePostController {
constructor(ctx) {
if (!api.hasPrivilege('posts:view')) {
if (!api.hasPrivilege("posts:view")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view posts.');
this._view.showError("You don't have privileges to view posts.");
return;
}
topNavigation.activate('posts');
topNavigation.setTitle('Post #' + ctx.parameters.id.toString());
topNavigation.activate("posts");
topNavigation.setTitle("Post #" + ctx.parameters.id.toString());
}
}

View file

@ -1,51 +1,55 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const PostList = require('../models/post_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const CommentsPageView = require('../views/comments_page_view.js');
const EmptyView = require('../views/empty_view.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const PostList = require("../models/post_list.js");
const topNavigation = require("../models/top_navigation.js");
const PageController = require("../controllers/page_controller.js");
const CommentsPageView = require("../views/comments_page_view.js");
const EmptyView = require("../views/empty_view.js");
const fields = ['id', 'comments', 'commentCount', 'thumbnailUrl'];
const fields = ["id", "comments", "commentCount", "thumbnailUrl"];
class CommentsController {
constructor(ctx) {
if (!api.hasPrivilege('comments:list')) {
if (!api.hasPrivilege("comments:list")) {
this._view = new EmptyView();
this._view.showError(
'You don\'t have privileges to view comments.');
"You don't have privileges to view comments."
);
return;
}
topNavigation.activate('comments');
topNavigation.setTitle('Listing comments');
topNavigation.activate("comments");
topNavigation.setTitle("Listing comments");
this._pageController = new PageController();
this._pageController.run({
parameters: ctx.parameters,
defaultLimit: 10,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign(
{}, ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('comments', parameters);
const parameters = Object.assign({}, ctx.parameters, {
offset: offset,
limit: limit,
});
return uri.formatClientLink("comments", parameters);
},
requestPage: (offset, limit) => {
return PostList.search(
'sort:comment-date comment-count-min:1',
"sort:comment-date comment-count-min:1",
offset,
limit,
fields);
fields
);
},
pageRenderer: pageCtx => {
pageRenderer: (pageCtx) => {
Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege('posts:view'),
canViewPosts: api.hasPrivilege("posts:view"),
});
const view = new CommentsPageView(pageCtx);
view.addEventListener('submit', e => this._evtUpdate(e));
view.addEventListener('score', e => this._evtScore(e));
view.addEventListener('delete', e => this._evtDelete(e));
view.addEventListener("submit", (e) => this._evtUpdate(e));
view.addEventListener("score", (e) => this._evtScore(e));
view.addEventListener("delete", (e) => this._evtDelete(e));
return view;
},
});
@ -54,26 +58,27 @@ class CommentsController {
_evtUpdate(e) {
// TODO: disable form
e.detail.comment.text = e.detail.text;
e.detail.comment.save()
.catch(error => {
e.detail.target.showError(error.message);
// TODO: enable form
});
e.detail.comment.save().catch((error) => {
e.detail.target.showError(error.message);
// TODO: enable form
});
}
_evtScore(e) {
e.detail.comment.setScore(e.detail.score)
.catch(error => window.alert(error.message));
e.detail.comment
.setScore(e.detail.score)
.catch((error) => window.alert(error.message));
}
_evtDelete(e) {
e.detail.comment.delete()
.catch(error => window.alert(error.message));
e.detail.comment
.delete()
.catch((error) => window.alert(error.message));
}
}
module.exports = router => {
router.enter(['comments'], (ctx, next) => {
module.exports = (router) => {
router.enter(["comments"], (ctx, next) => {
new CommentsController(ctx);
});
}
};

View file

@ -1,24 +1,24 @@
'use strict';
"use strict";
const topNavigation = require('../models/top_navigation.js');
const HelpView = require('../views/help_view.js');
const topNavigation = require("../models/top_navigation.js");
const HelpView = require("../views/help_view.js");
class HelpController {
constructor(section, subsection) {
topNavigation.activate('help');
topNavigation.setTitle('Help');
topNavigation.activate("help");
topNavigation.setTitle("Help");
this._helpView = new HelpView(section, subsection);
}
}
module.exports = router => {
router.enter(['help'], (ctx, next) => {
module.exports = (router) => {
router.enter(["help"], (ctx, next) => {
new HelpController();
});
router.enter(['help', ':section'], (ctx, next) => {
router.enter(["help", ":section"], (ctx, next) => {
new HelpController(ctx.parameters.section);
});
router.enter(['help', ':section', ':subsection'], (ctx, next) => {
router.enter(["help", ":section", ":subsection"], (ctx, next) => {
new HelpController(ctx.parameters.section, ctx.parameters.subsection);
});
};

View file

@ -1,26 +1,26 @@
'use strict';
"use strict";
const api = require('../api.js');
const config = require('../config.js');
const Info = require('../models/info.js');
const topNavigation = require('../models/top_navigation.js');
const HomeView = require('../views/home_view.js');
const api = require("../api.js");
const config = require("../config.js");
const Info = require("../models/info.js");
const topNavigation = require("../models/top_navigation.js");
const HomeView = require("../views/home_view.js");
class HomeController {
constructor() {
topNavigation.activate('home');
topNavigation.setTitle('Home');
topNavigation.activate("home");
topNavigation.setTitle("Home");
this._homeView = new HomeView({
name: api.getName(),
version: config.meta.version,
buildDate: config.meta.buildDate,
canListSnapshots: api.hasPrivilege('snapshots:list'),
canListPosts: api.hasPrivilege('posts:list'),
canListSnapshots: api.hasPrivilege("snapshots:list"),
canListPosts: api.hasPrivilege("posts:list"),
});
Info.get()
.then(info => {
Info.get().then(
(info) => {
this._homeView.setStats({
diskUsage: info.diskUsage,
postCount: info.postCount,
@ -31,7 +31,8 @@ class HomeController {
featuringTime: info.featuringTime,
});
},
error => this._homeView.showError(error.message));
(error) => this._homeView.showError(error.message)
);
}
showSuccess(message) {
@ -43,8 +44,8 @@ class HomeController {
}
}
module.exports = router => {
module.exports = (router) => {
router.enter([], (ctx, next) => {
ctx.controller = new HomeController();
});
}
};

View file

@ -1,18 +1,18 @@
'use strict';
"use strict";
const topNavigation = require('../models/top_navigation.js');
const NotFoundView = require('../views/not_found_view.js');
const topNavigation = require("../models/top_navigation.js");
const NotFoundView = require("../views/not_found_view.js");
class NotFoundController {
constructor(path) {
topNavigation.activate('');
topNavigation.setTitle('Not found');
topNavigation.activate("");
topNavigation.setTitle("Not found");
this._notFoundView = new NotFoundView(path);
}
}
module.exports = router => {
module.exports = (router) => {
router.enter(null, (ctx, next) => {
ctx.controller = new NotFoundController(ctx.canonicalPath);
});
}
};

View file

@ -1,8 +1,8 @@
'use strict';
"use strict";
const settings = require('../models/settings.js');
const EndlessPageView = require('../views/endless_page_view.js');
const ManualPageView = require('../views/manual_page_view.js');
const settings = require("../models/settings.js");
const EndlessPageView = require("../views/endless_page_view.js");
const ManualPageView = require("../views/manual_page_view.js");
class PageController {
constructor(ctx) {

View file

@ -1,19 +1,20 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const topNavigation = require('../models/top_navigation.js');
const PasswordResetView = require('../views/password_reset_view.js');
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const topNavigation = require("../models/top_navigation.js");
const PasswordResetView = require("../views/password_reset_view.js");
class PasswordResetController {
constructor() {
topNavigation.activate('login');
topNavigation.setTitle('Password reminder');
topNavigation.activate("login");
topNavigation.setTitle("Password reminder");
this._passwordResetView = new PasswordResetView();
this._passwordResetView.addEventListener(
'submit', e => this._evtReset(e));
this._passwordResetView.addEventListener("submit", (e) =>
this._evtReset(e)
);
}
_evtReset(e) {
@ -21,15 +22,20 @@ class PasswordResetController {
this._passwordResetView.disableForm();
api.forget();
api.logout();
api.get(uri.formatApiLink('password-reset', e.detail.userNameOrEmail))
.then(() => {
api.get(
uri.formatApiLink("password-reset", e.detail.userNameOrEmail)
).then(
() => {
this._passwordResetView.showSuccess(
'E-mail has been sent. To finish the procedure, ' +
'please click the link it contains.');
}, error => {
"E-mail has been sent. To finish the procedure, " +
"please click the link it contains."
);
},
(error) => {
this._passwordResetView.showError(error.message);
this._passwordResetView.enableForm();
});
}
);
}
}
@ -38,26 +44,30 @@ class PasswordResetFinishController {
api.forget();
api.logout();
let password = null;
api.post(uri.formatApiLink('password-reset', name), {token: token})
.then(response => {
api.post(uri.formatApiLink("password-reset", name), { token: token })
.then((response) => {
password = response.password;
return api.login(name, password, false);
}).then(() => {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('New password: ' + password);
}, error => {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showError(error.message);
});
})
.then(
() => {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess("New password: " + password);
},
(error) => {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showError(error.message);
}
);
}
}
module.exports = router => {
router.enter(['password-reset'], (ctx, next) => {
module.exports = (router) => {
router.enter(["password-reset"], (ctx, next) => {
ctx.controller = new PasswordResetController();
});
router.enter(['password-reset', ':descriptor'], (ctx, next) => {
const [name, token] = ctx.parameters.descriptor.split(':', 2);
router.enter(["password-reset", ":descriptor"], (ctx, next) => {
const [name, token] = ctx.parameters.descriptor.split(":", 2);
ctx.controller = new PasswordResetFinishController(name, token);
});
};

View file

@ -1,57 +1,69 @@
'use strict';
"use strict";
const api = require('../api.js');
const pools = require('../pools.js');
const PoolCategoryList = require('../models/pool_category_list.js');
const topNavigation = require('../models/top_navigation.js');
const PoolCategoriesView = require('../views/pool_categories_view.js');
const EmptyView = require('../views/empty_view.js');
const api = require("../api.js");
const pools = require("../pools.js");
const PoolCategoryList = require("../models/pool_category_list.js");
const topNavigation = require("../models/top_navigation.js");
const PoolCategoriesView = require("../views/pool_categories_view.js");
const EmptyView = require("../views/empty_view.js");
class PoolCategoriesController {
constructor() {
if (!api.hasPrivilege('poolCategories:list')) {
if (!api.hasPrivilege("poolCategories:list")) {
this._view = new EmptyView();
this._view.showError(
'You don\'t have privileges to view pool categories.');
"You don't have privileges to view pool categories."
);
return;
}
topNavigation.activate('pools');
topNavigation.setTitle('Listing pools');
PoolCategoryList.get().then(response => {
this._poolCategories = response.results;
this._view = new PoolCategoriesView({
poolCategories: this._poolCategories,
canEditName: api.hasPrivilege('poolCategories:edit:name'),
canEditColor: api.hasPrivilege('poolCategories:edit:color'),
canDelete: api.hasPrivilege('poolCategories:delete'),
canCreate: api.hasPrivilege('poolCategories:create'),
canSetDefault: api.hasPrivilege('poolCategories:setDefault'),
});
this._view.addEventListener('submit', e => this._evtSubmit(e));
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
topNavigation.activate("pools");
topNavigation.setTitle("Listing pools");
PoolCategoryList.get().then(
(response) => {
this._poolCategories = response.results;
this._view = new PoolCategoriesView({
poolCategories: this._poolCategories,
canEditName: api.hasPrivilege("poolCategories:edit:name"),
canEditColor: api.hasPrivilege(
"poolCategories:edit:color"
),
canDelete: api.hasPrivilege("poolCategories:delete"),
canCreate: api.hasPrivilege("poolCategories:create"),
canSetDefault: api.hasPrivilege(
"poolCategories:setDefault"
),
});
this._view.addEventListener("submit", (e) =>
this._evtSubmit(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
}
_evtSubmit(e) {
this._view.clearMessages();
this._view.disableForm();
this._poolCategories.save()
.then(() => {
this._poolCategories.save().then(
() => {
pools.refreshCategoryColorMap();
this._view.enableForm();
this._view.showSuccess('Changes saved.');
}, error => {
this._view.showSuccess("Changes saved.");
},
(error) => {
this._view.enableForm();
this._view.showError(error.message);
});
}
);
}
}
module.exports = router => {
router.enter(['pool-categories'], (ctx, next) => {
module.exports = (router) => {
router.enter(["pool-categories"], (ctx, next) => {
ctx.controller = new PoolCategoriesController(ctx, next);
});
};

View file

@ -1,63 +1,76 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const Pool = require('../models/pool.js');
const Post = require('../models/post.js');
const PoolCategoryList = require('../models/pool_category_list.js');
const topNavigation = require('../models/top_navigation.js');
const PoolView = require('../views/pool_view.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const Pool = require("../models/pool.js");
const Post = require("../models/post.js");
const PoolCategoryList = require("../models/pool_category_list.js");
const topNavigation = require("../models/top_navigation.js");
const PoolView = require("../views/pool_view.js");
const EmptyView = require("../views/empty_view.js");
class PoolController {
constructor(ctx, section) {
if (!api.hasPrivilege('pools:view')) {
if (!api.hasPrivilege("pools:view")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view pools.');
this._view.showError("You don't have privileges to view pools.");
return;
}
Promise.all([
PoolCategoryList.get(),
Pool.get(ctx.parameters.id)
]).then(responses => {
const [poolCategoriesResponse, pool] = responses;
Pool.get(ctx.parameters.id),
]).then(
(responses) => {
const [poolCategoriesResponse, pool] = responses;
topNavigation.activate('pools');
topNavigation.setTitle('Pool #' + pool.names[0]);
topNavigation.activate("pools");
topNavigation.setTitle("Pool #" + pool.names[0]);
this._name = ctx.parameters.name;
pool.addEventListener('change', e => this._evtSaved(e, section));
this._name = ctx.parameters.name;
pool.addEventListener("change", (e) =>
this._evtSaved(e, section)
);
const categories = {};
for (let category of poolCategoriesResponse.results) {
categories[category.name] = category.name;
const categories = {};
for (let category of poolCategoriesResponse.results) {
categories[category.name] = category.name;
}
this._view = new PoolView({
pool: pool,
section: section,
canEditAnything: api.hasPrivilege("pools:edit"),
canEditNames: api.hasPrivilege("pools:edit:names"),
canEditCategory: api.hasPrivilege("pools:edit:category"),
canEditDescription: api.hasPrivilege(
"pools:edit:description"
),
canEditPosts: api.hasPrivilege("pools:edit:posts"),
canMerge: api.hasPrivilege("pools:merge"),
canDelete: api.hasPrivilege("pools:delete"),
categories: categories,
escapeColons: uri.escapeColons,
});
this._view.addEventListener("change", (e) =>
this._evtChange(e)
);
this._view.addEventListener("submit", (e) =>
this._evtUpdate(e)
);
this._view.addEventListener("merge", (e) => this._evtMerge(e));
this._view.addEventListener("delete", (e) =>
this._evtDelete(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
this._view = new PoolView({
pool: pool,
section: section,
canEditAnything: api.hasPrivilege('pools:edit'),
canEditNames: api.hasPrivilege('pools:edit:names'),
canEditCategory: api.hasPrivilege('pools:edit:category'),
canEditDescription: api.hasPrivilege('pools:edit:description'),
canEditPosts: api.hasPrivilege('pools:edit:posts'),
canMerge: api.hasPrivilege('pools:merge'),
canDelete: api.hasPrivilege('pools:delete'),
categories: categories,
escapeColons: uri.escapeColons,
});
this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('submit', e => this._evtUpdate(e));
this._view.addEventListener('merge', e => this._evtMerge(e));
this._view.addEventListener('delete', e => this._evtDelete(e));
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
);
}
_evtChange(e) {
@ -67,7 +80,11 @@ class PoolController {
_evtSaved(e, section) {
misc.disableExitConfirmation();
if (this._name !== e.detail.pool.names[0]) {
router.replace(uri.formatClientLink('pool', e.detail.pool.id, section), null, false);
router.replace(
uri.formatClientLink("pool", e.detail.pool.id, section),
null,
false
);
}
}
@ -86,62 +103,74 @@ class PoolController {
if (e.detail.posts !== undefined) {
e.detail.pool.posts.clear();
for (let postId of e.detail.posts) {
e.detail.pool.posts.add(Post.fromResponse({id: parseInt(postId)}));
e.detail.pool.posts.add(
Post.fromResponse({ id: parseInt(postId) })
);
}
}
e.detail.pool.save().then(() => {
this._view.showSuccess('Pool saved.');
this._view.enableForm();
}, error => {
this._view.showError(error.message);
this._view.enableForm();
});
e.detail.pool.save().then(
() => {
this._view.showSuccess("Pool saved.");
this._view.enableForm();
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
_evtMerge(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.pool
.merge(e.detail.targetPoolId, e.detail.addAlias)
.then(() => {
this._view.showSuccess('Pool merged.');
e.detail.pool.merge(e.detail.targetPoolId, e.detail.addAlias).then(
() => {
this._view.showSuccess("Pool merged.");
this._view.enableForm();
router.replace(
uri.formatClientLink(
'pool', e.detail.targetPoolId, 'merge'),
"pool",
e.detail.targetPoolId,
"merge"
),
null,
false);
}, error => {
false
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
});
}
);
}
_evtDelete(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.pool.delete()
.then(() => {
const ctx = router.show(uri.formatClientLink('pools'));
ctx.controller.showSuccess('Pool deleted.');
}, error => {
e.detail.pool.delete().then(
() => {
const ctx = router.show(uri.formatClientLink("pools"));
ctx.controller.showSuccess("Pool deleted.");
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
});
}
);
}
}
module.exports = router => {
router.enter(['pool', ':id', 'edit'], (ctx, next) => {
ctx.controller = new PoolController(ctx, 'edit');
module.exports = (router) => {
router.enter(["pool", ":id", "edit"], (ctx, next) => {
ctx.controller = new PoolController(ctx, "edit");
});
router.enter(['pool', ':id', 'merge'], (ctx, next) => {
ctx.controller = new PoolController(ctx, 'merge');
router.enter(["pool", ":id", "merge"], (ctx, next) => {
ctx.controller = new PoolController(ctx, "merge");
});
router.enter(['pool', ':id', 'delete'], (ctx, next) => {
ctx.controller = new PoolController(ctx, 'delete');
router.enter(["pool", ":id", "delete"], (ctx, next) => {
ctx.controller = new PoolController(ctx, "delete");
});
router.enter(['pool', ':id'], (ctx, next) => {
ctx.controller = new PoolController(ctx, 'summary');
router.enter(["pool", ":id"], (ctx, next) => {
ctx.controller = new PoolController(ctx, "summary");
});
};

View file

@ -1,58 +1,65 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const PoolCategoryList = require('../models/pool_category_list.js');
const PoolCreateView = require('../views/pool_create_view.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const PoolCategoryList = require("../models/pool_category_list.js");
const PoolCreateView = require("../views/pool_create_view.js");
const EmptyView = require("../views/empty_view.js");
class PoolCreateController {
constructor(ctx) {
if (!api.hasPrivilege('pools:create')) {
if (!api.hasPrivilege("pools:create")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to create pools.');
this._view.showError("You don't have privileges to create pools.");
return;
}
PoolCategoryList.get().then(poolCategoriesResponse => {
const categories = {};
for (let category of poolCategoriesResponse.results) {
categories[category.name] = category.name;
PoolCategoryList.get().then(
(poolCategoriesResponse) => {
const categories = {};
for (let category of poolCategoriesResponse.results) {
categories[category.name] = category.name;
}
this._view = new PoolCreateView({
canCreate: api.hasPrivilege("pools:create"),
categories: categories,
escapeColons: uri.escapeColons,
});
this._view.addEventListener("submit", (e) =>
this._evtCreate(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
this._view = new PoolCreateView({
canCreate: api.hasPrivilege('pools:create'),
categories: categories,
escapeColons: uri.escapeColons,
});
this._view.addEventListener('submit', e => this._evtCreate(e));
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
);
}
_evtCreate(e) {
this._view.clearMessages();
this._view.disableForm();
api.post(uri.formatApiLink('pool'), e.detail)
.then(() => {
api.post(uri.formatApiLink("pool"), e.detail).then(
() => {
this._view.clearMessages();
misc.disableExitConfirmation();
const ctx = router.show(uri.formatClientLink('pools'));
ctx.controller.showSuccess('Pool created.');
}, error => {
const ctx = router.show(uri.formatClientLink("pools"));
ctx.controller.showSuccess("Pool created.");
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
});
}
);
}
}
module.exports = router => {
router.enter(['pool', 'create'], (ctx, next) => {
ctx.controller = new PoolCreateController(ctx, 'create');
module.exports = (router) => {
router.enter(["pool", "create"], (ctx, next) => {
ctx.controller = new PoolCreateController(ctx, "create");
});
};

View file

@ -1,47 +1,51 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const PoolList = require('../models/pool_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const PoolsHeaderView = require('../views/pools_header_view.js');
const PoolsPageView = require('../views/pools_page_view.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const PoolList = require("../models/pool_list.js");
const topNavigation = require("../models/top_navigation.js");
const PageController = require("../controllers/page_controller.js");
const PoolsHeaderView = require("../views/pools_header_view.js");
const PoolsPageView = require("../views/pools_page_view.js");
const EmptyView = require("../views/empty_view.js");
const fields = [
'id',
'names',
'posts',
'creationTime',
'postCount',
'category'
"id",
"names",
"posts",
"creationTime",
"postCount",
"category",
];
class PoolListController {
constructor(ctx) {
this._pageController = new PageController();
if (!api.hasPrivilege('pools:list')) {
if (!api.hasPrivilege("pools:list")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view pools.');
this._view.showError("You don't have privileges to view pools.");
return;
}
this._ctx = ctx;
topNavigation.activate('pools');
topNavigation.setTitle('Listing pools');
topNavigation.activate("pools");
topNavigation.setTitle("Listing pools");
this._headerView = new PoolsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters,
canCreate: api.hasPrivilege('pools:create'),
canEditPoolCategories: api.hasPrivilege('poolCategories:edit'),
canCreate: api.hasPrivilege("pools:create"),
canEditPoolCategories: api.hasPrivilege("poolCategories:edit"),
});
this._headerView.addEventListener(
'submit', e => this._evtSubmit(e), 'navigate', e => this._evtNavigate(e));
"submit",
(e) => this._evtSubmit(e),
"navigate",
(e) => this._evtNavigate(e)
);
this._syncPageController();
}
@ -57,24 +61,27 @@ class PoolListController {
_evtSubmit(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.pool.save()
.then(() => {
this._installView(e.detail.pool, 'edit');
this._view.showSuccess('Pool created.');
e.detail.pool.save().then(
() => {
this._installView(e.detail.pool, "edit");
this._view.showSuccess("Pool created.");
router.replace(
uri.formatClientLink(
'pool', e.detail.pool.id, 'edit'),
uri.formatClientLink("pool", e.detail.pool.id, "edit"),
null,
false);
}, error => {
false
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
});
}
);
}
_evtNavigate(e) {
router.showNoDispatch(
uri.formatClientLink('pools', e.detail.parameters));
uri.formatClientLink("pools", e.detail.parameters)
);
Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController();
}
@ -84,25 +91,29 @@ class PoolListController {
parameters: this._ctx.parameters,
defaultLimit: 50,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign(
{}, this._ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('pools', parameters);
const parameters = Object.assign({}, this._ctx.parameters, {
offset: offset,
limit: limit,
});
return uri.formatClientLink("pools", parameters);
},
requestPage: (offset, limit) => {
return PoolList.search(
this._ctx.parameters.query, offset, limit, fields);
this._ctx.parameters.query,
offset,
limit,
fields
);
},
pageRenderer: pageCtx => {
pageRenderer: (pageCtx) => {
return new PoolsPageView(pageCtx);
},
});
}
}
module.exports = router => {
router.enter(
['pools'],
(ctx, next) => {
ctx.controller = new PoolListController(ctx);
});
module.exports = (router) => {
router.enter(["pools"], (ctx, next) => {
ctx.controller = new PoolListController(ctx);
});
};

View file

@ -1,28 +1,33 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const settings = require('../models/settings.js');
const Post = require('../models/post.js');
const PostList = require('../models/post_list.js');
const PostDetailView = require('../views/post_detail_view.js');
const BasePostController = require('./base_post_controller.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const settings = require("../models/settings.js");
const Post = require("../models/post.js");
const PostList = require("../models/post_list.js");
const PostDetailView = require("../views/post_detail_view.js");
const BasePostController = require("./base_post_controller.js");
const EmptyView = require("../views/empty_view.js");
class PostDetailController extends BasePostController {
constructor(ctx, section) {
super(ctx);
Post.get(ctx.parameters.id).then(post => {
this._id = ctx.parameters.id;
post.addEventListener('change', e => this._evtSaved(e, section));
this._installView(post, section);
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
Post.get(ctx.parameters.id).then(
(post) => {
this._id = ctx.parameters.id;
post.addEventListener("change", (e) =>
this._evtSaved(e, section)
);
this._installView(post, section);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
}
showSuccess(message) {
@ -33,58 +38,68 @@ class PostDetailController extends BasePostController {
this._view = new PostDetailView({
post: post,
section: section,
canMerge: api.hasPrivilege('posts:merge'),
canMerge: api.hasPrivilege("posts:merge"),
});
this._view.addEventListener('select', e => this._evtSelect(e));
this._view.addEventListener('merge', e => this._evtMerge(e));
this._view.addEventListener("select", (e) => this._evtSelect(e));
this._view.addEventListener("merge", (e) => this._evtMerge(e));
}
_evtSelect(e) {
this._view.clearMessages();
this._view.disableForm();
Post.get(e.detail.postId).then(post => {
this._view.selectPost(post);
this._view.enableForm();
}, error => {
this._view.showError(error.message);
this._view.enableForm();
});
Post.get(e.detail.postId).then(
(post) => {
this._view.selectPost(post);
this._view.enableForm();
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
_evtSaved(e, section) {
misc.disableExitConfirmation();
if (this._id !== e.detail.post.id) {
router.replace(
uri.formatClientLink('post', e.detail.post.id, section),
uri.formatClientLink("post", e.detail.post.id, section),
null,
false);
false
);
}
}
_evtMerge(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.post.merge(e.detail.targetPost.id, e.detail.useOldContent)
.then(() => {
this._installView(e.detail.post, 'merge');
this._view.showSuccess('Post merged.');
router.replace(
uri.formatClientLink(
'post', e.detail.targetPost.id, 'merge'),
null,
false);
}, error => {
this._view.showError(error.message);
this._view.enableForm();
});
e.detail.post
.merge(e.detail.targetPost.id, e.detail.useOldContent)
.then(
() => {
this._installView(e.detail.post, "merge");
this._view.showSuccess("Post merged.");
router.replace(
uri.formatClientLink(
"post",
e.detail.targetPost.id,
"merge"
),
null,
false
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
}
module.exports = router => {
router.enter(
['post', ':id', 'merge'],
(ctx, next) => {
ctx.controller = new PostDetailController(ctx, 'merge');
});
module.exports = (router) => {
router.enter(["post", ":id", "merge"], (ctx, next) => {
ctx.controller = new PostDetailController(ctx, "merge");
});
};

View file

@ -1,48 +1,56 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const settings = require('../models/settings.js');
const uri = require('../util/uri.js');
const PostList = require('../models/post_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const PostsHeaderView = require('../views/posts_header_view.js');
const PostsPageView = require('../views/posts_page_view.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const settings = require("../models/settings.js");
const uri = require("../util/uri.js");
const PostList = require("../models/post_list.js");
const topNavigation = require("../models/top_navigation.js");
const PageController = require("../controllers/page_controller.js");
const PostsHeaderView = require("../views/posts_header_view.js");
const PostsPageView = require("../views/posts_page_view.js");
const EmptyView = require("../views/empty_view.js");
const fields = [
'id', 'thumbnailUrl', 'type', 'safety',
'score', 'favoriteCount', 'commentCount', 'tags', 'version'
"id",
"thumbnailUrl",
"type",
"safety",
"score",
"favoriteCount",
"commentCount",
"tags",
"version",
];
class PostListController {
constructor(ctx) {
this._pageController = new PageController();
if (!api.hasPrivilege('posts:list')) {
if (!api.hasPrivilege("posts:list")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view posts.');
this._view.showError("You don't have privileges to view posts.");
return;
}
this._ctx = ctx;
topNavigation.activate('posts');
topNavigation.setTitle('Listing posts');
topNavigation.activate("posts");
topNavigation.setTitle("Listing posts");
this._headerView = new PostsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters,
enableSafety: api.safetyEnabled(),
canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'),
canBulkEditSafety: api.hasPrivilege('posts:bulk-edit:safety'),
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"),
bulkEdit: {
tags: this._bulkEditTags
tags: this._bulkEditTags,
},
});
this._headerView.addEventListener(
'navigate', e => this._evtNavigate(e));
this._headerView.addEventListener("navigate", (e) =>
this._evtNavigate(e)
);
this._syncPageController();
}
@ -52,33 +60,35 @@ class PostListController {
}
get _bulkEditTags() {
return (this._ctx.parameters.tag || '').split(/\s+/).filter(s => s);
return (this._ctx.parameters.tag || "").split(/\s+/).filter((s) => s);
}
_evtNavigate(e) {
router.showNoDispatch(
uri.formatClientLink('posts', e.detail.parameters));
uri.formatClientLink("posts", e.detail.parameters)
);
Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController();
}
_evtTag(e) {
Promise.all(
this._bulkEditTags.map(tag => e.detail.post.tags.addByName(tag)))
this._bulkEditTags.map((tag) => e.detail.post.tags.addByName(tag))
)
.then(e.detail.post.save())
.catch(error => window.alert(error.message));
.catch((error) => window.alert(error.message));
}
_evtUntag(e) {
for (let tag of this._bulkEditTags) {
e.detail.post.tags.removeByName(tag);
}
e.detail.post.save().catch(error => window.alert(error.message));
e.detail.post.save().catch((error) => window.alert(error.message));
}
_evtChangeSafety(e) {
e.detail.post.safety = e.detail.safety;
e.detail.post.save().catch(error => window.alert(error.message));
e.detail.post.save().catch((error) => window.alert(error.message));
}
_syncPageController() {
@ -86,39 +96,45 @@ class PostListController {
parameters: this._ctx.parameters,
defaultLimit: parseInt(settings.get().postsPerPage),
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign(
{}, this._ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('posts', parameters);
const parameters = Object.assign({}, this._ctx.parameters, {
offset: offset,
limit: limit,
});
return uri.formatClientLink("posts", parameters);
},
requestPage: (offset, limit) => {
return PostList.search(
this._ctx.parameters.query, offset, limit, fields);
this._ctx.parameters.query,
offset,
limit,
fields
);
},
pageRenderer: pageCtx => {
pageRenderer: (pageCtx) => {
Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege('posts:view'),
canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'),
canBulkEditSafety:
api.hasPrivilege('posts:bulk-edit:safety'),
canViewPosts: api.hasPrivilege("posts:view"),
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
canBulkEditSafety: api.hasPrivilege(
"posts:bulk-edit:safety"
),
bulkEdit: {
tags: this._bulkEditTags,
},
});
const view = new PostsPageView(pageCtx);
view.addEventListener('tag', e => this._evtTag(e));
view.addEventListener('untag', e => this._evtUntag(e));
view.addEventListener(
'changeSafety', e => this._evtChangeSafety(e));
view.addEventListener("tag", (e) => this._evtTag(e));
view.addEventListener("untag", (e) => this._evtUntag(e));
view.addEventListener("changeSafety", (e) =>
this._evtChangeSafety(e)
);
return view;
},
});
}
}
module.exports = router => {
router.enter(
['posts'],
(ctx, next) => {
ctx.controller = new PostListController(ctx);
});
module.exports = (router) => {
router.enter(["posts"], (ctx, next) => {
ctx.controller = new PostListController(ctx);
});
};

View file

@ -1,16 +1,16 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const misc = require('../util/misc.js');
const settings = require('../models/settings.js');
const Comment = require('../models/comment.js');
const Post = require('../models/post.js');
const PostList = require('../models/post_list.js');
const PostMainView = require('../views/post_main_view.js');
const BasePostController = require('./base_post_controller.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const misc = require("../util/misc.js");
const settings = require("../models/settings.js");
const Comment = require("../models/comment.js");
const Post = require("../models/post.js");
const PostList = require("../models/post_list.js");
const PostMainView = require("../views/post_main_view.js");
const BasePostController = require("./base_post_controller.js");
const EmptyView = require("../views/empty_view.js");
class PostMainController extends BasePostController {
constructor(ctx, editMode) {
@ -21,74 +21,107 @@ class PostMainController extends BasePostController {
Post.get(ctx.parameters.id),
PostList.getAround(
ctx.parameters.id,
parameters ? parameters.query : null),
]).then(responses => {
const [post, aroundResponse] = responses;
parameters ? parameters.query : null
),
]).then(
(responses) => {
const [post, aroundResponse] = responses;
// remove junk from query, but save it into history so that it can
// be still accessed after history navigation / page refresh
if (parameters.query) {
ctx.state.parameters = parameters;
const url = editMode ?
uri.formatClientLink('post', ctx.parameters.id, 'edit') :
uri.formatClientLink('post', ctx.parameters.id);
router.replace(url, ctx.state, false);
// remove junk from query, but save it into history so that it can
// be still accessed after history navigation / page refresh
if (parameters.query) {
ctx.state.parameters = parameters;
const url = editMode
? uri.formatClientLink(
"post",
ctx.parameters.id,
"edit"
)
: uri.formatClientLink("post", ctx.parameters.id);
router.replace(url, ctx.state, false);
}
this._post = post;
this._view = new PostMainView({
post: post,
editMode: editMode,
prevPostId: aroundResponse.prev
? aroundResponse.prev.id
: null,
nextPostId: aroundResponse.next
? aroundResponse.next.id
: null,
canEditPosts: api.hasPrivilege("posts:edit"),
canDeletePosts: api.hasPrivilege("posts:delete"),
canFeaturePosts: api.hasPrivilege("posts:feature"),
canListComments: api.hasPrivilege("comments:list"),
canCreateComments: api.hasPrivilege("comments:create"),
parameters: parameters,
});
if (this._view.sidebarControl) {
this._view.sidebarControl.addEventListener(
"favorite",
(e) => this._evtFavoritePost(e)
);
this._view.sidebarControl.addEventListener(
"unfavorite",
(e) => this._evtUnfavoritePost(e)
);
this._view.sidebarControl.addEventListener("score", (e) =>
this._evtScorePost(e)
);
this._view.sidebarControl.addEventListener(
"fitModeChange",
(e) => this._evtFitModeChange(e)
);
this._view.sidebarControl.addEventListener("change", (e) =>
this._evtPostChange(e)
);
this._view.sidebarControl.addEventListener("submit", (e) =>
this._evtUpdatePost(e)
);
this._view.sidebarControl.addEventListener(
"feature",
(e) => this._evtFeaturePost(e)
);
this._view.sidebarControl.addEventListener("delete", (e) =>
this._evtDeletePost(e)
);
this._view.sidebarControl.addEventListener("merge", (e) =>
this._evtMergePost(e)
);
}
if (this._view.commentControl) {
this._view.commentControl.addEventListener("change", (e) =>
this._evtCommentChange(e)
);
this._view.commentControl.addEventListener("submit", (e) =>
this._evtCreateComment(e)
);
}
if (this._view.commentListControl) {
this._view.commentListControl.addEventListener(
"submit",
(e) => this._evtUpdateComment(e)
);
this._view.commentListControl.addEventListener(
"score",
(e) => this._evtScoreComment(e)
);
this._view.commentListControl.addEventListener(
"delete",
(e) => this._evtDeleteComment(e)
);
}
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
this._post = post;
this._view = new PostMainView({
post: post,
editMode: editMode,
prevPostId: aroundResponse.prev ? aroundResponse.prev.id : null,
nextPostId: aroundResponse.next ? aroundResponse.next.id : null,
canEditPosts: api.hasPrivilege('posts:edit'),
canDeletePosts: api.hasPrivilege('posts:delete'),
canFeaturePosts: api.hasPrivilege('posts:feature'),
canListComments: api.hasPrivilege('comments:list'),
canCreateComments: api.hasPrivilege('comments:create'),
parameters: parameters,
});
if (this._view.sidebarControl) {
this._view.sidebarControl.addEventListener(
'favorite', e => this._evtFavoritePost(e));
this._view.sidebarControl.addEventListener(
'unfavorite', e => this._evtUnfavoritePost(e));
this._view.sidebarControl.addEventListener(
'score', e => this._evtScorePost(e));
this._view.sidebarControl.addEventListener(
'fitModeChange', e => this._evtFitModeChange(e));
this._view.sidebarControl.addEventListener(
'change', e => this._evtPostChange(e));
this._view.sidebarControl.addEventListener(
'submit', e => this._evtUpdatePost(e));
this._view.sidebarControl.addEventListener(
'feature', e => this._evtFeaturePost(e));
this._view.sidebarControl.addEventListener(
'delete', e => this._evtDeletePost(e));
this._view.sidebarControl.addEventListener(
'merge', e => this._evtMergePost(e));
}
if (this._view.commentControl) {
this._view.commentControl.addEventListener(
'change', e => this._evtCommentChange(e));
this._view.commentControl.addEventListener(
'submit', e => this._evtCreateComment(e));
}
if (this._view.commentListControl) {
this._view.commentListControl.addEventListener(
'submit', e => this._evtUpdateComment(e));
this._view.commentListControl.addEventListener(
'score', e => this._evtScoreComment(e));
this._view.commentListControl.addEventListener(
'delete', e => this._evtDeleteComment(e));
}
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
);
}
_evtFitModeChange(e) {
@ -100,32 +133,36 @@ class PostMainController extends BasePostController {
_evtFeaturePost(e) {
this._view.sidebarControl.disableForm();
this._view.sidebarControl.clearMessages();
e.detail.post.feature()
.then(() => {
this._view.sidebarControl.showSuccess('Post featured.');
e.detail.post.feature().then(
() => {
this._view.sidebarControl.showSuccess("Post featured.");
this._view.sidebarControl.enableForm();
}, error => {
},
(error) => {
this._view.sidebarControl.showError(error.message);
this._view.sidebarControl.enableForm();
});
}
);
}
_evtMergePost(e) {
router.show(uri.formatClientLink('post', e.detail.post.id, 'merge'));
router.show(uri.formatClientLink("post", e.detail.post.id, "merge"));
}
_evtDeletePost(e) {
this._view.sidebarControl.disableForm();
this._view.sidebarControl.clearMessages();
e.detail.post.delete()
.then(() => {
e.detail.post.delete().then(
() => {
misc.disableExitConfirmation();
const ctx = router.show(uri.formatClientLink('posts'));
ctx.controller.showSuccess('Post deleted.');
}, error => {
const ctx = router.show(uri.formatClientLink("posts"));
ctx.controller.showSuccess("Post deleted.");
},
(error) => {
this._view.sidebarControl.showError(error.message);
this._view.sidebarControl.enableForm();
});
}
);
}
_evtUpdatePost(e) {
@ -150,15 +187,17 @@ class PostMainController extends BasePostController {
if (e.detail.source !== undefined) {
post.source = e.detail.source;
}
post.save()
.then(() => {
this._view.sidebarControl.showSuccess('Post saved.');
post.save().then(
() => {
this._view.sidebarControl.showSuccess("Post saved.");
this._view.sidebarControl.enableForm();
misc.disableExitConfirmation();
}, error => {
},
(error) => {
this._view.sidebarControl.showError(error.message);
this._view.sidebarControl.enableForm();
});
}
);
}
_evtPostChange(e) {
@ -173,79 +212,82 @@ class PostMainController extends BasePostController {
this._view.commentControl.disableForm();
const comment = Comment.create(this._post.id);
comment.text = e.detail.text;
comment.save()
.then(() => {
comment.save().then(
() => {
this._post.comments.add(comment);
this._view.commentControl.exitEditMode();
this._view.commentControl.enableForm();
misc.disableExitConfirmation();
}, error => {
},
(error) => {
this._view.commentControl.showError(error.message);
this._view.commentControl.enableForm();
});
}
);
}
_evtUpdateComment(e) {
// TODO: disable form
e.detail.comment.text = e.detail.text;
e.detail.comment.save()
.catch(error => {
e.detail.target.showError(error.message);
// TODO: enable form
});
e.detail.comment.save().catch((error) => {
e.detail.target.showError(error.message);
// TODO: enable form
});
}
_evtScoreComment(e) {
e.detail.comment.setScore(e.detail.score)
.catch(error => window.alert(error.message));
e.detail.comment
.setScore(e.detail.score)
.catch((error) => window.alert(error.message));
}
_evtDeleteComment(e) {
e.detail.comment.delete()
.catch(error => window.alert(error.message));
e.detail.comment
.delete()
.catch((error) => window.alert(error.message));
}
_evtScorePost(e) {
if (!api.hasPrivilege('posts:score')) {
if (!api.hasPrivilege("posts:score")) {
return;
}
e.detail.post.setScore(e.detail.score)
.catch(error => window.alert(error.message));
e.detail.post
.setScore(e.detail.score)
.catch((error) => window.alert(error.message));
}
_evtFavoritePost(e) {
if (!api.hasPrivilege('posts:favorite')) {
if (!api.hasPrivilege("posts:favorite")) {
return;
}
e.detail.post.addToFavorites()
.catch(error => window.alert(error.message));
e.detail.post
.addToFavorites()
.catch((error) => window.alert(error.message));
}
_evtUnfavoritePost(e) {
if (!api.hasPrivilege('posts:favorite')) {
if (!api.hasPrivilege("posts:favorite")) {
return;
}
e.detail.post.removeFromFavorites()
.catch(error => window.alert(error.message));
e.detail.post
.removeFromFavorites()
.catch((error) => window.alert(error.message));
}
}
module.exports = router => {
router.enter(['post', ':id', 'edit'],
(ctx, next) => {
// restore parameters from history state
if (ctx.state.parameters) {
Object.assign(ctx.parameters, ctx.state.parameters);
}
ctx.controller = new PostMainController(ctx, true);
});
router.enter(
['post', ':id'],
(ctx, next) => {
// restore parameters from history state
if (ctx.state.parameters) {
Object.assign(ctx.parameters, ctx.state.parameters);
}
ctx.controller = new PostMainController(ctx, false);
});
module.exports = (router) => {
router.enter(["post", ":id", "edit"], (ctx, next) => {
// restore parameters from history state
if (ctx.state.parameters) {
Object.assign(ctx.parameters, ctx.state.parameters);
}
ctx.controller = new PostMainController(ctx, true);
});
router.enter(["post", ":id"], (ctx, next) => {
// restore parameters from history state
if (ctx.state.parameters) {
Object.assign(ctx.parameters, ctx.state.parameters);
}
ctx.controller = new PostMainController(ctx, false);
});
};

View file

@ -1,40 +1,40 @@
'use strict';
"use strict";
const api = require('../api.js');
const router = require('../router.js');
const uri = require('../util/uri.js');
const misc = require('../util/misc.js');
const progress = require('../util/progress.js');
const topNavigation = require('../models/top_navigation.js');
const Post = require('../models/post.js');
const Tag = require('../models/tag.js');
const PostUploadView = require('../views/post_upload_view.js');
const EmptyView = require('../views/empty_view.js');
const api = require("../api.js");
const router = require("../router.js");
const uri = require("../util/uri.js");
const misc = require("../util/misc.js");
const progress = require("../util/progress.js");
const topNavigation = require("../models/top_navigation.js");
const Post = require("../models/post.js");
const Tag = require("../models/tag.js");
const PostUploadView = require("../views/post_upload_view.js");
const EmptyView = require("../views/empty_view.js");
const genericErrorMessage =
'One of the posts needs your attention; ' +
"One of the posts needs your attention; " +
'click "resume upload" when you\'re ready.';
class PostUploadController {
constructor() {
this._lastCancellablePromise = null;
if (!api.hasPrivilege('posts:create')) {
if (!api.hasPrivilege("posts:create")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to upload posts.');
this._view.showError("You don't have privileges to upload posts.");
return;
}
topNavigation.activate('upload');
topNavigation.setTitle('Upload');
topNavigation.activate("upload");
topNavigation.setTitle("Upload");
this._view = new PostUploadView({
canUploadAnonymously: api.hasPrivilege('posts:create:anonymous'),
canViewPosts: api.hasPrivilege('posts:view'),
canUploadAnonymously: api.hasPrivilege("posts:create:anonymous"),
canViewPosts: api.hasPrivilege("posts:view"),
enableSafety: api.safetyEnabled(),
});
this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('submit', e => this._evtSubmit(e));
this._view.addEventListener('cancel', e => this._evtCancel(e));
this._view.addEventListener("change", (e) => this._evtChange(e));
this._view.addEventListener("submit", (e) => this._evtSubmit(e));
this._view.addEventListener("cancel", (e) => this._evtCancel(e));
}
_evtChange(e) {
@ -56,87 +56,109 @@ class PostUploadController {
this._view.disableForm();
this._view.clearMessages();
e.detail.uploadables.reduce(
(promise, uploadable) => promise.then(() => this._uploadSinglePost(
uploadable, e.detail.skipDuplicates)),
Promise.resolve())
.then(() => {
this._view.clearMessages();
misc.disableExitConfirmation();
const ctx = router.show(uri.formatClientLink('posts'));
ctx.controller.showSuccess('Posts uploaded.');
}, error => {
if (error.uploadable) {
if (error.similarPosts) {
error.uploadable.lookalikes = error.similarPosts;
this._view.updateUploadable(error.uploadable);
this._view.showInfo(genericErrorMessage);
this._view.showInfo(
error.message, error.uploadable);
e.detail.uploadables
.reduce(
(promise, uploadable) =>
promise.then(() =>
this._uploadSinglePost(
uploadable,
e.detail.skipDuplicates
)
),
Promise.resolve()
)
.then(
() => {
this._view.clearMessages();
misc.disableExitConfirmation();
const ctx = router.show(uri.formatClientLink("posts"));
ctx.controller.showSuccess("Posts uploaded.");
},
(error) => {
if (error.uploadable) {
if (error.similarPosts) {
error.uploadable.lookalikes = error.similarPosts;
this._view.updateUploadable(error.uploadable);
this._view.showInfo(genericErrorMessage);
this._view.showInfo(
error.message,
error.uploadable
);
} else {
this._view.showError(genericErrorMessage);
this._view.showError(
error.message,
error.uploadable
);
}
} else {
this._view.showError(genericErrorMessage);
this._view.showError(
error.message, error.uploadable);
this._view.showError(error.message);
}
} else {
this._view.showError(error.message);
this._view.enableForm();
}
this._view.enableForm();
});
);
}
_uploadSinglePost(uploadable, skipDuplicates) {
progress.start();
let reverseSearchPromise = Promise.resolve();
if (!uploadable.lookalikesConfirmed) {
reverseSearchPromise =
Post.reverseSearch(uploadable.url || uploadable.file);
reverseSearchPromise = Post.reverseSearch(
uploadable.url || uploadable.file
);
}
this._lastCancellablePromise = reverseSearchPromise;
return reverseSearchPromise.then(searchResult => {
if (searchResult) {
// notify about exact duplicate
if (searchResult.exactPost) {
if (skipDuplicates) {
this._view.removeUploadable(uploadable);
return Promise.resolve();
} else {
let error = new Error('Post already uploaded ' +
`(@${searchResult.exactPost.id})`);
return reverseSearchPromise
.then((searchResult) => {
if (searchResult) {
// notify about exact duplicate
if (searchResult.exactPost) {
if (skipDuplicates) {
this._view.removeUploadable(uploadable);
return Promise.resolve();
} else {
let error = new Error(
"Post already uploaded " +
`(@${searchResult.exactPost.id})`
);
error.uploadable = uploadable;
return Promise.reject(error);
}
}
// notify about similar posts
if (searchResult.similarPosts.length) {
let error = new Error(
`Found ${searchResult.similarPosts.length} similar ` +
"posts.\nYou can resume or discard this upload."
);
error.uploadable = uploadable;
error.similarPosts = searchResult.similarPosts;
return Promise.reject(error);
}
}
// notify about similar posts
if (searchResult.similarPosts.length) {
let error = new Error(
`Found ${searchResult.similarPosts.length} similar ` +
'posts.\nYou can resume or discard this upload.');
error.uploadable = uploadable;
error.similarPosts = searchResult.similarPosts;
return Promise.reject(error);
}
}
// no duplicates, proceed with saving
let post = this._uploadableToPost(uploadable);
let savePromise = post.save(uploadable.anonymous)
.then(() => {
// no duplicates, proceed with saving
let post = this._uploadableToPost(uploadable);
let savePromise = post.save(uploadable.anonymous).then(() => {
this._view.removeUploadable(uploadable);
return Promise.resolve();
});
this._lastCancellablePromise = savePromise;
return savePromise;
}).then(result => {
progress.done();
return Promise.resolve(result);
}, error => {
error.uploadable = uploadable;
progress.done();
return Promise.reject(error);
});
this._lastCancellablePromise = savePromise;
return savePromise;
})
.then(
(result) => {
progress.done();
return Promise.resolve(result);
},
(error) => {
error.uploadable = uploadable;
progress.done();
return Promise.reject(error);
}
);
}
_uploadableToPost(uploadable) {
@ -159,8 +181,8 @@ class PostUploadController {
}
}
module.exports = router => {
router.enter(['upload'], (ctx, next) => {
module.exports = (router) => {
router.enter(["upload"], (ctx, next) => {
ctx.controller = new PostUploadController();
});
};

View file

@ -1,28 +1,28 @@
'use strict';
"use strict";
const settings = require('../models/settings.js');
const topNavigation = require('../models/top_navigation.js');
const SettingsView = require('../views/settings_view.js');
const settings = require("../models/settings.js");
const topNavigation = require("../models/top_navigation.js");
const SettingsView = require("../views/settings_view.js");
class SettingsController {
constructor() {
topNavigation.activate('settings');
topNavigation.setTitle('Browsing settings');
topNavigation.activate("settings");
topNavigation.setTitle("Browsing settings");
this._view = new SettingsView({
settings: settings.get(),
});
this._view.addEventListener('submit', e => this._evtSubmit(e));
this._view.addEventListener("submit", (e) => this._evtSubmit(e));
}
_evtSubmit(e) {
this._view.clearMessages();
settings.save(e.detail);
this._view.showSuccess('Settings saved.');
this._view.showSuccess("Settings saved.");
}
}
module.exports = router => {
router.enter(['settings'], (ctx, next) => {
module.exports = (router) => {
router.enter(["settings"], (ctx, next) => {
ctx.controller = new SettingsController();
});
};

View file

@ -1,41 +1,43 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const SnapshotList = require('../models/snapshot_list.js');
const PageController = require('../controllers/page_controller.js');
const topNavigation = require('../models/top_navigation.js');
const SnapshotsPageView = require('../views/snapshots_page_view.js');
const EmptyView = require('../views/empty_view.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const SnapshotList = require("../models/snapshot_list.js");
const PageController = require("../controllers/page_controller.js");
const topNavigation = require("../models/top_navigation.js");
const SnapshotsPageView = require("../views/snapshots_page_view.js");
const EmptyView = require("../views/empty_view.js");
class SnapshotsController {
constructor(ctx) {
if (!api.hasPrivilege('snapshots:list')) {
if (!api.hasPrivilege("snapshots:list")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view history.');
this._view.showError("You don't have privileges to view history.");
return;
}
topNavigation.activate('');
topNavigation.setTitle('History');
topNavigation.activate("");
topNavigation.setTitle("History");
this._pageController = new PageController();
this._pageController.run({
parameters: ctx.parameters,
defaultLimit: 25,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign(
{}, ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('history', parameters);
const parameters = Object.assign({}, ctx.parameters, {
offset: offset,
limit: limit,
});
return uri.formatClientLink("history", parameters);
},
requestPage: (offset, limit) => {
return SnapshotList.search('', offset, limit);
return SnapshotList.search("", offset, limit);
},
pageRenderer: pageCtx => {
pageRenderer: (pageCtx) => {
Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege('posts:view'),
canViewUsers: api.hasPrivilege('users:view'),
canViewTags: api.hasPrivilege('tags:view'),
canViewPosts: api.hasPrivilege("posts:view"),
canViewUsers: api.hasPrivilege("users:view"),
canViewTags: api.hasPrivilege("tags:view"),
});
return new SnapshotsPageView(pageCtx);
},
@ -43,9 +45,8 @@ class SnapshotsController {
}
}
module.exports = router => {
router.enter(['history'],
(ctx, next) => {
ctx.controller = new SnapshotsController(ctx);
});
module.exports = (router) => {
router.enter(["history"], (ctx, next) => {
ctx.controller = new SnapshotsController(ctx);
});
};

View file

@ -1,57 +1,67 @@
'use strict';
"use strict";
const api = require('../api.js');
const tags = require('../tags.js');
const TagCategoryList = require('../models/tag_category_list.js');
const topNavigation = require('../models/top_navigation.js');
const TagCategoriesView = require('../views/tag_categories_view.js');
const EmptyView = require('../views/empty_view.js');
const api = require("../api.js");
const tags = require("../tags.js");
const TagCategoryList = require("../models/tag_category_list.js");
const topNavigation = require("../models/top_navigation.js");
const TagCategoriesView = require("../views/tag_categories_view.js");
const EmptyView = require("../views/empty_view.js");
class TagCategoriesController {
constructor() {
if (!api.hasPrivilege('tagCategories:list')) {
if (!api.hasPrivilege("tagCategories:list")) {
this._view = new EmptyView();
this._view.showError(
'You don\'t have privileges to view tag categories.');
"You don't have privileges to view tag categories."
);
return;
}
topNavigation.activate('tags');
topNavigation.setTitle('Listing tags');
TagCategoryList.get().then(response => {
this._tagCategories = response.results;
this._view = new TagCategoriesView({
tagCategories: this._tagCategories,
canEditName: api.hasPrivilege('tagCategories:edit:name'),
canEditColor: api.hasPrivilege('tagCategories:edit:color'),
canDelete: api.hasPrivilege('tagCategories:delete'),
canCreate: api.hasPrivilege('tagCategories:create'),
canSetDefault: api.hasPrivilege('tagCategories:setDefault'),
});
this._view.addEventListener('submit', e => this._evtSubmit(e));
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
topNavigation.activate("tags");
topNavigation.setTitle("Listing tags");
TagCategoryList.get().then(
(response) => {
this._tagCategories = response.results;
this._view = new TagCategoriesView({
tagCategories: this._tagCategories,
canEditName: api.hasPrivilege("tagCategories:edit:name"),
canEditColor: api.hasPrivilege("tagCategories:edit:color"),
canDelete: api.hasPrivilege("tagCategories:delete"),
canCreate: api.hasPrivilege("tagCategories:create"),
canSetDefault: api.hasPrivilege(
"tagCategories:setDefault"
),
});
this._view.addEventListener("submit", (e) =>
this._evtSubmit(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
}
_evtSubmit(e) {
this._view.clearMessages();
this._view.disableForm();
this._tagCategories.save()
.then(() => {
this._tagCategories.save().then(
() => {
tags.refreshCategoryColorMap();
this._view.enableForm();
this._view.showSuccess('Changes saved.');
}, error => {
this._view.showSuccess("Changes saved.");
},
(error) => {
this._view.enableForm();
this._view.showError(error.message);
});
}
);
}
}
module.exports = router => {
router.enter(['tag-categories'], (ctx, next) => {
module.exports = (router) => {
router.enter(["tag-categories"], (ctx, next) => {
ctx.controller = new TagCategoriesController(ctx, next);
});
};

View file

@ -1,63 +1,80 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const Tag = require('../models/tag.js');
const TagCategoryList = require('../models/tag_category_list.js');
const topNavigation = require('../models/top_navigation.js');
const TagView = require('../views/tag_view.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const Tag = require("../models/tag.js");
const TagCategoryList = require("../models/tag_category_list.js");
const topNavigation = require("../models/top_navigation.js");
const TagView = require("../views/tag_view.js");
const EmptyView = require("../views/empty_view.js");
class TagController {
constructor(ctx, section) {
if (!api.hasPrivilege('tags:view')) {
if (!api.hasPrivilege("tags:view")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view tags.');
this._view.showError("You don't have privileges to view tags.");
return;
}
Promise.all([
TagCategoryList.get(),
Tag.get(ctx.parameters.name),
]).then(responses => {
const [tagCategoriesResponse, tag] = responses;
]).then(
(responses) => {
const [tagCategoriesResponse, tag] = responses;
topNavigation.activate('tags');
topNavigation.setTitle('Tag #' + tag.names[0]);
topNavigation.activate("tags");
topNavigation.setTitle("Tag #" + tag.names[0]);
this._name = ctx.parameters.name;
tag.addEventListener('change', e => this._evtSaved(e, section));
this._name = ctx.parameters.name;
tag.addEventListener("change", (e) =>
this._evtSaved(e, section)
);
const categories = {};
for (let category of tagCategoriesResponse.results) {
categories[category.name] = category.name;
const categories = {};
for (let category of tagCategoriesResponse.results) {
categories[category.name] = category.name;
}
this._view = new TagView({
tag: tag,
section: section,
canEditAnything: api.hasPrivilege("tags:edit"),
canEditNames: api.hasPrivilege("tags:edit:names"),
canEditCategory: api.hasPrivilege("tags:edit:category"),
canEditImplications: api.hasPrivilege(
"tags:edit:implications"
),
canEditSuggestions: api.hasPrivilege(
"tags:edit:suggestions"
),
canEditDescription: api.hasPrivilege(
"tags:edit:description"
),
canMerge: api.hasPrivilege("tags:merge"),
canDelete: api.hasPrivilege("tags:delete"),
categories: categories,
escapeColons: uri.escapeColons,
});
this._view.addEventListener("change", (e) =>
this._evtChange(e)
);
this._view.addEventListener("submit", (e) =>
this._evtUpdate(e)
);
this._view.addEventListener("merge", (e) => this._evtMerge(e));
this._view.addEventListener("delete", (e) =>
this._evtDelete(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
this._view = new TagView({
tag: tag,
section: section,
canEditAnything: api.hasPrivilege('tags:edit'),
canEditNames: api.hasPrivilege('tags:edit:names'),
canEditCategory: api.hasPrivilege('tags:edit:category'),
canEditImplications: api.hasPrivilege('tags:edit:implications'),
canEditSuggestions: api.hasPrivilege('tags:edit:suggestions'),
canEditDescription: api.hasPrivilege('tags:edit:description'),
canMerge: api.hasPrivilege('tags:merge'),
canDelete: api.hasPrivilege('tags:delete'),
categories: categories,
escapeColons: uri.escapeColons,
});
this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('submit', e => this._evtUpdate(e));
this._view.addEventListener('merge', e => this._evtMerge(e));
this._view.addEventListener('delete', e => this._evtDelete(e));
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
);
}
_evtChange(e) {
@ -68,9 +85,10 @@ class TagController {
misc.disableExitConfirmation();
if (this._name !== e.detail.tag.names[0]) {
router.replace(
uri.formatClientLink('tag', e.detail.tag.names[0], section),
uri.formatClientLink("tag", e.detail.tag.names[0], section),
null,
false);
false
);
}
}
@ -86,59 +104,69 @@ class TagController {
if (e.detail.description !== undefined) {
e.detail.tag.description = e.detail.description;
}
e.detail.tag.save().then(() => {
this._view.showSuccess('Tag saved.');
this._view.enableForm();
}, error => {
this._view.showError(error.message);
this._view.enableForm();
});
e.detail.tag.save().then(
() => {
this._view.showSuccess("Tag saved.");
this._view.enableForm();
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
_evtMerge(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.tag
.merge(e.detail.targetTagName, e.detail.addAlias)
.then(() => {
this._view.showSuccess('Tag merged.');
e.detail.tag.merge(e.detail.targetTagName, e.detail.addAlias).then(
() => {
this._view.showSuccess("Tag merged.");
this._view.enableForm();
router.replace(
uri.formatClientLink(
'tag', e.detail.targetTagName, 'merge'),
"tag",
e.detail.targetTagName,
"merge"
),
null,
false);
}, error => {
false
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
});
}
);
}
_evtDelete(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.tag.delete()
.then(() => {
const ctx = router.show(uri.formatClientLink('tags'));
ctx.controller.showSuccess('Tag deleted.');
}, error => {
e.detail.tag.delete().then(
() => {
const ctx = router.show(uri.formatClientLink("tags"));
ctx.controller.showSuccess("Tag deleted.");
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
});
}
);
}
}
module.exports = router => {
router.enter(['tag', ':name', 'edit'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'edit');
module.exports = (router) => {
router.enter(["tag", ":name", "edit"], (ctx, next) => {
ctx.controller = new TagController(ctx, "edit");
});
router.enter(['tag', ':name', 'merge'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'merge');
router.enter(["tag", ":name", "merge"], (ctx, next) => {
ctx.controller = new TagController(ctx, "merge");
});
router.enter(['tag', ':name', 'delete'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'delete');
router.enter(["tag", ":name", "delete"], (ctx, next) => {
ctx.controller = new TagController(ctx, "delete");
});
router.enter(['tag', ':name'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'summary');
router.enter(["tag", ":name"], (ctx, next) => {
ctx.controller = new TagController(ctx, "summary");
});
};

View file

@ -1,46 +1,47 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const TagList = require('../models/tag_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const TagsHeaderView = require('../views/tags_header_view.js');
const TagsPageView = require('../views/tags_page_view.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const TagList = require("../models/tag_list.js");
const topNavigation = require("../models/top_navigation.js");
const PageController = require("../controllers/page_controller.js");
const TagsHeaderView = require("../views/tags_header_view.js");
const TagsPageView = require("../views/tags_page_view.js");
const EmptyView = require("../views/empty_view.js");
const fields = [
'names',
'suggestions',
'implications',
'creationTime',
'usages',
'category'
"names",
"suggestions",
"implications",
"creationTime",
"usages",
"category",
];
class TagListController {
constructor(ctx) {
this._pageController = new PageController();
if (!api.hasPrivilege('tags:list')) {
if (!api.hasPrivilege("tags:list")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view tags.');
this._view.showError("You don't have privileges to view tags.");
return;
}
this._ctx = ctx;
topNavigation.activate('tags');
topNavigation.setTitle('Listing tags');
topNavigation.activate("tags");
topNavigation.setTitle("Listing tags");
this._headerView = new TagsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters,
canEditTagCategories: api.hasPrivilege('tagCategories:edit'),
canEditTagCategories: api.hasPrivilege("tagCategories:edit"),
});
this._headerView.addEventListener(
'navigate', e => this._evtNavigate(e));
this._headerView.addEventListener("navigate", (e) =>
this._evtNavigate(e)
);
this._syncPageController();
}
@ -55,7 +56,8 @@ class TagListController {
_evtNavigate(e) {
router.showNoDispatch(
uri.formatClientLink('tags', e.detail.parameters));
uri.formatClientLink("tags", e.detail.parameters)
);
Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController();
}
@ -65,25 +67,29 @@ class TagListController {
parameters: this._ctx.parameters,
defaultLimit: 50,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign(
{}, this._ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('tags', parameters);
const parameters = Object.assign({}, this._ctx.parameters, {
offset: offset,
limit: limit,
});
return uri.formatClientLink("tags", parameters);
},
requestPage: (offset, limit) => {
return TagList.search(
this._ctx.parameters.query, offset, limit, fields);
this._ctx.parameters.query,
offset,
limit,
fields
);
},
pageRenderer: pageCtx => {
pageRenderer: (pageCtx) => {
return new TagsPageView(pageCtx);
},
});
}
}
module.exports = router => {
router.enter(
['tags'],
(ctx, next) => {
ctx.controller = new TagListController(ctx);
});
module.exports = (router) => {
router.enter(["tags"], (ctx, next) => {
ctx.controller = new TagListController(ctx);
});
};

View file

@ -1,19 +1,20 @@
'use strict';
"use strict";
const api = require('../api.js');
const topNavigation = require('../models/top_navigation.js');
const TopNavigationView = require('../views/top_navigation_view.js');
const api = require("../api.js");
const topNavigation = require("../models/top_navigation.js");
const TopNavigationView = require("../views/top_navigation_view.js");
class TopNavigationController {
constructor() {
api.fetchConfig().then(() => {
this._topNavigationView = new TopNavigationView();
topNavigation.addEventListener(
'activate', e => this._evtActivate(e));
topNavigation.addEventListener("activate", (e) =>
this._evtActivate(e)
);
api.addEventListener('login', e => this._evtAuthChange(e));
api.addEventListener('logout', e => this._evtAuthChange(e));
api.addEventListener("login", (e) => this._evtAuthChange(e));
api.addEventListener("logout", (e) => this._evtAuthChange(e));
this._render();
});
@ -28,37 +29,38 @@ class TopNavigationController {
}
_updateNavigationFromPrivileges() {
topNavigation.get('account').url = 'user/' + api.userName;
topNavigation.get('account').imageUrl =
api.user ? api.user.avatarUrl : null;
topNavigation.get("account").url = "user/" + api.userName;
topNavigation.get("account").imageUrl = api.user
? api.user.avatarUrl
: null;
topNavigation.showAll();
if (!api.hasPrivilege('posts:list')) {
topNavigation.hide('posts');
if (!api.hasPrivilege("posts:list")) {
topNavigation.hide("posts");
}
if (!api.hasPrivilege('posts:create')) {
topNavigation.hide('upload');
if (!api.hasPrivilege("posts:create")) {
topNavigation.hide("upload");
}
if (!api.hasPrivilege('comments:list')) {
topNavigation.hide('comments');
if (!api.hasPrivilege("comments:list")) {
topNavigation.hide("comments");
}
if (!api.hasPrivilege('tags:list')) {
topNavigation.hide('tags');
if (!api.hasPrivilege("tags:list")) {
topNavigation.hide("tags");
}
if (!api.hasPrivilege('users:list')) {
topNavigation.hide('users');
if (!api.hasPrivilege("users:list")) {
topNavigation.hide("users");
}
if (api.isLoggedIn()) {
if (!api.hasPrivilege('users:create:any')) {
topNavigation.hide('register');
if (!api.hasPrivilege("users:create:any")) {
topNavigation.hide("register");
}
topNavigation.hide('login');
topNavigation.hide("login");
} else {
if (!api.hasPrivilege('users:create:self')) {
topNavigation.hide('register');
if (!api.hasPrivilege("users:create:self")) {
topNavigation.hide("register");
}
topNavigation.hide('account');
topNavigation.hide('logout');
topNavigation.hide("account");
topNavigation.hide("logout");
}
}
@ -66,10 +68,11 @@ class TopNavigationController {
this._updateNavigationFromPrivileges();
this._topNavigationView.render({
items: topNavigation.getAll(),
name: api.getName()
name: api.getName(),
});
this._topNavigationView.activate(
topNavigation.activeItem ? topNavigation.activeItem.key : '');
topNavigation.activeItem ? topNavigation.activeItem.key : ""
);
}
}

View file

@ -1,23 +1,25 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
const User = require('../models/user.js');
const UserToken = require('../models/user_token.js');
const topNavigation = require('../models/top_navigation.js');
const UserView = require('../views/user_view.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const misc = require("../util/misc.js");
const views = require("../util/views.js");
const User = require("../models/user.js");
const UserToken = require("../models/user_token.js");
const topNavigation = require("../models/top_navigation.js");
const UserView = require("../views/user_view.js");
const EmptyView = require("../views/empty_view.js");
class UserController {
constructor(ctx, section) {
const userName = ctx.parameters.name;
if (!api.hasPrivilege('users:view') &&
!api.isLoggedIn({name: userName})) {
if (
!api.hasPrivilege("users:view") &&
!api.isLoggedIn({ name: userName })
) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view users.');
this._view.showError("You don't have privileges to view users.");
return;
}
@ -25,100 +27,129 @@ class UserController {
this._errorMessages = [];
let userTokenPromise = Promise.resolve([]);
if (section === 'list-tokens') {
userTokenPromise = UserToken.get(userName)
.then(userTokens => {
return userTokens.map(token => {
token.isCurrentAuthToken = api.isCurrentAuthToken(token);
if (section === "list-tokens") {
userTokenPromise = UserToken.get(userName).then(
(userTokens) => {
return userTokens.map((token) => {
token.isCurrentAuthToken = api.isCurrentAuthToken(
token
);
return token;
});
}, error => {
},
(error) => {
return [];
});
}
);
}
topNavigation.setTitle('User ' + userName);
Promise.all([
userTokenPromise,
User.get(userName)
]).then(responses => {
const [userTokens, user] = responses;
const isLoggedIn = api.isLoggedIn(user);
const infix = isLoggedIn ? 'self' : 'any';
topNavigation.setTitle("User " + userName);
Promise.all([userTokenPromise, User.get(userName)]).then(
(responses) => {
const [userTokens, user] = responses;
const isLoggedIn = api.isLoggedIn(user);
const infix = isLoggedIn ? "self" : "any";
this._name = userName;
user.addEventListener('change', e => this._evtSaved(e, section));
this._name = userName;
user.addEventListener("change", (e) =>
this._evtSaved(e, section)
);
const myRankIndex = api.user ?
api.allRanks.indexOf(api.user.rank) :
0;
let ranks = {};
for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) {
if (rankIdentifier === 'anonymous') {
continue;
const myRankIndex = api.user
? api.allRanks.indexOf(api.user.rank)
: 0;
let ranks = {};
for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) {
if (rankIdentifier === "anonymous") {
continue;
}
if (rankIdx > myRankIndex) {
continue;
}
ranks[rankIdentifier] = api.rankNames.get(rankIdentifier);
}
if (rankIdx > myRankIndex) {
continue;
if (isLoggedIn) {
topNavigation.activate("account");
} else {
topNavigation.activate("users");
}
ranks[rankIdentifier] = api.rankNames.get(rankIdentifier);
this._view = new UserView({
user: user,
section: section,
isLoggedIn: isLoggedIn,
canEditName: api.hasPrivilege(`users:edit:${infix}:name`),
canEditPassword: api.hasPrivilege(
`users:edit:${infix}:pass`
),
canEditEmail: api.hasPrivilege(
`users:edit:${infix}:email`
),
canEditRank: api.hasPrivilege(`users:edit:${infix}:rank`),
canEditAvatar: api.hasPrivilege(
`users:edit:${infix}:avatar`
),
canEditAnything: api.hasPrivilege(`users:edit:${infix}`),
canListTokens: api.hasPrivilege(
`userTokens:list:${infix}`
),
canCreateToken: api.hasPrivilege(
`userTokens:create:${infix}`
),
canEditToken: api.hasPrivilege(`userTokens:edit:${infix}`),
canDeleteToken: api.hasPrivilege(
`userTokens:delete:${infix}`
),
canDelete: api.hasPrivilege(`users:delete:${infix}`),
ranks: ranks,
tokens: userTokens,
});
this._view.addEventListener("change", (e) =>
this._evtChange(e)
);
this._view.addEventListener("submit", (e) =>
this._evtUpdate(e)
);
this._view.addEventListener("delete", (e) =>
this._evtDelete(e)
);
this._view.addEventListener("create-token", (e) =>
this._evtCreateToken(e)
);
this._view.addEventListener("delete-token", (e) =>
this._evtDeleteToken(e)
);
this._view.addEventListener("update-token", (e) =>
this._evtUpdateToken(e)
);
for (let message of this._successMessages) {
this.showSuccess(message);
}
for (let message of this._errorMessages) {
this.showError(message);
}
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
if (isLoggedIn) {
topNavigation.activate('account');
} else {
topNavigation.activate('users');
}
this._view = new UserView({
user: user,
section: section,
isLoggedIn: isLoggedIn,
canEditName: api.hasPrivilege(`users:edit:${infix}:name`),
canEditPassword: api.hasPrivilege(`users:edit:${infix}:pass`),
canEditEmail: api.hasPrivilege(`users:edit:${infix}:email`),
canEditRank: api.hasPrivilege(`users:edit:${infix}:rank`),
canEditAvatar: api.hasPrivilege(`users:edit:${infix}:avatar`),
canEditAnything: api.hasPrivilege(`users:edit:${infix}`),
canListTokens: api.hasPrivilege(`userTokens:list:${infix}`),
canCreateToken: api.hasPrivilege(`userTokens:create:${infix}`),
canEditToken: api.hasPrivilege(`userTokens:edit:${infix}`),
canDeleteToken: api.hasPrivilege(`userTokens:delete:${infix}`),
canDelete: api.hasPrivilege(`users:delete:${infix}`),
ranks: ranks,
tokens: userTokens,
});
this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('submit', e => this._evtUpdate(e));
this._view.addEventListener('delete', e => this._evtDelete(e));
this._view.addEventListener('create-token', e => this._evtCreateToken(e));
this._view.addEventListener('delete-token', e => this._evtDeleteToken(e));
this._view.addEventListener('update-token', e => this._evtUpdateToken(e));
for (let message of this._successMessages) {
this.showSuccess(message);
}
for (let message of this._errorMessages) {
this.showError(message);
}
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
);
}
showSuccess(message) {
if (typeof this._view === 'undefined') {
this._successMessages.push(message)
if (typeof this._view === "undefined") {
this._successMessages.push(message);
} else {
this._view.showSuccess(message);
}
}
showError(message) {
if (typeof this._view === 'undefined') {
this._errorMessages.push(message)
if (typeof this._view === "undefined") {
this._errorMessages.push(message);
} else {
this._view.showError(message);
}
@ -132,9 +163,10 @@ class UserController {
misc.disableExitConfirmation();
if (this._name !== e.detail.user.name) {
router.replace(
uri.formatClientLink('user', e.detail.user.name, section),
uri.formatClientLink("user", e.detail.user.name, section),
null,
false);
false
);
}
}
@ -142,7 +174,7 @@ class UserController {
this._view.clearMessages();
this._view.disableForm();
const isLoggedIn = api.isLoggedIn(e.detail.user);
const infix = isLoggedIn ? 'self' : 'any';
const infix = isLoggedIn ? "self" : "any";
if (e.detail.name !== undefined) {
e.detail.user.name = e.detail.name;
@ -165,72 +197,105 @@ class UserController {
}
}
e.detail.user.save().then(() => {
return isLoggedIn ?
api.login(
e.detail.name || api.userName,
e.detail.password || api.userPassword,
false) :
Promise.resolve();
}).then(() => {
this._view.showSuccess('Settings updated.');
this._view.enableForm();
}, error => {
this._view.showError(error.message);
this._view.enableForm();
});
e.detail.user
.save()
.then(() => {
return isLoggedIn
? api.login(
e.detail.name || api.userName,
e.detail.password || api.userPassword,
false
)
: Promise.resolve();
})
.then(
() => {
this._view.showSuccess("Settings updated.");
this._view.enableForm();
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
_evtDelete(e) {
this._view.clearMessages();
this._view.disableForm();
const isLoggedIn = api.isLoggedIn(e.detail.user);
e.detail.user.delete()
.then(() => {
e.detail.user.delete().then(
() => {
if (isLoggedIn) {
api.forget();
api.logout();
}
if (api.hasPrivilege('users:list')) {
const ctx = router.show(uri.formatClientLink('users'));
ctx.controller.showSuccess('Account deleted.');
if (api.hasPrivilege("users:list")) {
const ctx = router.show(uri.formatClientLink("users"));
ctx.controller.showSuccess("Account deleted.");
} else {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('Account deleted.');
ctx.controller.showSuccess("Account deleted.");
}
}, error => {
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
});
}
);
}
_evtCreateToken(e) {
this._view.clearMessages();
this._view.disableForm();
UserToken.create(e.detail.user.name, e.detail.note, e.detail.expirationTime)
.then(response => {
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
ctx.controller.showSuccess('Token ' + response.token + ' created.');
}, error => {
UserToken.create(
e.detail.user.name,
e.detail.note,
e.detail.expirationTime
).then(
(response) => {
const ctx = router.show(
uri.formatClientLink(
"user",
e.detail.user.name,
"list-tokens"
)
);
ctx.controller.showSuccess(
"Token " + response.token + " created."
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
});
}
);
}
_evtDeleteToken(e) {
this._view.clearMessages();
this._view.disableForm();
if (api.isCurrentAuthToken(e.detail.userToken)) {
router.show(uri.formatClientLink('logout'));
router.show(uri.formatClientLink("logout"));
} else {
e.detail.userToken.delete(e.detail.user.name)
.then(() => {
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
ctx.controller.showSuccess('Token ' + e.detail.userToken.token + ' deleted.');
}, error => {
e.detail.userToken.delete(e.detail.user.name).then(
() => {
const ctx = router.show(
uri.formatClientLink(
"user",
e.detail.user.name,
"list-tokens"
)
);
ctx.controller.showSuccess(
"Token " + e.detail.userToken.token + " deleted."
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
});
}
);
}
}
@ -242,27 +307,38 @@ class UserController {
e.detail.userToken.note = e.detail.note;
}
e.detail.userToken.save(e.detail.user.name).then(response => {
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
ctx.controller.showSuccess('Token ' + response.token + ' updated.');
}, error => {
this._view.showError(error.message);
this._view.enableForm();
});
e.detail.userToken.save(e.detail.user.name).then(
(response) => {
const ctx = router.show(
uri.formatClientLink(
"user",
e.detail.user.name,
"list-tokens"
)
);
ctx.controller.showSuccess(
"Token " + response.token + " updated."
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
}
module.exports = router => {
router.enter(['user', ':name'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'summary');
module.exports = (router) => {
router.enter(["user", ":name"], (ctx, next) => {
ctx.controller = new UserController(ctx, "summary");
});
router.enter(['user', ':name', 'edit'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'edit');
router.enter(["user", ":name", "edit"], (ctx, next) => {
ctx.controller = new UserController(ctx, "edit");
});
router.enter(['user', ':name', 'list-tokens'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'list-tokens');
router.enter(["user", ":name", "list-tokens"], (ctx, next) => {
ctx.controller = new UserController(ctx, "list-tokens");
});
router.enter(['user', ':name', 'delete'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'delete');
router.enter(["user", ":name", "delete"], (ctx, next) => {
ctx.controller = new UserController(ctx, "delete");
});
};

View file

@ -1,27 +1,27 @@
'use strict';
"use strict";
const api = require('../api.js');
const router = require('../router.js');
const uri = require('../util/uri.js');
const UserList = require('../models/user_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const UsersHeaderView = require('../views/users_header_view.js');
const UsersPageView = require('../views/users_page_view.js');
const EmptyView = require('../views/empty_view.js');
const api = require("../api.js");
const router = require("../router.js");
const uri = require("../util/uri.js");
const UserList = require("../models/user_list.js");
const topNavigation = require("../models/top_navigation.js");
const PageController = require("../controllers/page_controller.js");
const UsersHeaderView = require("../views/users_header_view.js");
const UsersPageView = require("../views/users_page_view.js");
const EmptyView = require("../views/empty_view.js");
class UserListController {
constructor(ctx) {
this._pageController = new PageController();
if (!api.hasPrivilege('users:list')) {
if (!api.hasPrivilege("users:list")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view users.');
this._view.showError("You don't have privileges to view users.");
return;
}
topNavigation.activate('users');
topNavigation.setTitle('Listing users');
topNavigation.activate("users");
topNavigation.setTitle("Listing users");
this._ctx = ctx;
@ -29,8 +29,9 @@ class UserListController {
hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters,
});
this._headerView.addEventListener(
'navigate', e => this._evtNavigate(e));
this._headerView.addEventListener("navigate", (e) =>
this._evtNavigate(e)
);
this._syncPageController();
}
@ -41,7 +42,8 @@ class UserListController {
_evtNavigate(e) {
router.showNoDispatch(
uri.formatClientLink('users', e.detail.parameters));
uri.formatClientLink("users", e.detail.parameters)
);
Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController();
}
@ -51,17 +53,22 @@ class UserListController {
parameters: this._ctx.parameters,
defaultLimit: 30,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign(
{}, this._ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('users', parameters);
const parameters = Object.assign({}, this._ctx.parameters, {
offset: offset,
limit: limit,
});
return uri.formatClientLink("users", parameters);
},
requestPage: (offset, limit) => {
return UserList.search(
this._ctx.parameters.query, offset, limit);
this._ctx.parameters.query,
offset,
limit
);
},
pageRenderer: pageCtx => {
pageRenderer: (pageCtx) => {
Object.assign(pageCtx, {
canViewUsers: api.hasPrivilege('users:view'),
canViewUsers: api.hasPrivilege("users:view"),
});
return new UsersPageView(pageCtx);
},
@ -69,10 +76,8 @@ class UserListController {
}
}
module.exports = router => {
router.enter(
['users'],
(ctx, next) => {
ctx.controller = new UserListController(ctx);
});
module.exports = (router) => {
router.enter(["users"], (ctx, next) => {
ctx.controller = new UserListController(ctx);
});
};

View file

@ -1,25 +1,25 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const User = require('../models/user.js');
const topNavigation = require('../models/top_navigation.js');
const RegistrationView = require('../views/registration_view.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const User = require("../models/user.js");
const topNavigation = require("../models/top_navigation.js");
const RegistrationView = require("../views/registration_view.js");
const EmptyView = require("../views/empty_view.js");
class UserRegistrationController {
constructor() {
if (!api.hasPrivilege('users:create:self')) {
if (!api.hasPrivilege("users:create:self")) {
this._view = new EmptyView();
this._view.showError('Registration is closed.');
this._view.showError("Registration is closed.");
return;
}
topNavigation.activate('register');
topNavigation.setTitle('Registration');
topNavigation.activate("register");
topNavigation.setTitle("Registration");
this._view = new RegistrationView();
this._view.addEventListener('submit', e => this._evtRegister(e));
this._view.addEventListener("submit", (e) => this._evtRegister(e));
}
_evtRegister(e) {
@ -30,30 +30,35 @@ class UserRegistrationController {
user.email = e.detail.email;
user.password = e.detail.password;
const isLoggedIn = api.isLoggedIn();
user.save().then(() => {
if (isLoggedIn) {
return Promise.resolve();
} else {
api.forget();
return api.login(e.detail.name, e.detail.password, false);
}
}).then(() => {
if (isLoggedIn) {
const ctx = router.show(uri.formatClientLink('users'));
ctx.controller.showSuccess('User added!');
} else {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('Welcome aboard!');
}
}, error => {
this._view.showError(error.message);
this._view.enableForm();
});
user.save()
.then(() => {
if (isLoggedIn) {
return Promise.resolve();
} else {
api.forget();
return api.login(e.detail.name, e.detail.password, false);
}
})
.then(
() => {
if (isLoggedIn) {
const ctx = router.show(uri.formatClientLink("users"));
ctx.controller.showSuccess("User added!");
} else {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess("Welcome aboard!");
}
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
}
module.exports = router => {
router.enter(['register'], (ctx, next) => {
module.exports = (router) => {
router.enter(["register"], (ctx, next) => {
new UserRegistrationController();
});
};

View file

@ -1,6 +1,6 @@
'use strict';
"use strict";
const views = require('../util/views.js');
const views = require("../util/views.js");
const KEY_TAB = 9;
const KEY_RETURN = 13;
@ -10,14 +10,14 @@ const KEY_UP = 38;
const KEY_DOWN = 40;
function _getSelectionStart(input) {
if ('selectionStart' in 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);
sel.moveStart("character", -input.value.length);
return sel.text.length - selLen;
}
return 0;
@ -27,18 +27,22 @@ class AutoCompleteControl {
constructor(sourceInputNode, options) {
this._sourceInputNode = sourceInputNode;
this._options = {};
Object.assign(this._options, {
verticalShift: 2,
maxResults: 15,
getTextToFind: () => {
const value = sourceInputNode.value;
const start = _getSelectionStart(sourceInputNode);
return value.substring(0, start).replace(/.*\s+/, '');
Object.assign(
this._options,
{
verticalShift: 2,
maxResults: 15,
getTextToFind: () => {
const value = sourceInputNode.value;
const start = _getSelectionStart(sourceInputNode);
return value.substring(0, start).replace(/.*\s+/, "");
},
confirm: null,
delete: null,
getMatches: null,
},
confirm: null,
delete: null,
getMatches: null,
}, options);
options
);
this._showTimeout = null;
this._results = [];
@ -49,22 +53,22 @@ class AutoCompleteControl {
hide() {
window.clearTimeout(this._showTimeout);
this._suggestionDiv.style.display = 'none';
this._suggestionDiv.style.display = "none";
this._isVisible = false;
}
replaceSelectedText(result, addSpace) {
const start = _getSelectionStart(this._sourceInputNode);
let prefix = '';
let prefix = "";
let suffix = this._sourceInputNode.value.substring(start);
let middle = this._sourceInputNode.value.substring(0, start);
const index = middle.lastIndexOf(' ');
const index = middle.lastIndexOf(" ");
if (index !== -1) {
prefix = this._sourceInputNode.value.substring(0, index + 1);
middle = this._sourceInputNode.value.substring(index + 1);
}
this._sourceInputNode.value = (
prefix + result.toString() + ' ' + suffix.trimLeft());
this._sourceInputNode.value =
prefix + result.toString() + " " + suffix.trimLeft();
if (!addSpace) {
this._sourceInputNode.value = this._sourceInputNode.value.trim();
}
@ -86,7 +90,7 @@ class AutoCompleteControl {
}
_show() {
this._suggestionDiv.style.display = 'block';
this._suggestionDiv.style.display = "block";
this._isVisible = true;
}
@ -101,29 +105,32 @@ class AutoCompleteControl {
_install() {
if (!this._sourceInputNode) {
throw new Error('Input element was not found');
throw new Error("Input element was not found");
}
if (this._sourceInputNode.getAttribute('data-autocomplete')) {
if (this._sourceInputNode.getAttribute("data-autocomplete")) {
throw new Error(
'Autocompletion was already added for this element');
"Autocompletion was already added for this element"
);
}
this._sourceInputNode.setAttribute('data-autocomplete', true);
this._sourceInputNode.setAttribute('autocomplete', 'off');
this._sourceInputNode.setAttribute("data-autocomplete", true);
this._sourceInputNode.setAttribute("autocomplete", "off");
this._sourceInputNode.addEventListener(
'keydown', e => this._evtKeyDown(e));
this._sourceInputNode.addEventListener(
'blur', e => this._evtBlur(e));
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');
'<div class="autocomplete"><ul></ul></div>'
);
this._suggestionList = this._suggestionDiv.querySelector("ul");
document.body.appendChild(this._suggestionDiv);
views.monitorNodeRemoval(
this._sourceInputNode, () => {
this._uninstall();
});
views.monitorNodeRemoval(this._sourceInputNode, () => {
this._uninstall();
});
}
_uninstall() {
@ -174,10 +181,9 @@ class AutoCompleteControl {
func();
} else {
window.clearTimeout(this._showTimeout);
this._showTimeout = window.setTimeout(
() => {
this._showOrHide();
}, 250);
this._showTimeout = window.setTimeout(() => {
this._showOrHide();
}, 250);
}
}
@ -196,9 +202,11 @@ class AutoCompleteControl {
}
_selectPrevious() {
this._select(this._activeResult === -1 ?
this._results.length - 1 :
this._activeResult - 1);
this._select(
this._activeResult === -1
? this._results.length - 1
: this._activeResult - 1
);
}
_selectNext() {
@ -206,15 +214,18 @@ class AutoCompleteControl {
}
_select(newActiveResult) {
this._activeResult =
newActiveResult.between(0, this._results.length - 1, true) ?
newActiveResult :
-1;
this._activeResult = newActiveResult.between(
0,
this._results.length - 1,
true
)
? newActiveResult
: -1;
this._refreshActiveResult();
}
_updateResults(textToFind) {
this._options.getMatches(textToFind).then(matches => {
this._options.getMatches(textToFind).then((matches) => {
const oldResults = this._results.slice();
this._results = matches.slice(0, this._options.maxResults);
const oldResultsHash = JSON.stringify(oldResults);
@ -237,34 +248,30 @@ class AutoCompleteControl {
}
for (let [resultIndex, resultItem] of this._results.entries()) {
let resultIndexWorkaround = resultIndex;
const listItem = document.createElement('li');
const link = document.createElement('a');
const listItem = document.createElement("li");
const link = document.createElement("a");
link.innerHTML = resultItem.caption;
link.setAttribute('href', '');
link.setAttribute('data-key', resultItem.value);
link.addEventListener(
'mouseenter',
e => {
e.preventDefault();
this._activeResult = resultIndexWorkaround;
this._refreshActiveResult();
});
link.addEventListener(
'mousedown',
e => {
e.preventDefault();
this._activeResult = resultIndexWorkaround;
this._confirm(this._getActiveSuggestion());
this.hide();
});
link.setAttribute("href", "");
link.setAttribute("data-key", resultItem.value);
link.addEventListener("mouseenter", (e) => {
e.preventDefault();
this._activeResult = resultIndexWorkaround;
this._refreshActiveResult();
});
link.addEventListener("mousedown", (e) => {
e.preventDefault();
this._activeResult = resultIndexWorkaround;
this._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._suggestionDiv.style.left = "-9999px";
this._suggestionDiv.style.top = "-9999px";
this._show();
const verticalShift = this._options.verticalShift;
const inputRect = this._sourceInputNode.getBoundingClientRect();
@ -275,17 +282,23 @@ class AutoCompleteControl {
// 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;
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;
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) {
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();
@ -295,19 +308,19 @@ class AutoCompleteControl {
}
}
this._suggestionDiv.style.left = x + 'px';
this._suggestionDiv.style.top = y + 'px';
this._suggestionDiv.style.left = x + "px";
this._suggestionDiv.style.top = y + "px";
}
_refreshActiveResult() {
let activeItem = this._suggestionList.querySelector('li.active');
let activeItem = this._suggestionList.querySelector("li.active");
if (activeItem) {
activeItem.classList.remove('active');
activeItem.classList.remove("active");
}
if (this._activeResult >= 0) {
const allItems = this._suggestionList.querySelectorAll('li');
const allItems = this._suggestionList.querySelectorAll("li");
activeItem = allItems[this._activeResult];
activeItem.classList.add('active');
activeItem.classList.add("active");
}
}
}

View file

@ -1,12 +1,12 @@
'use strict';
"use strict";
const api = require('../api.js');
const misc = require('../util/misc.js');
const events = require('../events.js');
const views = require('../util/views.js');
const api = require("../api.js");
const misc = require("../util/misc.js");
const events = require("../events.js");
const views = require("../util/views.js");
const template = views.getTemplate('comment');
const scoreTemplate = views.getTemplate('score');
const template = views.getTemplate("comment");
const scoreTemplate = views.getTemplate("score");
class CommentControl extends events.EventTarget {
constructor(hostNode, comment, onlyEditing) {
@ -16,104 +16,111 @@ class CommentControl extends events.EventTarget {
this._onlyEditing = onlyEditing;
if (comment) {
comment.addEventListener(
'change', e => this._evtChange(e));
comment.addEventListener(
'changeScore', e => this._evtChangeScore(e));
comment.addEventListener("change", (e) => this._evtChange(e));
comment.addEventListener("changeScore", (e) =>
this._evtChangeScore(e)
);
}
const isLoggedIn = comment && api.isLoggedIn(comment.user);
const infix = isLoggedIn ? 'own' : 'any';
views.replaceContent(this._hostNode, template({
comment: comment,
user: comment ? comment.user : api.user,
canViewUsers: api.hasPrivilege('users:view'),
canEditComment: api.hasPrivilege(`comments:edit:${infix}`),
canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`),
onlyEditing: onlyEditing,
}));
const infix = isLoggedIn ? "own" : "any";
views.replaceContent(
this._hostNode,
template({
comment: comment,
user: comment ? comment.user : api.user,
canViewUsers: api.hasPrivilege("users:view"),
canEditComment: api.hasPrivilege(`comments:edit:${infix}`),
canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`),
onlyEditing: onlyEditing,
})
);
if (this._editButtonNodes) {
for (let node of this._editButtonNodes) {
node.addEventListener('click', e => this._evtEditClick(e));
node.addEventListener("click", (e) => this._evtEditClick(e));
}
}
if (this._deleteButtonNode) {
this._deleteButtonNode.addEventListener(
'click', e => this._evtDeleteClick(e));
this._deleteButtonNode.addEventListener("click", (e) =>
this._evtDeleteClick(e)
);
}
if (this._previewEditingButtonNode) {
this._previewEditingButtonNode.addEventListener(
'click', e => this._evtPreviewEditingClick(e));
this._previewEditingButtonNode.addEventListener("click", (e) =>
this._evtPreviewEditingClick(e)
);
}
if (this._saveChangesButtonNode) {
this._saveChangesButtonNode.addEventListener(
'click', e => this._evtSaveChangesClick(e));
this._saveChangesButtonNode.addEventListener("click", (e) =>
this._evtSaveChangesClick(e)
);
}
if (this._cancelEditingButtonNode) {
this._cancelEditingButtonNode.addEventListener(
'click', e => this._evtCancelEditingClick(e));
this._cancelEditingButtonNode.addEventListener("click", (e) =>
this._evtCancelEditingClick(e)
);
}
this._installScore();
if (onlyEditing) {
this._selectNav('edit');
this._selectTab('edit');
this._selectNav("edit");
this._selectTab("edit");
} else {
this._selectNav('readonly');
this._selectTab('preview');
this._selectNav("readonly");
this._selectTab("preview");
}
}
get _formNode() {
return this._hostNode.querySelector('form');
return this._hostNode.querySelector("form");
}
get _scoreContainerNode() {
return this._hostNode.querySelector('.score-container');
return this._hostNode.querySelector(".score-container");
}
get _editButtonNodes() {
return this._hostNode.querySelectorAll('li.edit>a, a.edit');
return this._hostNode.querySelectorAll("li.edit>a, a.edit");
}
get _previewEditingButtonNode() {
return this._hostNode.querySelector('li.preview>a');
return this._hostNode.querySelector("li.preview>a");
}
get _deleteButtonNode() {
return this._hostNode.querySelector('.delete');
return this._hostNode.querySelector(".delete");
}
get _upvoteButtonNode() {
return this._hostNode.querySelector('.upvote');
return this._hostNode.querySelector(".upvote");
}
get _downvoteButtonNode() {
return this._hostNode.querySelector('.downvote');
return this._hostNode.querySelector(".downvote");
}
get _saveChangesButtonNode() {
return this._hostNode.querySelector('.save-changes');
return this._hostNode.querySelector(".save-changes");
}
get _cancelEditingButtonNode() {
return this._hostNode.querySelector('.cancel-editing');
return this._hostNode.querySelector(".cancel-editing");
}
get _textareaNode() {
return this._hostNode.querySelector('.tab.edit textarea');
return this._hostNode.querySelector(".tab.edit textarea");
}
get _contentNode() {
return this._hostNode.querySelector('.tab.preview .comment-content');
return this._hostNode.querySelector(".tab.preview .comment-content");
}
get _heightKeeperNode() {
return this._hostNode.querySelector('.keep-height');
return this._hostNode.querySelector(".keep-height");
}
_installScore() {
@ -122,32 +129,35 @@ class CommentControl extends events.EventTarget {
scoreTemplate({
score: this._comment ? this._comment.score : 0,
ownScore: this._comment ? this._comment.ownScore : 0,
canScore: api.hasPrivilege('comments:score'),
}));
canScore: api.hasPrivilege("comments:score"),
})
);
if (this._upvoteButtonNode) {
this._upvoteButtonNode.addEventListener(
'click', e => this._evtScoreClick(e, 1));
this._upvoteButtonNode.addEventListener("click", (e) =>
this._evtScoreClick(e, 1)
);
}
if (this._downvoteButtonNode) {
this._downvoteButtonNode.addEventListener(
'click', e => this._evtScoreClick(e, -1));
this._downvoteButtonNode.addEventListener("click", (e) =>
this._evtScoreClick(e, -1)
);
}
}
enterEditMode() {
this._selectNav('edit');
this._selectTab('edit');
this._selectNav("edit");
this._selectTab("edit");
}
exitEditMode() {
if (this._onlyEditing) {
this._selectNav('edit');
this._selectTab('edit');
this._setText('');
this._selectNav("edit");
this._selectTab("edit");
this._setText("");
} else {
this._selectNav('readonly');
this._selectTab('preview');
this._selectNav("readonly");
this._selectTab("preview");
this._setText(this._comment.text);
}
this._forgetHeight();
@ -173,27 +183,31 @@ class CommentControl extends events.EventTarget {
_evtScoreClick(e, score) {
e.preventDefault();
if (!api.hasPrivilege('comments:score')) {
if (!api.hasPrivilege("comments:score")) {
return;
}
this.dispatchEvent(new CustomEvent('score', {
detail: {
comment: this._comment,
score: this._comment.ownScore === score ? 0 : score,
},
}));
this.dispatchEvent(
new CustomEvent("score", {
detail: {
comment: this._comment,
score: this._comment.ownScore === score ? 0 : score,
},
})
);
}
_evtDeleteClick(e) {
e.preventDefault();
if (!window.confirm('Are you sure you want to delete this comment?')) {
if (!window.confirm("Are you sure you want to delete this comment?")) {
return;
}
this.dispatchEvent(new CustomEvent('delete', {
detail: {
comment: this._comment,
},
}));
this.dispatchEvent(
new CustomEvent("delete", {
detail: {
comment: this._comment,
},
})
);
}
_evtChange(e) {
@ -206,21 +220,24 @@ class CommentControl extends events.EventTarget {
_evtPreviewEditingClick(e) {
e.preventDefault();
this._contentNode.innerHTML =
misc.formatMarkdown(this._textareaNode.value);
this._selectTab('edit');
this._selectTab('preview');
this._contentNode.innerHTML = misc.formatMarkdown(
this._textareaNode.value
);
this._selectTab("edit");
this._selectTab("preview");
}
_evtSaveChangesClick(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
target: this,
comment: this._comment,
text: this._textareaNode.value,
},
}));
this.dispatchEvent(
new CustomEvent("submit", {
detail: {
target: this,
comment: this._comment,
text: this._textareaNode.value,
},
})
);
}
_evtCancelEditingClick(e) {
@ -234,22 +251,22 @@ class CommentControl extends events.EventTarget {
}
_selectNav(modeName) {
for (let node of this._hostNode.querySelectorAll('nav')) {
node.classList.toggle('active', node.classList.contains(modeName));
for (let node of this._hostNode.querySelectorAll("nav")) {
node.classList.toggle("active", node.classList.contains(modeName));
}
}
_selectTab(tabName) {
this._ensureHeight();
for (let node of this._hostNode.querySelectorAll('.tab, .tabs li')) {
node.classList.toggle('active', node.classList.contains(tabName));
for (let node of this._hostNode.querySelectorAll(".tab, .tabs li")) {
node.classList.toggle("active", node.classList.contains(tabName));
}
}
_ensureHeight() {
this._heightKeeperNode.style.minHeight =
this._heightKeeperNode.getBoundingClientRect().height + 'px';
this._heightKeeperNode.getBoundingClientRect().height + "px";
}
_forgetHeight() {

View file

@ -1,10 +1,10 @@
'use strict';
"use strict";
const events = require('../events.js');
const views = require('../util/views.js');
const CommentControl = require('../controls/comment_control.js');
const events = require("../events.js");
const views = require("../util/views.js");
const CommentControl = require("../controls/comment_control.js");
const template = views.getTemplate('comment-list');
const template = views.getTemplate("comment-list");
class CommentListControl extends events.EventTarget {
constructor(hostNode, comments, reversed) {
@ -13,8 +13,8 @@ class CommentListControl extends events.EventTarget {
this._comments = comments;
this._commentIdToNode = {};
comments.addEventListener('add', e => this._evtAdd(e));
comments.addEventListener('remove', e => this._evtRemove(e));
comments.addEventListener("add", (e) => this._evtAdd(e));
comments.addEventListener("remove", (e) => this._evtRemove(e));
views.replaceContent(this._hostNode, template());
@ -28,16 +28,19 @@ class CommentListControl extends events.EventTarget {
}
get _commentListNode() {
return this._hostNode.querySelector('ul');
return this._hostNode.querySelector("ul");
}
_installCommentNode(comment) {
const commentListItemNode = document.createElement('li');
const commentListItemNode = document.createElement("li");
const commentControl = new CommentControl(
commentListItemNode, comment, false);
events.proxyEvent(commentControl, this, 'submit');
events.proxyEvent(commentControl, this, 'score');
events.proxyEvent(commentControl, this, 'delete');
commentListItemNode,
comment,
false
);
events.proxyEvent(commentControl, this, "submit");
events.proxyEvent(commentControl, this, "score");
events.proxyEvent(commentControl, this, "delete");
this._commentIdToNode[comment.id] = commentListItemNode;
this._commentListNode.appendChild(commentListItemNode);
}

View file

@ -1,26 +1,28 @@
'use strict';
"use strict";
const ICON_CLASS_OPENED = 'fa-chevron-down';
const ICON_CLASS_CLOSED = 'fa-chevron-up';
const ICON_CLASS_OPENED = "fa-chevron-down";
const ICON_CLASS_CLOSED = "fa-chevron-up";
const views = require('../util/views.js');
const views = require("../util/views.js");
const template = views.getTemplate('expander');
const template = views.getTemplate("expander");
class ExpanderControl {
constructor(name, title, nodes) {
this._name = name;
nodes = Array.from(nodes).filter(n => n);
nodes = Array.from(nodes).filter((n) => n);
if (!nodes.length) {
return;
}
const expanderNode = template({title: title});
const toggleLinkNode = expanderNode.querySelector('a');
const toggleIconNode = expanderNode.querySelector('i');
const expanderContentNode = expanderNode.querySelector('div');
toggleLinkNode.addEventListener('click', e => this._evtToggleClick(e));
const expanderNode = template({ title: title });
const toggleLinkNode = expanderNode.querySelector("a");
const toggleIconNode = expanderNode.querySelector("i");
const expanderContentNode = expanderNode.querySelector("div");
toggleLinkNode.addEventListener("click", (e) =>
this._evtToggleClick(e)
);
nodes[0].parentNode.insertBefore(expanderNode, nodes[0]);
@ -32,29 +34,30 @@ class ExpanderControl {
this._toggleIconNode = toggleIconNode;
expanderNode.classList.toggle(
'collapsed',
this._allStates[this._name] === undefined ?
false :
!this._allStates[this._name]);
"collapsed",
this._allStates[this._name] === undefined
? false
: !this._allStates[this._name]
);
this._syncIcon();
}
// eslint-disable-next-line accessor-pairs
set title(newTitle) {
if (this._expanderNode) {
this._expanderNode
.querySelector('header span')
.textContent = newTitle;
this._expanderNode.querySelector(
"header span"
).textContent = newTitle;
}
}
get _isOpened() {
return !this._expanderNode.classList.contains('collapsed');
return !this._expanderNode.classList.contains("collapsed");
}
get _allStates() {
try {
return JSON.parse(localStorage.getItem('expander')) || {};
return JSON.parse(localStorage.getItem("expander")) || {};
} catch (e) {
return {};
}
@ -63,12 +66,12 @@ class ExpanderControl {
_save() {
const newStates = Object.assign({}, this._allStates);
newStates[this._name] = this._isOpened;
localStorage.setItem('expander', JSON.stringify(newStates));
localStorage.setItem("expander", JSON.stringify(newStates));
}
_evtToggleClick(e) {
e.preventDefault();
this._expanderNode.classList.toggle('collapsed');
this._expanderNode.classList.toggle("collapsed");
this._save();
this._syncIcon();
}

View file

@ -1,9 +1,9 @@
'use strict';
"use strict";
const events = require('../events.js');
const views = require('../util/views.js');
const events = require("../events.js");
const views = require("../util/views.js");
const template = views.getTemplate('file-dropper');
const template = views.getTemplate("file-dropper");
const KEY_RETURN = 13;
@ -17,37 +17,42 @@ class FileDropperControl extends events.EventTarget {
allowMultiple: options.allowMultiple,
allowUrls: options.allowUrls,
lock: options.lock,
id: 'file-' + Math.random().toString(36).substring(7),
id: "file-" + Math.random().toString(36).substring(7),
urlPlaceholder:
options.urlPlaceholder || 'Alternatively, paste an URL here.',
options.urlPlaceholder || "Alternatively, paste an URL here.",
});
this._dropperNode = source.querySelector('.file-dropper');
this._urlInputNode = source.querySelector('input[type=text]');
this._urlConfirmButtonNode = source.querySelector('button');
this._fileInputNode = source.querySelector('input[type=file]');
this._fileInputNode.style.display = 'none';
this._dropperNode = source.querySelector(".file-dropper");
this._urlInputNode = source.querySelector("input[type=text]");
this._urlConfirmButtonNode = source.querySelector("button");
this._fileInputNode = source.querySelector("input[type=file]");
this._fileInputNode.style.display = "none";
this._fileInputNode.multiple = 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));
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)
);
if (this._urlInputNode) {
this._urlInputNode.addEventListener(
'keydown', e => this._evtUrlInputKeyDown(e));
this._urlInputNode.addEventListener("keydown", (e) =>
this._evtUrlInputKeyDown(e)
);
}
if (this._urlConfirmButtonNode) {
this._urlConfirmButtonNode.addEventListener(
'click', e => this._evtUrlConfirmButtonClick(e));
this._urlConfirmButtonNode.addEventListener("click", (e) =>
this._evtUrlConfirmButtonClick(e)
);
}
this._originalHtml = this._dropperNode.innerHTML;
@ -56,24 +61,27 @@ class FileDropperControl extends events.EventTarget {
reset() {
this._dropperNode.innerHTML = this._originalHtml;
this.dispatchEvent(new CustomEvent('reset'));
this.dispatchEvent(new CustomEvent("reset"));
}
_emitFiles(files) {
files = Array.from(files);
if (this._options.lock) {
this._dropperNode.innerText =
files.map(file => file.name).join(', ');
this._dropperNode.innerText = files
.map((file) => file.name)
.join(", ");
}
this.dispatchEvent(
new CustomEvent('fileadd', {detail: {files: files}}));
new CustomEvent("fileadd", { detail: { files: files } })
);
}
_emitUrls(urls) {
urls = Array.from(urls).map(url => url.trim());
urls = Array.from(urls).map((url) => url.trim());
if (this._options.lock) {
this._dropperNode.innerText =
urls.map(url => url.split(/\//).reverse()[0]).join(', ');
this._dropperNode.innerText = urls
.map((url) => url.split(/\//).reverse()[0])
.join(", ");
}
for (let url of urls) {
if (!url) {
@ -84,18 +92,20 @@ class FileDropperControl extends events.EventTarget {
return;
}
}
this.dispatchEvent(new CustomEvent('urladd', {detail: {urls: urls}}));
this.dispatchEvent(
new CustomEvent("urladd", { detail: { urls: urls } })
);
}
_evtDragEnter(e) {
this._dropperNode.classList.add('active');
this._dropperNode.classList.add("active");
this._counter++;
}
_evtDragLeave(e) {
this._counter--;
if (this._counter === 0) {
this._dropperNode.classList.remove('active');
this._dropperNode.classList.remove("active");
}
}
@ -109,12 +119,12 @@ class FileDropperControl extends events.EventTarget {
_evtDrop(e) {
e.preventDefault();
this._dropperNode.classList.remove('active');
this._dropperNode.classList.remove("active");
if (!e.dataTransfer.files.length) {
window.alert('Only files are supported.');
window.alert("Only files are supported.");
}
if (!this._options.allowMultiple && e.dataTransfer.files.length > 1) {
window.alert('Cannot select multiple files.');
window.alert("Cannot select multiple files.");
}
this._emitFiles(e.dataTransfer.files);
}
@ -124,16 +134,16 @@ class FileDropperControl extends events.EventTarget {
return;
}
e.preventDefault();
this._dropperNode.classList.remove('active');
this._dropperNode.classList.remove("active");
this._emitUrls(this._urlInputNode.value.split(/[\r\n]/));
this._urlInputNode.value = '';
this._urlInputNode.value = "";
}
_evtUrlConfirmButtonClick(e) {
e.preventDefault();
this._dropperNode.classList.remove('active');
this._dropperNode.classList.remove("active");
this._emitUrls(this._urlInputNode.value.split(/[\r\n]/));
this._urlInputNode.value = '';
this._urlInputNode.value = "";
}
}

View file

@ -1,43 +1,54 @@
'use strict';
"use strict";
const misc = require('../util/misc.js');
const PoolList = require('../models/pool_list.js');
const AutoCompleteControl = require('./auto_complete_control.js');
const misc = require("../util/misc.js");
const PoolList = require("../models/pool_list.js");
const AutoCompleteControl = require("./auto_complete_control.js");
function _poolListToMatches(pools, options) {
return [...pools].sort((pool1, pool2) => {
return pool2.postCount - pool1.postCount;
}).map(pool => {
let cssName = misc.makeCssName(pool.category, 'pool');
const caption = (
'<span class="' + cssName + '">'
+ misc.escapeHtml(pool.names[0] + ' (' + pool.postCount + ')')
+ '</span>');
return {
caption: caption,
value: pool,
};
});
return [...pools]
.sort((pool1, pool2) => {
return pool2.postCount - pool1.postCount;
})
.map((pool) => {
let cssName = misc.makeCssName(pool.category, "pool");
const caption =
'<span class="' +
cssName +
'">' +
misc.escapeHtml(pool.names[0] + " (" + pool.postCount + ")") +
"</span>";
return {
caption: caption,
value: pool,
};
});
}
class PoolAutoCompleteControl extends AutoCompleteControl {
constructor(input, options) {
const minLengthForPartialSearch = 3;
options.getMatches = text => {
options.getMatches = (text) => {
const term = misc.escapeSearchTerm(text);
const query = (
text.length < minLengthForPartialSearch
? term + '*'
: '*' + term + '*') + ' sort:post-count';
const query =
(text.length < minLengthForPartialSearch
? term + "*"
: "*" + term + "*") + " sort:post-count";
return new Promise((resolve, reject) => {
PoolList.search(
query, 0, this._options.maxResults, ['id', 'names', 'category', 'postCount', 'version'])
.then(
response => resolve(
_poolListToMatches(response.results, this._options)),
reject);
PoolList.search(query, 0, this._options.maxResults, [
"id",
"names",
"category",
"postCount",
"version",
]).then(
(response) =>
resolve(
_poolListToMatches(response.results, this._options)
),
reject
);
});
};

View file

@ -1,24 +1,24 @@
'use strict';
"use strict";
const api = require('../api.js');
const pools = require('../pools.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const Pool = require('../models/pool.js');
const settings = require('../models/settings.js');
const events = require('../events.js');
const views = require('../util/views.js');
const PoolAutoCompleteControl = require('./pool_auto_complete_control.js');
const api = require("../api.js");
const pools = require("../pools.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const Pool = require("../models/pool.js");
const settings = require("../models/settings.js");
const events = require("../events.js");
const views = require("../util/views.js");
const PoolAutoCompleteControl = require("./pool_auto_complete_control.js");
const KEY_SPACE = 32;
const KEY_RETURN = 13;
const SOURCE_INIT = 'init';
const SOURCE_IMPLICATION = 'implication';
const SOURCE_USER_INPUT = 'user-input';
const SOURCE_CLIPBOARD = 'clipboard';
const SOURCE_INIT = "init";
const SOURCE_IMPLICATION = "implication";
const SOURCE_USER_INPUT = "user-input";
const SOURCE_CLIPBOARD = "clipboard";
const template = views.getTemplate('pool-input');
const template = views.getTemplate("pool-input");
function _fadeOutListItemNodeStatus(listItemNode) {
if (listItemNode.classList.length) {
@ -27,8 +27,7 @@ function _fadeOutListItemNodeStatus(listItemNode) {
}
listItemNode.fadeTimeout = window.setTimeout(() => {
while (listItemNode.classList.length) {
listItemNode.classList.remove(
listItemNode.classList.item(0));
listItemNode.classList.remove(listItemNode.classList.item(0));
}
listItemNode.fadeTimeout = null;
}, 2500);
@ -45,29 +44,33 @@ class PoolInputControl extends events.EventTarget {
// dom
const editAreaNode = template();
this._editAreaNode = editAreaNode;
this._poolInputNode = editAreaNode.querySelector('input');
this._poolListNode = editAreaNode.querySelector('ul.compact-pools');
this._poolInputNode = editAreaNode.querySelector("input");
this._poolListNode = editAreaNode.querySelector("ul.compact-pools");
this._autoCompleteControl = new PoolAutoCompleteControl(
this._poolInputNode, {
this._poolInputNode,
{
getTextToFind: () => {
return this._poolInputNode.value;
},
confirm: pool => {
this._poolInputNode.value = '';
confirm: (pool) => {
this._poolInputNode.value = "";
this.addPool(pool, SOURCE_USER_INPUT);
},
delete: pool => {
this._poolInputNode.value = '';
delete: (pool) => {
this._poolInputNode.value = "";
this.deletePool(pool);
},
verticalShift: -2
});
verticalShift: -2,
}
);
// show
this._hostNode.style.display = 'none';
this._hostNode.style.display = "none";
this._hostNode.parentNode.insertBefore(
this._editAreaNode, hostNode.nextSibling);
this._editAreaNode,
hostNode.nextSibling
);
// add existing pools
for (let pool of [...this.pools]) {
@ -81,19 +84,21 @@ class PoolInputControl extends events.EventTarget {
return Promise.resolve();
}
this.pools.add(pool, false)
this.pools.add(pool, false);
const listItemNode = this._createListItemNode(pool);
if (!pool.category) {
listItemNode.classList.add('new');
listItemNode.classList.add("new");
}
this._poolListNode.prependChild(listItemNode);
_fadeOutListItemNodeStatus(listItemNode);
this.dispatchEvent(new CustomEvent('add', {
detail: {pool: pool, source: source},
}));
this.dispatchEvent(new CustomEvent('change'));
this.dispatchEvent(
new CustomEvent("add", {
detail: { pool: pool, source: source },
})
);
this.dispatchEvent(new CustomEvent("change"));
return Promise.resolve();
}
@ -107,52 +112,57 @@ class PoolInputControl extends events.EventTarget {
this._deleteListItemNode(pool);
this.dispatchEvent(new CustomEvent('remove', {
detail: {pool: pool},
}));
this.dispatchEvent(new CustomEvent('change'));
this.dispatchEvent(
new CustomEvent("remove", {
detail: { pool: pool },
})
);
this.dispatchEvent(new CustomEvent("change"));
}
_createListItemNode(pool) {
const className = pool.category ?
misc.makeCssName(pool.category, 'pool') :
null;
const className = pool.category
? misc.makeCssName(pool.category, "pool")
: null;
const poolLinkNode = document.createElement('a');
const poolLinkNode = document.createElement("a");
if (className) {
poolLinkNode.classList.add(className);
}
poolLinkNode.setAttribute(
'href', uri.formatClientLink('pool', pool.names[0]));
"href",
uri.formatClientLink("pool", pool.names[0])
);
const poolIconNode = document.createElement('i');
poolIconNode.classList.add('fa');
poolIconNode.classList.add('fa-pool');
const poolIconNode = document.createElement("i");
poolIconNode.classList.add("fa");
poolIconNode.classList.add("fa-pool");
poolLinkNode.appendChild(poolIconNode);
const searchLinkNode = document.createElement('a');
const searchLinkNode = document.createElement("a");
if (className) {
searchLinkNode.classList.add(className);
}
searchLinkNode.setAttribute(
'href', uri.formatClientLink(
'posts', {query: "pool:" + pool.id}));
searchLinkNode.textContent = pool.names[0] + ' ';
"href",
uri.formatClientLink("posts", { query: "pool:" + pool.id })
);
searchLinkNode.textContent = pool.names[0] + " ";
const usagesNode = document.createElement('span');
usagesNode.classList.add('pool-usages');
usagesNode.setAttribute('data-pseudo-content', pool.postCount);
const usagesNode = document.createElement("span");
usagesNode.classList.add("pool-usages");
usagesNode.setAttribute("data-pseudo-content", pool.postCount);
const removalLinkNode = document.createElement('a');
removalLinkNode.classList.add('remove-pool');
removalLinkNode.setAttribute('href', '');
removalLinkNode.setAttribute('data-pseudo-content', '×');
removalLinkNode.addEventListener('click', e => {
const removalLinkNode = document.createElement("a");
removalLinkNode.classList.add("remove-pool");
removalLinkNode.setAttribute("href", "");
removalLinkNode.setAttribute("data-pseudo-content", "×");
removalLinkNode.addEventListener("click", (e) => {
e.preventDefault();
this.deletePool(pool);
});
const listItemNode = document.createElement('li');
const listItemNode = document.createElement("li");
listItemNode.appendChild(removalLinkNode);
listItemNode.appendChild(poolLinkNode);
listItemNode.appendChild(searchLinkNode);

View file

@ -1,36 +1,38 @@
'use strict';
"use strict";
const settings = require('../models/settings.js');
const views = require('../util/views.js');
const optimizedResize = require('../util/optimized_resize.js');
const settings = require("../models/settings.js");
const views = require("../util/views.js");
const optimizedResize = require("../util/optimized_resize.js");
class PostContentControl {
constructor(hostNode, post, viewportSizeCalculator, fitFunctionOverride) {
this._post = post;
this._viewportSizeCalculator = viewportSizeCalculator;
this._hostNode = hostNode;
this._template = views.getTemplate('post-content');
this._template = views.getTemplate("post-content");
let fitMode = settings.get().fitMode;
if (typeof fitFunctionOverride !== 'undefined') {
if (typeof fitFunctionOverride !== "undefined") {
fitMode = fitFunctionOverride;
}
this._currentFitFunction = {
'fit-both': this.fitBoth,
'fit-original': this.fitOriginal,
'fit-width': this.fitWidth,
'fit-height': this.fitHeight,
}[fitMode] || this.fitBoth;
this._currentFitFunction =
{
"fit-both": this.fitBoth,
"fit-original": this.fitOriginal,
"fit-width": this.fitWidth,
"fit-height": this.fitHeight,
}[fitMode] || this.fitBoth;
this._install();
this._post.addEventListener(
'changeContent', e => this._evtPostContentChange(e));
this._post.addEventListener("changeContent", (e) =>
this._evtPostContentChange(e)
);
}
disableOverlay() {
this._hostNode.querySelector('.post-overlay').style.display = 'none';
this._hostNode.querySelector(".post-overlay").style.display = "none";
}
fitWidth() {
@ -92,10 +94,11 @@ class PostContentControl {
_resize(width, height) {
const resizeListenerNodes = [this._postContentNode].concat(
...this._postContentNode.querySelectorAll('.resize-listener'));
...this._postContentNode.querySelectorAll(".resize-listener")
);
for (let node of resizeListenerNodes) {
node.style.width = width + 'px';
node.style.height = height + 'px';
node.style.width = width + "px";
node.style.height = height + "px";
}
}
@ -106,10 +109,9 @@ class PostContentControl {
_install() {
this._reinstall();
optimizedResize.add(() => this._refreshSize());
views.monitorNodeRemoval(
this._hostNode, () => {
this._uninstall();
});
views.monitorNodeRemoval(this._hostNode, () => {
this._uninstall();
});
}
_reinstall() {
@ -118,7 +120,7 @@ class PostContentControl {
autoplay: settings.get().autoplayVideos,
});
if (settings.get().transparencyGrid) {
newNode.classList.add('transparency-grid');
newNode.classList.add("transparency-grid");
}
if (this._postContentNode) {
this._hostNode.replaceChild(newNode, this._postContentNode);

View file

@ -1,17 +1,17 @@
'use strict';
"use strict";
const api = require('../api.js');
const events = require('../events.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
const Note = require('../models/note.js');
const Point = require('../models/point.js');
const TagInputControl = require('./tag_input_control.js');
const PoolInputControl = require('./pool_input_control.js');
const ExpanderControl = require('../controls/expander_control.js');
const FileDropperControl = require('../controls/file_dropper_control.js');
const api = require("../api.js");
const events = require("../events.js");
const misc = require("../util/misc.js");
const views = require("../util/views.js");
const Note = require("../models/note.js");
const Point = require("../models/point.js");
const TagInputControl = require("./tag_input_control.js");
const PoolInputControl = require("./pool_input_control.js");
const ExpanderControl = require("../controls/expander_control.js");
const FileDropperControl = require("../controls/file_dropper_control.js");
const template = views.getTemplate('post-edit-sidebar');
const template = views.getTemplate("post-edit-sidebar");
class PostEditSidebarControl extends events.EventTarget {
constructor(hostNode, post, postContentControl, postNotesOverlayControl) {
@ -24,182 +24,222 @@ class PostEditSidebarControl extends events.EventTarget {
this._postNotesOverlayControl.switchToPassiveEdit();
views.replaceContent(this._hostNode, template({
post: this._post,
enableSafety: api.safetyEnabled(),
hasClipboard: document.queryCommandSupported('copy'),
canEditPostSafety: api.hasPrivilege('posts:edit:safety'),
canEditPostSource: api.hasPrivilege('posts:edit:source'),
canEditPostTags: api.hasPrivilege('posts:edit:tags'),
canEditPostRelations: api.hasPrivilege('posts:edit:relations'),
canEditPostNotes: api.hasPrivilege('posts:edit:notes') &&
post.type !== 'video' &&
post.type !== 'flash',
canEditPostFlags: api.hasPrivilege('posts:edit:flags'),
canEditPostContent: api.hasPrivilege('posts:edit:content'),
canEditPostThumbnail: api.hasPrivilege('posts:edit:thumbnail'),
canEditPoolPosts: api.hasPrivilege('pools:edit:posts'),
canCreateAnonymousPosts: api.hasPrivilege('posts:create:anonymous'),
canDeletePosts: api.hasPrivilege('posts:delete'),
canFeaturePosts: api.hasPrivilege('posts:feature'),
canMergePosts: api.hasPrivilege('posts:merge'),
}));
views.replaceContent(
this._hostNode,
template({
post: this._post,
enableSafety: api.safetyEnabled(),
hasClipboard: document.queryCommandSupported("copy"),
canEditPostSafety: api.hasPrivilege("posts:edit:safety"),
canEditPostSource: api.hasPrivilege("posts:edit:source"),
canEditPostTags: api.hasPrivilege("posts:edit:tags"),
canEditPostRelations: api.hasPrivilege("posts:edit:relations"),
canEditPostNotes:
api.hasPrivilege("posts:edit:notes") &&
post.type !== "video" &&
post.type !== "flash",
canEditPostFlags: api.hasPrivilege("posts:edit:flags"),
canEditPostContent: api.hasPrivilege("posts:edit:content"),
canEditPostThumbnail: api.hasPrivilege("posts:edit:thumbnail"),
canEditPoolPosts: api.hasPrivilege("pools:edit:posts"),
canCreateAnonymousPosts: api.hasPrivilege(
"posts:create:anonymous"
),
canDeletePosts: api.hasPrivilege("posts:delete"),
canFeaturePosts: api.hasPrivilege("posts:feature"),
canMergePosts: api.hasPrivilege("posts:merge"),
})
);
new ExpanderControl(
'post-info',
'Basic info',
this._hostNode.querySelectorAll('.safety, .relations, .flags, .post-source'));
"post-info",
"Basic info",
this._hostNode.querySelectorAll(
".safety, .relations, .flags, .post-source"
)
);
this._tagsExpander = new ExpanderControl(
'post-tags',
"post-tags",
`Tags (${this._post.tags.length})`,
this._hostNode.querySelectorAll('.tags'));
this._hostNode.querySelectorAll(".tags")
);
this._notesExpander = new ExpanderControl(
'post-notes',
'Notes',
this._hostNode.querySelectorAll('.notes'));
"post-notes",
"Notes",
this._hostNode.querySelectorAll(".notes")
);
this._poolsExpander = new ExpanderControl(
'post-pools',
"post-pools",
`Pools (${this._post.pools.length})`,
this._hostNode.querySelectorAll('.pools'));
this._hostNode.querySelectorAll(".pools")
);
new ExpanderControl(
'post-content',
'Content',
this._hostNode.querySelectorAll('.post-content, .post-thumbnail'));
"post-content",
"Content",
this._hostNode.querySelectorAll(".post-content, .post-thumbnail")
);
new ExpanderControl(
'post-management',
'Management',
this._hostNode.querySelectorAll('.management'));
"post-management",
"Management",
this._hostNode.querySelectorAll(".management")
);
this._syncExpanderTitles();
if (this._formNode) {
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
this._formNode.addEventListener("submit", (e) =>
this._evtSubmit(e)
);
}
if (this._tagInputNode) {
this._tagControl = new TagInputControl(
this._tagInputNode, post.tags);
this._tagInputNode,
post.tags
);
}
if (this._poolInputNode) {
this._poolControl = new PoolInputControl(
this._poolInputNode, post.pools);
this._poolInputNode,
post.pools
);
}
if (this._contentInputNode) {
this._contentFileDropper = new FileDropperControl(
this._contentInputNode, {allowUrls: true,
this._contentInputNode,
{
allowUrls: true,
lock: true,
urlPlaceholder: '...or paste an URL here.'});
this._contentFileDropper.addEventListener('fileadd', e => {
urlPlaceholder: "...or paste an URL here.",
}
);
this._contentFileDropper.addEventListener("fileadd", (e) => {
this._newPostContent = e.detail.files[0];
});
this._contentFileDropper.addEventListener('urladd', e => {
this._contentFileDropper.addEventListener("urladd", (e) => {
this._newPostContent = e.detail.urls[0];
});
}
if (this._thumbnailInputNode) {
this._thumbnailFileDropper = new FileDropperControl(
this._thumbnailInputNode, {lock: true});
this._thumbnailFileDropper.addEventListener('fileadd', e => {
this._thumbnailInputNode,
{ lock: true }
);
this._thumbnailFileDropper.addEventListener("fileadd", (e) => {
this._newPostThumbnail = e.detail.files[0];
this._thumbnailRemovalLinkNode.style.display = 'block';
this._thumbnailRemovalLinkNode.style.display = "block";
});
}
if (this._thumbnailRemovalLinkNode) {
this._thumbnailRemovalLinkNode.addEventListener(
'click', e => this._evtRemoveThumbnailClick(e));
this._thumbnailRemovalLinkNode.style.display =
this._post.hasCustomThumbnail ? 'block' : 'none';
this._thumbnailRemovalLinkNode.addEventListener("click", (e) =>
this._evtRemoveThumbnailClick(e)
);
this._thumbnailRemovalLinkNode.style.display = this._post
.hasCustomThumbnail
? "block"
: "none";
}
if (this._addNoteLinkNode) {
this._addNoteLinkNode.addEventListener(
'click', e => this._evtAddNoteClick(e));
this._addNoteLinkNode.addEventListener("click", (e) =>
this._evtAddNoteClick(e)
);
}
if (this._copyNotesLinkNode) {
this._copyNotesLinkNode.addEventListener(
'click', e => this._evtCopyNotesClick(e));
this._copyNotesLinkNode.addEventListener("click", (e) =>
this._evtCopyNotesClick(e)
);
}
if (this._pasteNotesLinkNode) {
this._pasteNotesLinkNode.addEventListener(
'click', e => this._evtPasteNotesClick(e));
this._pasteNotesLinkNode.addEventListener("click", (e) =>
this._evtPasteNotesClick(e)
);
}
if (this._deleteNoteLinkNode) {
this._deleteNoteLinkNode.addEventListener(
'click', e => this._evtDeleteNoteClick(e));
this._deleteNoteLinkNode.addEventListener("click", (e) =>
this._evtDeleteNoteClick(e)
);
}
if (this._featureLinkNode) {
this._featureLinkNode.addEventListener(
'click', e => this._evtFeatureClick(e));
this._featureLinkNode.addEventListener("click", (e) =>
this._evtFeatureClick(e)
);
}
if (this._mergeLinkNode) {
this._mergeLinkNode.addEventListener(
'click', e => this._evtMergeClick(e));
this._mergeLinkNode.addEventListener("click", (e) =>
this._evtMergeClick(e)
);
}
if (this._deleteLinkNode) {
this._deleteLinkNode.addEventListener(
'click', e => this._evtDeleteClick(e));
this._deleteLinkNode.addEventListener("click", (e) =>
this._evtDeleteClick(e)
);
}
this._postNotesOverlayControl.addEventListener(
'blur', e => this._evtNoteBlur(e));
this._postNotesOverlayControl.addEventListener("blur", (e) =>
this._evtNoteBlur(e)
);
this._postNotesOverlayControl.addEventListener(
'focus', e => this._evtNoteFocus(e));
this._postNotesOverlayControl.addEventListener("focus", (e) =>
this._evtNoteFocus(e)
);
this._post.addEventListener(
'changeContent', e => this._evtPostContentChange(e));
this._post.addEventListener("changeContent", (e) =>
this._evtPostContentChange(e)
);
this._post.addEventListener(
'changeThumbnail', e => this._evtPostThumbnailChange(e));
this._post.addEventListener("changeThumbnail", (e) =>
this._evtPostThumbnailChange(e)
);
if (this._formNode) {
const inputNodes = this._formNode.querySelectorAll(
'input, textarea');
"input, textarea"
);
for (let node of inputNodes) {
node.addEventListener(
'change',
e => this.dispatchEvent(new CustomEvent('change')));
node.addEventListener("change", (e) =>
this.dispatchEvent(new CustomEvent("change"))
);
}
this._postNotesOverlayControl.addEventListener(
'change',
e => this.dispatchEvent(new CustomEvent('change')));
this._postNotesOverlayControl.addEventListener("change", (e) =>
this.dispatchEvent(new CustomEvent("change"))
);
}
for (let eventType of ['add', 'remove']) {
this._post.notes.addEventListener(eventType, e => {
for (let eventType of ["add", "remove"]) {
this._post.notes.addEventListener(eventType, (e) => {
this._syncExpanderTitles();
});
this._post.pools.addEventListener(eventType, e => {
this._post.pools.addEventListener(eventType, (e) => {
this._syncExpanderTitles();
});
}
this._tagControl.addEventListener(
'change', e => {
this.dispatchEvent(new CustomEvent('change'));
this._syncExpanderTitles();
});
this._tagControl.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent("change"));
this._syncExpanderTitles();
});
if (this._noteTextareaNode) {
this._noteTextareaNode.addEventListener(
'change', e => this._evtNoteTextChangeRequest(e));
this._noteTextareaNode.addEventListener("change", (e) =>
this._evtNoteTextChangeRequest(e)
);
}
this._poolControl.addEventListener(
'change', e => {
this.dispatchEvent(new CustomEvent('change'));
this._syncExpanderTitles();
});
this._poolControl.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent("change"));
this._syncExpanderTitles();
});
}
_syncExpanderTitles() {
@ -220,37 +260,43 @@ class PostEditSidebarControl extends events.EventTarget {
e.preventDefault();
this._thumbnailFileDropper.reset();
this._newPostThumbnail = null;
this._thumbnailRemovalLinkNode.style.display = 'none';
this._thumbnailRemovalLinkNode.style.display = "none";
}
_evtFeatureClick(e) {
e.preventDefault();
if (confirm('Are you sure you want to feature this post?')) {
this.dispatchEvent(new CustomEvent('feature', {
detail: {
post: this._post,
},
}));
if (confirm("Are you sure you want to feature this post?")) {
this.dispatchEvent(
new CustomEvent("feature", {
detail: {
post: this._post,
},
})
);
}
}
_evtMergeClick(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('merge', {
detail: {
post: this._post,
},
}));
this.dispatchEvent(
new CustomEvent("merge", {
detail: {
post: this._post,
},
})
);
}
_evtDeleteClick(e) {
e.preventDefault();
if (confirm('Are you sure you want to delete this post?')) {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
post: this._post,
},
}));
if (confirm("Are you sure you want to delete this post?")) {
this.dispatchEvent(
new CustomEvent("delete", {
detail: {
post: this._post,
},
})
);
}
}
@ -262,60 +308,64 @@ class PostEditSidebarControl extends events.EventTarget {
_evtNoteFocus(e) {
this._editedNote = e.detail.note;
this._addNoteLinkNode.classList.remove('inactive');
this._deleteNoteLinkNode.classList.remove('inactive');
this._noteTextareaNode.removeAttribute('disabled');
this._addNoteLinkNode.classList.remove("inactive");
this._deleteNoteLinkNode.classList.remove("inactive");
this._noteTextareaNode.removeAttribute("disabled");
this._noteTextareaNode.value = e.detail.note.text;
}
_evtNoteBlur(e) {
this._evtNoteTextChangeRequest(null);
this._addNoteLinkNode.classList.remove('inactive');
this._deleteNoteLinkNode.classList.add('inactive');
this._addNoteLinkNode.classList.remove("inactive");
this._deleteNoteLinkNode.classList.add("inactive");
this._noteTextareaNode.blur();
this._noteTextareaNode.setAttribute('disabled', 'disabled');
this._noteTextareaNode.value = '';
this._noteTextareaNode.setAttribute("disabled", "disabled");
this._noteTextareaNode.value = "";
}
_evtAddNoteClick(e) {
e.preventDefault();
if (e.target.classList.contains('inactive')) {
if (e.target.classList.contains("inactive")) {
return;
}
this._addNoteLinkNode.classList.add('inactive');
this._addNoteLinkNode.classList.add("inactive");
this._postNotesOverlayControl.switchToDrawing();
}
_evtCopyNotesClick(e) {
e.preventDefault();
let textarea = document.createElement('textarea');
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
textarea.value = JSON.stringify([...this._post.notes].map(note => ({
polygon: [...note.polygon].map(
point => [point.x, point.y]),
text: note.text,
})));
let textarea = document.createElement("textarea");
textarea.style.position = "fixed";
textarea.style.opacity = "0";
textarea.value = JSON.stringify(
[...this._post.notes].map((note) => ({
polygon: [...note.polygon].map((point) => [point.x, point.y]),
text: note.text,
}))
);
document.body.appendChild(textarea);
textarea.select();
let success = false;
try {
success = document.execCommand('copy');
success = document.execCommand("copy");
} catch (err) {
// continue regardless of error
}
textarea.blur();
document.body.removeChild(textarea);
alert(success
? 'Notes copied to clipboard.'
: 'Failed to copy the text to clipboard. Sorry.');
alert(
success
? "Notes copied to clipboard."
: "Failed to copy the text to clipboard. Sorry."
);
}
_evtPasteNotesClick(e) {
e.preventDefault();
const text = window.prompt(
'Please enter the exported notes snapshot:');
"Please enter the exported notes snapshot:"
);
if (!text) {
return;
}
@ -333,7 +383,7 @@ class PostEditSidebarControl extends events.EventTarget {
_evtDeleteNoteClick(e) {
e.preventDefault();
if (e.target.classList.contains('inactive')) {
if (e.target.classList.contains("inactive")) {
return;
}
this._post.notes.remove(this._editedNote);
@ -342,72 +392,78 @@ class PostEditSidebarControl extends events.EventTarget {
_evtSubmit(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
post: this._post,
this.dispatchEvent(
new CustomEvent("submit", {
detail: {
post: this._post,
safety: this._safetyButtonNodes.length ?
Array.from(this._safetyButtonNodes)
.filter(node => node.checked)[0]
.value.toLowerCase() :
undefined,
safety: this._safetyButtonNodes.length
? Array.from(this._safetyButtonNodes)
.filter((node) => node.checked)[0]
.value.toLowerCase()
: undefined,
flags: this._videoFlags,
flags: this._videoFlags,
tags: this._tagInputNode ?
misc.splitByWhitespace(this._tagInputNode.value) :
undefined,
tags: this._tagInputNode
? misc.splitByWhitespace(this._tagInputNode.value)
: undefined,
pools: this._poolInputNode ?
misc.splitByWhitespace(this._poolInputNode.value) :
undefined,
pools: this._poolInputNode
? misc.splitByWhitespace(this._poolInputNode.value)
: undefined,
relations: this._relationsInputNode ?
misc.splitByWhitespace(this._relationsInputNode.value)
.map(x => parseInt(x)) :
undefined,
relations: this._relationsInputNode
? misc
.splitByWhitespace(
this._relationsInputNode.value
)
.map((x) => parseInt(x))
: undefined,
content: this._newPostContent ?
this._newPostContent :
undefined,
content: this._newPostContent
? this._newPostContent
: undefined,
thumbnail: this._newPostThumbnail !== undefined ?
this._newPostThumbnail :
undefined,
thumbnail:
this._newPostThumbnail !== undefined
? this._newPostThumbnail
: undefined,
source: this._sourceInputNode ?
this._sourceInputNode.value :
undefined,
},
}));
source: this._sourceInputNode
? this._sourceInputNode.value
: undefined,
},
})
);
}
get _formNode() {
return this._hostNode.querySelector('form');
return this._hostNode.querySelector("form");
}
get _submitButtonNode() {
return this._hostNode.querySelector('.submit');
return this._hostNode.querySelector(".submit");
}
get _safetyButtonNodes() {
return this._formNode.querySelectorAll('.safety input');
return this._formNode.querySelectorAll(".safety input");
}
get _tagInputNode() {
return this._formNode.querySelector('.tags input');
return this._formNode.querySelector(".tags input");
}
get _poolInputNode() {
return this._formNode.querySelector('.pools input');
return this._formNode.querySelector(".pools input");
}
get _loopVideoInputNode() {
return this._formNode.querySelector('.flags input[name=loop]');
return this._formNode.querySelector(".flags input[name=loop]");
}
get _soundVideoInputNode() {
return this._formNode.querySelector('.flags input[name=sound]');
return this._formNode.querySelector(".flags input[name=sound]");
}
get _videoFlags() {
@ -416,65 +472,68 @@ class PostEditSidebarControl extends events.EventTarget {
}
let ret = [];
if (this._loopVideoInputNode.checked) {
ret.push('loop');
ret.push("loop");
}
if (this._soundVideoInputNode.checked) {
ret.push('sound');
ret.push("sound");
}
return ret;
}
get _relationsInputNode() {
return this._formNode.querySelector('.relations input');
return this._formNode.querySelector(".relations input");
}
get _contentInputNode() {
return this._formNode.querySelector('.post-content .dropper-container');
return this._formNode.querySelector(
".post-content .dropper-container"
);
}
get _thumbnailInputNode() {
return this._formNode.querySelector(
'.post-thumbnail .dropper-container');
".post-thumbnail .dropper-container"
);
}
get _thumbnailRemovalLinkNode() {
return this._formNode.querySelector('.post-thumbnail a');
return this._formNode.querySelector(".post-thumbnail a");
}
get _sourceInputNode() {
return this._formNode.querySelector('.post-source textarea');
return this._formNode.querySelector(".post-source textarea");
}
get _featureLinkNode() {
return this._formNode.querySelector('.management .feature');
return this._formNode.querySelector(".management .feature");
}
get _mergeLinkNode() {
return this._formNode.querySelector('.management .merge');
return this._formNode.querySelector(".management .merge");
}
get _deleteLinkNode() {
return this._formNode.querySelector('.management .delete');
return this._formNode.querySelector(".management .delete");
}
get _addNoteLinkNode() {
return this._formNode.querySelector('.notes .add');
return this._formNode.querySelector(".notes .add");
}
get _copyNotesLinkNode() {
return this._formNode.querySelector('.notes .copy');
return this._formNode.querySelector(".notes .copy");
}
get _pasteNotesLinkNode() {
return this._formNode.querySelector('.notes .paste');
return this._formNode.querySelector(".notes .paste");
}
get _deleteNoteLinkNode() {
return this._formNode.querySelector('.notes .delete');
return this._formNode.querySelector(".notes .delete");
}
get _noteTextareaNode() {
return this._formNode.querySelector('.notes textarea');
return this._formNode.querySelector(".notes textarea");
}
enableForm() {

View file

@ -1,13 +1,13 @@
'use strict';
"use strict";
const keyboard = require('../util/keyboard.js');
const views = require('../util/views.js');
const events = require('../events.js');
const misc = require('../util/misc.js');
const Note = require('../models/note.js');
const Point = require('../models/point.js');
const keyboard = require("../util/keyboard.js");
const views = require("../util/views.js");
const events = require("../events.js");
const misc = require("../util/misc.js");
const Note = require("../models/note.js");
const Point = require("../models/point.js");
const svgNS = 'http://www.w3.org/2000/svg';
const svgNS = "http://www.w3.org/2000/svg";
const snapThreshold = 10;
const circleSize = 10;
@ -22,19 +22,19 @@ const KEY_RETURN = 13;
function _getDistance(point1, point2) {
return Math.sqrt(
Math.pow(point1.x - point2.x, 2) +
Math.pow(point1.y - point2.y, 2));
Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2)
);
}
function _setNodeState(node, stateName) {
if (node === null) {
return;
}
node.setAttribute('data-state', stateName);
node.setAttribute("data-state", stateName);
}
function _clearEditedNote(hostNode) {
const node = hostNode.querySelector('[data-state=\'editing\']');
const node = hostNode.querySelector("[data-state='editing']");
_setNodeState(node, null);
return node !== null;
}
@ -48,7 +48,7 @@ function _getNoteCentroid(note) {
const y0 = note.polygon.at(i).y;
const x1 = note.polygon.at((i + 1) % vertexCount).x;
const y1 = note.polygon.at((i + 1) % vertexCount).y;
const a = (x0 * y1) - (x1 * y0);
const a = x0 * y1 - x1 * y0;
signedArea += a;
centroid.x += (x0 + x1) * a;
centroid.y += (y0 + y1) * a;
@ -82,32 +82,30 @@ class State {
return false;
}
evtCanvasKeyDown(e) {
}
evtCanvasKeyDown(e) {}
evtNoteMouseDown(e, hoveredNote) {
}
evtNoteMouseDown(e, hoveredNote) {}
evtCanvasMouseDown(e) {
}
evtCanvasMouseDown(e) {}
evtCanvasMouseMove(e) {
}
evtCanvasMouseMove(e) {}
evtCanvasMouseUp(e) {
}
evtCanvasMouseUp(e) {}
_getScreenPoint(point) {
return new Point(
point.x * this._control.boundingBox.width,
point.y * this._control.boundingBox.height);
point.y * this._control.boundingBox.height
);
}
_snapPoints(targetPoint, referencePoint) {
const targetScreenPoint = this._getScreenPoint(targetPoint);
const referenceScreenPoint = this._getScreenPoint(referencePoint);
if (_getDistance(targetScreenPoint, referenceScreenPoint) <
snapThreshold) {
if (
_getDistance(targetScreenPoint, referenceScreenPoint) <
snapThreshold
) {
targetPoint.x = referencePoint.x;
targetPoint.y = referencePoint.y;
}
@ -124,15 +122,16 @@ class State {
(e.clientX - this._control.boundingBox.left) /
this._control.boundingBox.width,
(e.clientY - this._control.boundingBox.top) /
this._control.boundingBox.height);
this._control.boundingBox.height
);
}
}
class ReadOnlyState extends State {
constructor(control) {
super(control, 'read-only');
super(control, "read-only");
if (_clearEditedNote(control._hostNode)) {
this._control.dispatchEvent(new CustomEvent('blur'));
this._control.dispatchEvent(new CustomEvent("blur"));
}
keyboard.unpause();
}
@ -144,9 +143,9 @@ class ReadOnlyState extends State {
class PassiveState extends State {
constructor(control) {
super(control, 'passive');
super(control, "passive");
if (_clearEditedNote(control._hostNode)) {
this._control.dispatchEvent(new CustomEvent('blur'));
this._control.dispatchEvent(new CustomEvent("blur"));
}
keyboard.unpause();
}
@ -164,23 +163,24 @@ class ActiveState extends State {
constructor(control, note, stateName) {
super(control, stateName);
if (_clearEditedNote(control._hostNode)) {
this._control.dispatchEvent(new CustomEvent('blur'));
this._control.dispatchEvent(new CustomEvent("blur"));
}
keyboard.pause();
if (note !== null) {
this._note = note;
this._control.dispatchEvent(
new CustomEvent('focus', {
detail: {note: note},
}));
_setNodeState(this._note.groupNode, 'editing');
new CustomEvent("focus", {
detail: { note: note },
})
);
_setNodeState(this._note.groupNode, "editing");
}
}
}
class SelectedState extends ActiveState {
constructor(control, note) {
super(control, note, 'selected');
super(control, note, "selected");
this._clickTimeout = null;
this._control._hideNoteText();
}
@ -211,27 +211,40 @@ class SelectedState extends ActiveState {
const mouseScreenPoint = this._getScreenPoint(mousePoint);
if (e.shiftKey) {
this._control._state = new ScalingNoteState(
this._control, this._note, mousePoint);
this._control,
this._note,
mousePoint
);
return;
}
if (this._note !== hoveredNote) {
this._control._state =
new SelectedState(this._control, hoveredNote);
this._control._state = new SelectedState(
this._control,
hoveredNote
);
return;
}
this._clickTimeout = window.setTimeout(() => {
for (let polygonPoint of this._note.polygon) {
const distance = _getDistance(
mouseScreenPoint,
this._getScreenPoint(polygonPoint));
this._getScreenPoint(polygonPoint)
);
if (distance < circleSize) {
this._control._state = new MovingPointState(
this._control, this._note, polygonPoint, mousePoint);
this._control,
this._note,
polygonPoint,
mousePoint
);
return;
}
}
this._control._state = new MovingNoteState(
this._control, this._note, mousePoint);
this._control,
this._note,
mousePoint
);
}, 100);
}
@ -241,9 +254,12 @@ class SelectedState extends ActiveState {
for (let polygonPoint of this._note.polygon) {
const distance = _getDistance(
mouseScreenPoint,
this._getScreenPoint(polygonPoint));
this._getScreenPoint(polygonPoint)
);
polygonPoint.edgeNode.classList.toggle(
'nearby', distance < circleSize);
"nearby",
distance < circleSize
);
}
}
@ -252,16 +268,24 @@ class SelectedState extends ActiveState {
const mouseScreenPoint = this._getScreenPoint(mousePoint);
if (e.shiftKey) {
this._control._state = new ScalingNoteState(
this._control, this._note, mousePoint);
this._control,
this._note,
mousePoint
);
return;
}
for (let polygonPoint of this._note.polygon) {
const distance = _getDistance(
mouseScreenPoint,
this._getScreenPoint(polygonPoint));
this._getScreenPoint(polygonPoint)
);
if (distance < circleSize) {
this._control._state = new MovingPointState(
this._control, this._note, polygonPoint, mousePoint);
this._control,
this._note,
polygonPoint,
mousePoint
);
return;
}
}
@ -283,32 +307,37 @@ class SelectedState extends ActiveState {
const origin = _getNoteCentroid(this._note);
const originalSize = _getNoteSize(this._note);
const targetSize = new Point(
originalSize.x + (x / this._control.boundingBox.width),
originalSize.y + (y / this._control.boundingBox.height));
originalSize.x + x / this._control.boundingBox.width,
originalSize.y + y / this._control.boundingBox.height
);
const scale = new Point(
targetSize.x / originalSize.x,
targetSize.y / originalSize.y);
targetSize.y / originalSize.y
);
for (let point of this._note.polygon) {
point.x = origin.x + ((point.x - origin.x) * scale.x);
point.y = origin.y + ((point.y - origin.y) * scale.y);
point.x = origin.x + (point.x - origin.x) * scale.x;
point.y = origin.y + (point.y - origin.y) * scale.y;
}
}
}
class MovingPointState extends ActiveState {
constructor(control, note, notePoint, mousePoint) {
super(control, note, 'moving-point');
super(control, note, "moving-point");
this._notePoint = notePoint;
this._originalNotePoint = {x: notePoint.x, y: notePoint.y};
this._originalNotePoint = { x: notePoint.x, y: notePoint.y };
this._originalPosition = mousePoint;
_setNodeState(this._note.groupNode, 'editing');
_setNodeState(this._note.groupNode, "editing");
}
evtCanvasKeyDown(e) {
if (e.which === KEY_ESCAPE) {
this._notePoint.x = this._originalNotePoint.x;
this._notePoint.y = this._originalNotePoint.y;
this._control._state = new SelectedState(this._control, this._note);
this._control._state = new SelectedState(
this._control,
this._note
);
}
}
@ -326,9 +355,11 @@ class MovingPointState extends ActiveState {
class MovingNoteState extends ActiveState {
constructor(control, note, mousePoint) {
super(control, note, 'moving-note');
this._originalPolygon = [...note.polygon].map(
point => ({x: point.x, y: point.y}));
super(control, note, "moving-note");
this._originalPolygon = [...note.polygon].map((point) => ({
x: point.x,
y: point.y,
}));
this._originalPosition = mousePoint;
}
@ -338,7 +369,10 @@ class MovingNoteState extends ActiveState {
this._note.polygon.at(i).x = this._originalPolygon[i].x;
this._note.polygon.at(i).y = this._originalPolygon[i].y;
}
this._control._state = new SelectedState(this._control, this._note);
this._control._state = new SelectedState(
this._control,
this._note
);
}
}
@ -358,9 +392,11 @@ class MovingNoteState extends ActiveState {
class ScalingNoteState extends ActiveState {
constructor(control, note, mousePoint) {
super(control, note, 'scaling-note');
this._originalPolygon = [...note.polygon].map(
point => ({x: point.x, y: point.y}));
super(control, note, "scaling-note");
this._originalPolygon = [...note.polygon].map((point) => ({
x: point.x,
y: point.y,
}));
this._originalMousePoint = mousePoint;
this._originalSize = _getNoteSize(note);
}
@ -371,7 +407,10 @@ class ScalingNoteState extends ActiveState {
this._note.polygon.at(i).x = this._originalPolygon[i].x;
this._note.polygon.at(i).y = this._originalPolygon[i].y;
}
this._control._state = new SelectedState(this._control, this._note);
this._control._state = new SelectedState(
this._control,
this._note
);
}
}
@ -384,12 +423,16 @@ class ScalingNoteState extends ActiveState {
const originalPolygonPoint = this._originalPolygon[i];
polygonPoint.x =
originalMousePoint.x +
((originalPolygonPoint.x - originalMousePoint.x) *
(1 + ((mousePoint.x - originalMousePoint.x) / originalSize.x)));
(originalPolygonPoint.x - originalMousePoint.x) *
(1 +
(mousePoint.x - originalMousePoint.x) /
originalSize.x);
polygonPoint.y =
originalMousePoint.y +
((originalPolygonPoint.y - originalMousePoint.y) *
(1 + ((mousePoint.y - originalMousePoint.y) / originalSize.y)));
(originalPolygonPoint.y - originalMousePoint.y) *
(1 +
(mousePoint.y - originalMousePoint.y) /
originalSize.y);
}
}
@ -400,7 +443,7 @@ class ScalingNoteState extends ActiveState {
class ReadyToDrawState extends ActiveState {
constructor(control) {
super(control, null, 'ready-to-draw');
super(control, null, "ready-to-draw");
}
evtNoteMouseDown(e, hoveredNote) {
@ -411,23 +454,27 @@ class ReadyToDrawState extends ActiveState {
const mousePoint = this._getPointFromEvent(e);
if (e.shiftKey) {
this._control._state = new DrawingRectangleState(
this._control, mousePoint);
this._control,
mousePoint
);
} else {
this._control._state = new DrawingPolygonState(
this._control, mousePoint);
this._control,
mousePoint
);
}
}
}
class DrawingRectangleState extends ActiveState {
constructor(control, mousePoint) {
super(control, null, 'drawing-rectangle');
super(control, null, "drawing-rectangle");
this._note = this._createNote();
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
_setNodeState(this._note.groupNode, 'drawing');
_setNodeState(this._note.groupNode, "drawing");
}
evtCanvasMouseUp(e) {
@ -443,7 +490,10 @@ class DrawingRectangleState extends ActiveState {
this._control._state = new ReadyToDrawState(this._control);
} else {
this._control._post.notes.add(this._note);
this._control._state = new SelectedState(this._control, this._note);
this._control._state = new SelectedState(
this._control,
this._note
);
}
}
@ -458,11 +508,11 @@ class DrawingRectangleState extends ActiveState {
class DrawingPolygonState extends ActiveState {
constructor(control, mousePoint) {
super(control, null, 'drawing-polygon');
super(control, null, "drawing-polygon");
this._note = this._createNote();
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
_setNodeState(this._note.groupNode, 'drawing');
_setNodeState(this._note.groupNode, "drawing");
}
evtCanvasKeyDown(e) {
@ -502,11 +552,16 @@ class DrawingPolygonState extends ActiveState {
}
if (e.shiftKey && secondLastPoint) {
const direction = (Math.round(
Math.atan2(
secondLastPoint.y - mousePoint.y,
secondLastPoint.x - mousePoint.x) /
(2 * Math.PI / 4)) + 4) % 4;
const direction =
(Math.round(
Math.atan2(
secondLastPoint.y - mousePoint.y,
secondLastPoint.x - mousePoint.x
) /
((2 * Math.PI) / 4)
) +
4) %
4;
if (direction === 0 || direction === 2) {
lastPoint.x = mousePoint.x;
lastPoint.y = secondLastPoint.y;
@ -533,7 +588,10 @@ class DrawingPolygonState extends ActiveState {
} else {
this._control._deleteDomNode(this._note);
this._control._post.notes.add(this._note);
this._control._state = new SelectedState(this._control, this._note);
this._control._state = new SelectedState(
this._control,
this._note
);
}
}
}
@ -544,48 +602,51 @@ class PostNotesOverlayControl extends events.EventTarget {
this._post = post;
this._hostNode = hostNode;
this._svgNode = document.createElementNS(svgNS, 'svg');
this._svgNode.classList.add('resize-listener');
this._svgNode.classList.add('notes-overlay');
this._svgNode.setAttribute('preserveAspectRatio', 'none');
this._svgNode.setAttribute('viewBox', '0 0 1 1');
this._svgNode = document.createElementNS(svgNS, "svg");
this._svgNode.classList.add("resize-listener");
this._svgNode.classList.add("notes-overlay");
this._svgNode.setAttribute("preserveAspectRatio", "none");
this._svgNode.setAttribute("viewBox", "0 0 1 1");
for (let note of this._post.notes) {
this._createPolygonNode(note);
}
this._hostNode.appendChild(this._svgNode);
this._post.addEventListener('change', e => this._evtPostChange(e));
this._post.notes.addEventListener('remove', e => {
this._post.addEventListener("change", (e) => this._evtPostChange(e));
this._post.notes.addEventListener("remove", (e) => {
this._deleteDomNode(e.detail.note);
});
this._post.notes.addEventListener('add', e => {
this._post.notes.addEventListener("add", (e) => {
this._createPolygonNode(e.detail.note);
});
const keyHandler = e => this._evtCanvasKeyDown(e);
document.addEventListener('keydown', keyHandler);
this._svgNode.addEventListener(
'mousedown', e => this._evtCanvasMouseDown(e));
this._svgNode.addEventListener(
'mouseup', e => this._evtCanvasMouseUp(e));
this._svgNode.addEventListener(
'mousemove', e => this._evtCanvasMouseMove(e));
const keyHandler = (e) => this._evtCanvasKeyDown(e);
document.addEventListener("keydown", keyHandler);
this._svgNode.addEventListener("mousedown", (e) =>
this._evtCanvasMouseDown(e)
);
this._svgNode.addEventListener("mouseup", (e) =>
this._evtCanvasMouseUp(e)
);
this._svgNode.addEventListener("mousemove", (e) =>
this._evtCanvasMouseMove(e)
);
const wrapperNode = document.createElement('div');
wrapperNode.classList.add('wrapper');
this._textNode = document.createElement('div');
this._textNode.classList.add('note-text');
const wrapperNode = document.createElement("div");
wrapperNode.classList.add("wrapper");
this._textNode = document.createElement("div");
this._textNode.classList.add("note-text");
this._textNode.appendChild(wrapperNode);
this._textNode.addEventListener(
'mouseleave', e => this._evtNoteMouseLeave(e));
this._textNode.addEventListener("mouseleave", (e) =>
this._evtNoteMouseLeave(e)
);
document.body.appendChild(this._textNode);
views.monitorNodeRemoval(
this._hostNode, () => {
this._hostNode.removeChild(this._svgNode);
document.removeEventListener('keydown', keyHandler);
document.body.removeChild(this._textNode);
this._state = new ReadOnlyState(this);
});
views.monitorNodeRemoval(this._hostNode, () => {
this._hostNode.removeChild(this._svgNode);
document.removeEventListener("keydown", keyHandler);
document.body.removeChild(this._textNode);
this._state = new ReadOnlyState(this);
});
this._state = new ReadOnlyState(this);
}
@ -613,7 +674,7 @@ class PostNotesOverlayControl extends events.EventTarget {
}
_evtCanvasKeyDown(e) {
const illegalNodeNames = ['textarea', 'input', 'select'];
const illegalNodeNames = ["textarea", "input", "select"];
if (illegalNodeNames.includes(e.target.nodeName.toLowerCase())) {
return;
}
@ -655,53 +716,58 @@ class PostNotesOverlayControl extends events.EventTarget {
_evtNoteMouseLeave(e) {
const newElement = e.relatedTarget;
if (newElement === this._svgNode ||
(!this._svgNode.contains(newElement) &&
if (
newElement === this._svgNode ||
(!this._svgNode.contains(newElement) &&
!this._textNode.contains(newElement) &&
newElement !== this._textNode)) {
newElement !== this._textNode)
) {
this._hideNoteText();
}
}
_showNoteText(note) {
this._textNode.querySelector('.wrapper').innerHTML =
misc.formatMarkdown(note.text);
this._textNode.style.display = 'block';
this._textNode.querySelector(
".wrapper"
).innerHTML = misc.formatMarkdown(note.text);
this._textNode.style.display = "block";
const bodyRect = document.body.getBoundingClientRect();
const noteRect = this._textNode.getBoundingClientRect();
const svgRect = this.boundingBox;
const centroid = _getNoteCentroid(note);
const x = (
const x =
-bodyRect.left +
svgRect.left +
(svgRect.width * centroid.x) -
(noteRect.width / 2));
const y = (
svgRect.width * centroid.x -
noteRect.width / 2;
const y =
-bodyRect.top +
svgRect.top +
(svgRect.height * centroid.y) -
(noteRect.height / 2));
this._textNode.style.left = x + 'px';
this._textNode.style.top = y + 'px';
svgRect.height * centroid.y -
noteRect.height / 2;
this._textNode.style.left = x + "px";
this._textNode.style.top = y + "px";
}
_hideNoteText() {
this._textNode.style.display = 'none';
this._textNode.style.display = "none";
}
_updatePolygonNotePoints(note) {
note.polygonNode.setAttribute(
'points',
[...note.polygon].map(
point => [point.x, point.y].join(',')).join(' '));
"points",
[...note.polygon]
.map((point) => [point.x, point.y].join(","))
.join(" ")
);
}
_createEdgeNode(point, groupNode) {
const node = document.createElementNS(svgNS, 'ellipse');
node.setAttribute('cx', point.x);
node.setAttribute('cy', point.y);
node.setAttribute('rx', circleSize / 2 / this.boundingBox.width);
node.setAttribute('ry', circleSize / 2 / this.boundingBox.height);
const node = document.createElementNS(svgNS, "ellipse");
node.setAttribute("cx", point.x);
node.setAttribute("cy", point.y);
node.setAttribute("rx", circleSize / 2 / this.boundingBox.width);
node.setAttribute("ry", circleSize / 2 / this.boundingBox.height);
point.edgeNode = node;
groupNode.appendChild(node);
}
@ -713,8 +779,8 @@ class PostNotesOverlayControl extends events.EventTarget {
_updateEdgeNode(point, note) {
this._updatePolygonNotePoints(note);
point.edgeNode.setAttribute('cx', point.x);
point.edgeNode.setAttribute('cy', point.y);
point.edgeNode.setAttribute("cx", point.x);
point.edgeNode.setAttribute("cy", point.y);
}
_deleteDomNode(note) {
@ -722,17 +788,19 @@ class PostNotesOverlayControl extends events.EventTarget {
}
_createPolygonNode(note) {
const groupNode = document.createElementNS(svgNS, 'g');
const groupNode = document.createElementNS(svgNS, "g");
note.groupNode = groupNode;
{
const node = document.createElementNS(svgNS, 'polygon');
const node = document.createElementNS(svgNS, "polygon");
note.polygonNode = node;
node.setAttribute('vector-effect', 'non-scaling-stroke');
node.setAttribute('stroke-alignment', 'inside');
node.addEventListener(
'mouseenter', e => this._evtNoteMouseEnter(e, note));
node.addEventListener(
'mouseleave', e => this._evtNoteMouseLeave(e));
node.setAttribute("vector-effect", "non-scaling-stroke");
node.setAttribute("stroke-alignment", "inside");
node.addEventListener("mouseenter", (e) =>
this._evtNoteMouseEnter(e, note)
);
node.addEventListener("mouseleave", (e) =>
this._evtNoteMouseLeave(e)
);
this._updatePolygonNotePoints(note);
groupNode.appendChild(node);
}
@ -740,17 +808,17 @@ class PostNotesOverlayControl extends events.EventTarget {
this._createEdgeNode(point, groupNode);
}
note.polygon.addEventListener('change', e => {
note.polygon.addEventListener("change", (e) => {
this._updateEdgeNode(e.detail.point, note);
this.dispatchEvent(new CustomEvent('change'));
this.dispatchEvent(new CustomEvent("change"));
});
note.polygon.addEventListener('remove', e => {
note.polygon.addEventListener("remove", (e) => {
this._deleteEdgeNode(e.detail.point, note);
this.dispatchEvent(new CustomEvent('change'));
this.dispatchEvent(new CustomEvent("change"));
});
note.polygon.addEventListener('add', e => {
note.polygon.addEventListener("add", (e) => {
this._createEdgeNode(e.detail.point, groupNode);
this.dispatchEvent(new CustomEvent('change'));
this.dispatchEvent(new CustomEvent("change"));
});
this._svgNode.appendChild(groupNode);

View file

@ -1,14 +1,14 @@
'use strict';
"use strict";
const api = require('../api.js');
const events = require('../events.js');
const views = require('../util/views.js');
const uri = require('../util/uri.js');
const misc = require('../util/misc.js');
const api = require("../api.js");
const events = require("../events.js");
const views = require("../util/views.js");
const uri = require("../util/uri.js");
const misc = require("../util/misc.js");
const template = views.getTemplate('post-readonly-sidebar');
const scoreTemplate = views.getTemplate('score');
const favTemplate = views.getTemplate('fav');
const template = views.getTemplate("post-readonly-sidebar");
const scoreTemplate = views.getTemplate("score");
const favTemplate = views.getTemplate("fav");
class PostReadonlySidebarControl extends events.EventTarget {
constructor(hostNode, post, postContentControl) {
@ -17,19 +17,22 @@ class PostReadonlySidebarControl extends events.EventTarget {
this._post = post;
this._postContentControl = postContentControl;
post.addEventListener('changeFavorite', e => this._evtChangeFav(e));
post.addEventListener('changeScore', e => this._evtChangeScore(e));
post.addEventListener("changeFavorite", (e) => this._evtChangeFav(e));
post.addEventListener("changeScore", (e) => this._evtChangeScore(e));
views.replaceContent(this._hostNode, template({
post: this._post,
enableSafety: api.safetyEnabled(),
canListPosts: api.hasPrivilege('posts:list'),
canEditPosts: api.hasPrivilege('posts:edit'),
canViewTags: api.hasPrivilege('tags:view'),
escapeColons: uri.escapeColons,
extractRootDomain: uri.extractRootDomain,
getPrettyTagName: misc.getPrettyTagName,
}));
views.replaceContent(
this._hostNode,
template({
post: this._post,
enableSafety: api.safetyEnabled(),
canListPosts: api.hasPrivilege("posts:list"),
canEditPosts: api.hasPrivilege("posts:edit"),
canViewTags: api.hasPrivilege("tags:view"),
escapeColons: uri.escapeColons,
extractRootDomain: uri.extractRootDomain,
getPrettyTagName: misc.getPrettyTagName,
})
);
this._installFav();
this._installScore();
@ -38,58 +41,62 @@ class PostReadonlySidebarControl extends events.EventTarget {
}
get _scoreContainerNode() {
return this._hostNode.querySelector('.score-container');
return this._hostNode.querySelector(".score-container");
}
get _favContainerNode() {
return this._hostNode.querySelector('.fav-container');
return this._hostNode.querySelector(".fav-container");
}
get _upvoteButtonNode() {
return this._hostNode.querySelector('.upvote');
return this._hostNode.querySelector(".upvote");
}
get _downvoteButtonNode() {
return this._hostNode.querySelector('.downvote');
return this._hostNode.querySelector(".downvote");
}
get _addFavButtonNode() {
return this._hostNode.querySelector('.add-favorite');
return this._hostNode.querySelector(".add-favorite");
}
get _remFavButtonNode() {
return this._hostNode.querySelector('.remove-favorite');
return this._hostNode.querySelector(".remove-favorite");
}
get _fitBothButtonNode() {
return this._hostNode.querySelector('.fit-both');
return this._hostNode.querySelector(".fit-both");
}
get _fitOriginalButtonNode() {
return this._hostNode.querySelector('.fit-original');
return this._hostNode.querySelector(".fit-original");
}
get _fitWidthButtonNode() {
return this._hostNode.querySelector('.fit-width');
return this._hostNode.querySelector(".fit-width");
}
get _fitHeightButtonNode() {
return this._hostNode.querySelector('.fit-height');
return this._hostNode.querySelector(".fit-height");
}
_installFitButtons() {
this._fitBothButtonNode.addEventListener(
'click', this._eventZoomProxy(
() => this._postContentControl.fitBoth()));
"click",
this._eventZoomProxy(() => this._postContentControl.fitBoth())
);
this._fitOriginalButtonNode.addEventListener(
'click', this._eventZoomProxy(
() => this._postContentControl.fitOriginal()));
"click",
this._eventZoomProxy(() => this._postContentControl.fitOriginal())
);
this._fitWidthButtonNode.addEventListener(
'click', this._eventZoomProxy(
() => this._postContentControl.fitWidth()));
"click",
this._eventZoomProxy(() => this._postContentControl.fitWidth())
);
this._fitHeightButtonNode.addEventListener(
'click', this._eventZoomProxy(
() => this._postContentControl.fitHeight()));
"click",
this._eventZoomProxy(() => this._postContentControl.fitHeight())
);
}
_installFav() {
@ -98,16 +105,19 @@ class PostReadonlySidebarControl extends events.EventTarget {
favTemplate({
favoriteCount: this._post.favoriteCount,
ownFavorite: this._post.ownFavorite,
canFavorite: api.hasPrivilege('posts:favorite'),
}));
canFavorite: api.hasPrivilege("posts:favorite"),
})
);
if (this._addFavButtonNode) {
this._addFavButtonNode.addEventListener(
'click', e => this._evtAddToFavoritesClick(e));
this._addFavButtonNode.addEventListener("click", (e) =>
this._evtAddToFavoritesClick(e)
);
}
if (this._remFavButtonNode) {
this._remFavButtonNode.addEventListener(
'click', e => this._evtRemoveFromFavoritesClick(e));
this._remFavButtonNode.addEventListener("click", (e) =>
this._evtRemoveFromFavoritesClick(e)
);
}
}
@ -117,77 +127,88 @@ class PostReadonlySidebarControl extends events.EventTarget {
scoreTemplate({
score: this._post.score,
ownScore: this._post.ownScore,
canScore: api.hasPrivilege('posts:score'),
}));
canScore: api.hasPrivilege("posts:score"),
})
);
if (this._upvoteButtonNode) {
this._upvoteButtonNode.addEventListener(
'click', e => this._evtScoreClick(e, 1));
this._upvoteButtonNode.addEventListener("click", (e) =>
this._evtScoreClick(e, 1)
);
}
if (this._downvoteButtonNode) {
this._downvoteButtonNode.addEventListener(
'click', e => this._evtScoreClick(e, -1));
this._downvoteButtonNode.addEventListener("click", (e) =>
this._evtScoreClick(e, -1)
);
}
}
_eventZoomProxy(func) {
return e => {
return (e) => {
e.preventDefault();
e.target.blur();
func();
this._syncFitButton();
this.dispatchEvent(new CustomEvent('fitModeChange', {
detail: {
mode: this._getFitMode(),
},
}));
this.dispatchEvent(
new CustomEvent("fitModeChange", {
detail: {
mode: this._getFitMode(),
},
})
);
};
}
_getFitMode() {
const funcToName = {};
funcToName[this._postContentControl.fitBoth] = 'fit-both';
funcToName[this._postContentControl.fitOriginal] = 'fit-original';
funcToName[this._postContentControl.fitWidth] = 'fit-width';
funcToName[this._postContentControl.fitHeight] = 'fit-height';
funcToName[this._postContentControl.fitBoth] = "fit-both";
funcToName[this._postContentControl.fitOriginal] = "fit-original";
funcToName[this._postContentControl.fitWidth] = "fit-width";
funcToName[this._postContentControl.fitHeight] = "fit-height";
return funcToName[this._postContentControl._currentFitFunction];
}
_syncFitButton() {
const className = this._getFitMode();
const oldNode = this._hostNode.querySelector('.zoom a.active');
const oldNode = this._hostNode.querySelector(".zoom a.active");
const newNode = this._hostNode.querySelector(`.zoom a.${className}`);
if (oldNode) {
oldNode.classList.remove('active');
oldNode.classList.remove("active");
}
newNode.classList.add('active');
newNode.classList.add("active");
}
_evtAddToFavoritesClick(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('favorite', {
detail: {
post: this._post,
},
}));
this.dispatchEvent(
new CustomEvent("favorite", {
detail: {
post: this._post,
},
})
);
}
_evtRemoveFromFavoritesClick(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('unfavorite', {
detail: {
post: this._post,
},
}));
this.dispatchEvent(
new CustomEvent("unfavorite", {
detail: {
post: this._post,
},
})
);
}
_evtScoreClick(e, score) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('score', {
detail: {
post: this._post,
score: this._post.ownScore === score ? 0 : score,
},
}));
this.dispatchEvent(
new CustomEvent("score", {
detail: {
post: this._post,
score: this._post.ownScore === score ? 0 : score,
},
})
);
}
_evtChangeFav(e) {

View file

@ -1,51 +1,63 @@
'use strict';
"use strict";
const misc = require('../util/misc.js');
const views = require('../util/views.js');
const TagList = require('../models/tag_list.js');
const AutoCompleteControl = require('./auto_complete_control.js');
const misc = require("../util/misc.js");
const views = require("../util/views.js");
const TagList = require("../models/tag_list.js");
const AutoCompleteControl = require("./auto_complete_control.js");
function _tagListToMatches(tags, options) {
return [...tags].sort((tag1, tag2) => {
return tag2.usages - tag1.usages;
}).map(tag => {
let cssName = misc.makeCssName(tag.category, 'tag');
if (options.isTaggedWith(tag.names[0])) {
cssName += ' disabled';
}
const caption = (
'<span class="' + cssName + '">'
+ misc.escapeHtml(tag.names[0] + ' (' + tag.postCount + ')')
+ '</span>');
return {
caption: caption,
value: tag,
};
});
return [...tags]
.sort((tag1, tag2) => {
return tag2.usages - tag1.usages;
})
.map((tag) => {
let cssName = misc.makeCssName(tag.category, "tag");
if (options.isTaggedWith(tag.names[0])) {
cssName += " disabled";
}
const caption =
'<span class="' +
cssName +
'">' +
misc.escapeHtml(tag.names[0] + " (" + tag.postCount + ")") +
"</span>";
return {
caption: caption,
value: tag,
};
});
}
class TagAutoCompleteControl extends AutoCompleteControl {
constructor(input, options) {
const minLengthForPartialSearch = 3;
options = Object.assign({
isTaggedWith: tag => false,
}, options);
options = Object.assign(
{
isTaggedWith: (tag) => false,
},
options
);
options.getMatches = text => {
options.getMatches = (text) => {
const term = misc.escapeSearchTerm(text);
const query = (
text.length < minLengthForPartialSearch
? term + '*'
: '*' + term + '*') + ' sort:usages';
const query =
(text.length < minLengthForPartialSearch
? term + "*"
: "*" + term + "*") + " sort:usages";
return new Promise((resolve, reject) => {
TagList.search(
query, 0, this._options.maxResults, ['names', 'category', 'usages'])
.then(
response => resolve(
_tagListToMatches(response.results, this._options)),
reject);
TagList.search(query, 0, this._options.maxResults, [
"names",
"category",
"usages",
]).then(
(response) =>
resolve(
_tagListToMatches(response.results, this._options)
),
reject
);
});
};

View file

@ -1,25 +1,25 @@
'use strict';
"use strict";
const api = require('../api.js');
const tags = require('../tags.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const Tag = require('../models/tag.js');
const settings = require('../models/settings.js');
const events = require('../events.js');
const views = require('../util/views.js');
const TagAutoCompleteControl = require('./tag_auto_complete_control.js');
const api = require("../api.js");
const tags = require("../tags.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const Tag = require("../models/tag.js");
const settings = require("../models/settings.js");
const events = require("../events.js");
const views = require("../util/views.js");
const TagAutoCompleteControl = require("./tag_auto_complete_control.js");
const KEY_SPACE = 32;
const KEY_RETURN = 13;
const SOURCE_INIT = 'init';
const SOURCE_IMPLICATION = 'implication';
const SOURCE_USER_INPUT = 'user-input';
const SOURCE_SUGGESTION = 'suggestions';
const SOURCE_CLIPBOARD = 'clipboard';
const SOURCE_INIT = "init";
const SOURCE_IMPLICATION = "implication";
const SOURCE_USER_INPUT = "user-input";
const SOURCE_SUGGESTION = "suggestions";
const SOURCE_CLIPBOARD = "clipboard";
const template = views.getTemplate('tag-input');
const template = views.getTemplate("tag-input");
function _fadeOutListItemNodeStatus(listItemNode) {
if (listItemNode.classList.length) {
@ -28,8 +28,7 @@ function _fadeOutListItemNodeStatus(listItemNode) {
}
listItemNode.fadeTimeout = window.setTimeout(() => {
while (listItemNode.classList.length) {
listItemNode.classList.remove(
listItemNode.classList.item(0));
listItemNode.classList.remove(listItemNode.classList.item(0));
}
listItemNode.fadeTimeout = null;
}, 2500);
@ -51,7 +50,9 @@ class SuggestionList {
}
set(suggestion, weight) {
if (Object.prototype.hasOwnProperty.call(this._suggestions, suggestion)) {
if (
Object.prototype.hasOwnProperty.call(this._suggestions, suggestion)
) {
weight = Math.max(weight, this._suggestions[suggestion]);
}
this._suggestions[suggestion] = weight;
@ -74,8 +75,8 @@ class SuggestionList {
let nameDiff = a[0].localeCompare(b[0]);
return weightDiff === 0 ? nameDiff : weightDiff;
});
return tuples.map(tuple => {
return {tagName: tuple[0], weight: tuple[1]};
return tuples.map((tuple) => {
return { tagName: tuple[0], weight: tuple[1] };
});
}
}
@ -91,45 +92,58 @@ class TagInputControl extends events.EventTarget {
// dom
const editAreaNode = template();
this._editAreaNode = editAreaNode;
this._tagInputNode = editAreaNode.querySelector('input');
this._suggestionsNode = editAreaNode.querySelector('.tag-suggestions');
this._tagListNode = editAreaNode.querySelector('ul.compact-tags');
this._tagInputNode = editAreaNode.querySelector("input");
this._suggestionsNode = editAreaNode.querySelector(".tag-suggestions");
this._tagListNode = editAreaNode.querySelector("ul.compact-tags");
this._autoCompleteControl = new TagAutoCompleteControl(
this._tagInputNode, {
this._tagInputNode,
{
getTextToFind: () => {
return this._tagInputNode.value;
},
confirm: tag => {
this._tagInputNode.value = '';
confirm: (tag) => {
this._tagInputNode.value = "";
// note: tags from autocomplete don't contain implications
// so they need to be looked up in API
this.addTagByName(tag.names[0], SOURCE_USER_INPUT);
},
delete: tag => {
this._tagInputNode.value = '';
delete: (tag) => {
this._tagInputNode.value = "";
this.deleteTag(tag);
},
verticalShift: -2,
isTaggedWith: tagName => this.tags.isTaggedWith(tagName),
});
isTaggedWith: (tagName) => this.tags.isTaggedWith(tagName),
}
);
// dom events
this._tagInputNode.addEventListener(
'keydown', e => this._evtInputKeyDown(e));
this._tagInputNode.addEventListener(
'paste', e => this._evtInputPaste(e));
this._editAreaNode.querySelector('a.opacity').addEventListener(
'click', e => this._evtToggleSuggestionsPopupOpacityClick(e));
this._editAreaNode.querySelector('a.close').addEventListener(
'click', e => this._evtCloseSuggestionsPopupClick(e));
this._editAreaNode.querySelector('button').addEventListener(
'click', e => this._evtAddTagButtonClick(e));
this._tagInputNode.addEventListener("keydown", (e) =>
this._evtInputKeyDown(e)
);
this._tagInputNode.addEventListener("paste", (e) =>
this._evtInputPaste(e)
);
this._editAreaNode
.querySelector("a.opacity")
.addEventListener("click", (e) =>
this._evtToggleSuggestionsPopupOpacityClick(e)
);
this._editAreaNode
.querySelector("a.close")
.addEventListener("click", (e) =>
this._evtCloseSuggestionsPopupClick(e)
);
this._editAreaNode
.querySelector("button")
.addEventListener("click", (e) => this._evtAddTagButtonClick(e));
// show
this._hostNode.style.display = 'none';
this._hostNode.style.display = "none";
this._hostNode.parentNode.insertBefore(
this._editAreaNode, hostNode.nextSibling);
this._editAreaNode,
hostNode.nextSibling
);
// add existing tags
for (let tag of [...this.tags]) {
@ -139,7 +153,10 @@ class TagInputControl extends events.EventTarget {
}
addTagByText(text, source) {
for (let tagName of text.split(/\s+/).filter(word => word).reverse()) {
for (let tagName of text
.split(/\s+/)
.filter((word) => word)
.reverse()) {
this.addTagByName(tagName, source);
}
}
@ -149,48 +166,60 @@ class TagInputControl extends events.EventTarget {
if (!name) {
return;
}
return Tag.get(name).then(tag => {
return this.addTag(tag, source);
}, () => {
const tag = new Tag();
tag.names = [name];
tag.category = null;
return this.addTag(tag, source);
});
return Tag.get(name).then(
(tag) => {
return this.addTag(tag, source);
},
() => {
const tag = new Tag();
tag.names = [name];
tag.category = null;
return this.addTag(tag, source);
}
);
}
addTag(tag, source) {
if (source !== SOURCE_INIT && this.tags.isTaggedWith(tag.names[0])) {
const listItemNode = this._getListItemNode(tag);
if (source !== SOURCE_IMPLICATION) {
listItemNode.classList.add('duplicate');
listItemNode.classList.add("duplicate");
_fadeOutListItemNodeStatus(listItemNode);
}
return Promise.resolve();
}
return this.tags.addByName(tag.names[0], false).then(() => {
const listItemNode = this._createListItemNode(tag);
if (!tag.category) {
listItemNode.classList.add('new');
}
if (source === SOURCE_IMPLICATION) {
listItemNode.classList.add('implication');
}
this._tagListNode.prependChild(listItemNode);
_fadeOutListItemNodeStatus(listItemNode);
return this.tags
.addByName(tag.names[0], false)
.then(() => {
const listItemNode = this._createListItemNode(tag);
if (!tag.category) {
listItemNode.classList.add("new");
}
if (source === SOURCE_IMPLICATION) {
listItemNode.classList.add("implication");
}
this._tagListNode.prependChild(listItemNode);
_fadeOutListItemNodeStatus(listItemNode);
return Promise.all(
tag.implications.map(
implication => this.addTagByName(
implication.names[0], SOURCE_IMPLICATION)));
}).then(() => {
this.dispatchEvent(new CustomEvent('add', {
detail: {tag: tag, source: source},
}));
this.dispatchEvent(new CustomEvent('change'));
return Promise.resolve();
});
return Promise.all(
tag.implications.map((implication) =>
this.addTagByName(
implication.names[0],
SOURCE_IMPLICATION
)
)
);
})
.then(() => {
this.dispatchEvent(
new CustomEvent("add", {
detail: { tag: tag, source: source },
})
);
this.dispatchEvent(new CustomEvent("change"));
return Promise.resolve();
});
}
deleteTag(tag) {
@ -202,25 +231,27 @@ class TagInputControl extends events.EventTarget {
this._deleteListItemNode(tag);
this.dispatchEvent(new CustomEvent('remove', {
detail: {tag: tag},
}));
this.dispatchEvent(new CustomEvent('change'));
this.dispatchEvent(
new CustomEvent("remove", {
detail: { tag: tag },
})
);
this.dispatchEvent(new CustomEvent("change"));
}
_evtInputPaste(e) {
e.preventDefault();
const pastedText = window.clipboardData ?
window.clipboardData.getData('Text') :
(e.originalEvent || e).clipboardData.getData('text/plain');
const pastedText = window.clipboardData
? window.clipboardData.getData("Text")
: (e.originalEvent || e).clipboardData.getData("text/plain");
if (pastedText.length > 2000) {
window.alert('Pasted text is too long.');
window.alert("Pasted text is too long.");
return;
}
this._hideAutoComplete();
this.addTagByText(pastedText, SOURCE_CLIPBOARD);
this._tagInputNode.value = '';
this._tagInputNode.value = "";
}
_evtCloseSuggestionsPopupClick(e) {
@ -231,7 +262,7 @@ class TagInputControl extends events.EventTarget {
_evtAddTagButtonClick(e) {
e.preventDefault();
this.addTagByName(this._tagInputNode.value, SOURCE_USER_INPUT);
this._tagInputNode.value = '';
this._tagInputNode.value = "";
}
_evtToggleSuggestionsPopupOpacityClick(e) {
@ -244,36 +275,41 @@ class TagInputControl extends events.EventTarget {
e.preventDefault();
this._hideAutoComplete();
this.addTagByText(this._tagInputNode.value, SOURCE_USER_INPUT);
this._tagInputNode.value = '';
this._tagInputNode.value = "";
}
}
_createListItemNode(tag) {
const className = tag.category ?
misc.makeCssName(tag.category, 'tag') :
null;
const className = tag.category
? misc.makeCssName(tag.category, "tag")
: null;
const tagLinkNode = document.createElement('a');
const tagLinkNode = document.createElement("a");
if (className) {
tagLinkNode.classList.add(className);
}
tagLinkNode.setAttribute(
'href', uri.formatClientLink('tag', tag.names[0]));
"href",
uri.formatClientLink("tag", tag.names[0])
);
const tagIconNode = document.createElement('i');
tagIconNode.classList.add('fa');
tagIconNode.classList.add('fa-tag');
const tagIconNode = document.createElement("i");
tagIconNode.classList.add("fa");
tagIconNode.classList.add("fa-tag");
tagLinkNode.appendChild(tagIconNode);
const searchLinkNode = document.createElement('a');
const searchLinkNode = document.createElement("a");
if (className) {
searchLinkNode.classList.add(className);
}
searchLinkNode.setAttribute(
'href', uri.formatClientLink(
'posts', {query: uri.escapeColons(tag.names[0])}));
searchLinkNode.textContent = tag.names[0] + ' ';
searchLinkNode.addEventListener('click', e => {
"href",
uri.formatClientLink("posts", {
query: uri.escapeColons(tag.names[0]),
})
);
searchLinkNode.textContent = tag.names[0] + " ";
searchLinkNode.addEventListener("click", (e) => {
e.preventDefault();
this._suggestions.clear();
if (tag.postCount > 0) {
@ -284,20 +320,20 @@ class TagInputControl extends events.EventTarget {
}
});
const usagesNode = document.createElement('span');
usagesNode.classList.add('tag-usages');
usagesNode.setAttribute('data-pseudo-content', tag.postCount);
const usagesNode = document.createElement("span");
usagesNode.classList.add("tag-usages");
usagesNode.setAttribute("data-pseudo-content", tag.postCount);
const removalLinkNode = document.createElement('a');
removalLinkNode.classList.add('remove-tag');
removalLinkNode.setAttribute('href', '');
removalLinkNode.setAttribute('data-pseudo-content', '×');
removalLinkNode.addEventListener('click', e => {
const removalLinkNode = document.createElement("a");
removalLinkNode.classList.add("remove-tag");
removalLinkNode.setAttribute("href", "");
removalLinkNode.setAttribute("data-pseudo-content", "×");
removalLinkNode.addEventListener("click", (e) => {
e.preventDefault();
this.deleteTag(tag);
});
const listItemNode = document.createElement('li');
const listItemNode = document.createElement("li");
listItemNode.appendChild(removalLinkNode);
listItemNode.appendChild(tagLinkNode);
listItemNode.appendChild(searchLinkNode);
@ -327,20 +363,25 @@ class TagInputControl extends events.EventTarget {
if (!browsingSettings.tagSuggestions) {
return;
}
api.get(
uri.formatApiLink('tag-siblings', tag.names[0]),
{noProgress: true})
.then(response => {
return Promise.resolve(response.results);
}, response => {
return Promise.resolve([]);
}).then(siblings => {
const args = siblings.map(s => s.occurrences);
api.get(uri.formatApiLink("tag-siblings", tag.names[0]), {
noProgress: true,
})
.then(
(response) => {
return Promise.resolve(response.results);
},
(response) => {
return Promise.resolve([]);
}
)
.then((siblings) => {
const args = siblings.map((s) => s.occurrences);
let maxSiblingOccurrences = Math.max(1, ...args);
for (let sibling of siblings) {
this._suggestions.set(
sibling.tag.names[0],
sibling.occurrences * 4.9 / maxSiblingOccurrences);
(sibling.occurrences * 4.9) / maxSiblingOccurrences
);
}
for (let suggestion of tag.suggestions || []) {
this._suggestions.set(suggestion, 5);
@ -354,10 +395,10 @@ class TagInputControl extends events.EventTarget {
}
_refreshSuggestionsPopup() {
if (!this._suggestionsNode.classList.contains('shown')) {
if (!this._suggestionsNode.classList.contains("shown")) {
return;
}
const listNode = this._suggestionsNode.querySelector('ul');
const listNode = this._suggestionsNode.querySelector("ul");
listNode.scrollTop = 0;
while (listNode.firstChild) {
listNode.removeChild(listNode.firstChild);
@ -369,35 +410,36 @@ class TagInputControl extends events.EventTarget {
continue;
}
const addLinkNode = document.createElement('a');
const addLinkNode = document.createElement("a");
addLinkNode.textContent = tagName;
addLinkNode.classList.add('add-tag');
addLinkNode.setAttribute('href', '');
Tag.get(tagName).then(tag => {
addLinkNode.classList.add("add-tag");
addLinkNode.setAttribute("href", "");
Tag.get(tagName).then((tag) => {
addLinkNode.classList.add(
misc.makeCssName(tag.category, 'tag'));
misc.makeCssName(tag.category, "tag")
);
});
addLinkNode.addEventListener('click', e => {
addLinkNode.addEventListener("click", (e) => {
e.preventDefault();
listNode.removeChild(listItemNode);
this.addTagByName(tagName, SOURCE_SUGGESTION);
});
const weightNode = document.createElement('span');
weightNode.classList.add('tag-weight');
weightNode.setAttribute('data-pseudo-content', weight);
const weightNode = document.createElement("span");
weightNode.classList.add("tag-weight");
weightNode.setAttribute("data-pseudo-content", weight);
const removalLinkNode = document.createElement('a');
removalLinkNode.classList.add('remove-tag');
removalLinkNode.setAttribute('href', '');
removalLinkNode.setAttribute('data-pseudo-content', '×');
removalLinkNode.addEventListener('click', e => {
const removalLinkNode = document.createElement("a");
removalLinkNode.classList.add("remove-tag");
removalLinkNode.setAttribute("href", "");
removalLinkNode.setAttribute("data-pseudo-content", "×");
removalLinkNode.addEventListener("click", (e) => {
e.preventDefault();
listNode.removeChild(listItemNode);
this._suggestions.ban(tagName);
});
const listItemNode = document.createElement('li');
const listItemNode = document.createElement("li");
listItemNode.appendChild(removalLinkNode);
listItemNode.appendChild(weightNode);
listItemNode.appendChild(addLinkNode);
@ -407,19 +449,19 @@ class TagInputControl extends events.EventTarget {
_closeSuggestionsPopup() {
this._suggestions.clear();
this._suggestionsNode.classList.remove('shown');
this._suggestionsNode.classList.remove("shown");
}
_removeSuggestionsPopupOpacity() {
this._suggestionsNode.classList.remove('translucent');
this._suggestionsNode.classList.remove("translucent");
}
_toggleSuggestionsPopupOpacity() {
this._suggestionsNode.classList.toggle('translucent');
this._suggestionsNode.classList.toggle("translucent");
}
_openSuggestionsPopup() {
this._suggestionsNode.classList.add('shown');
this._suggestionsNode.classList.add("shown");
this._refreshSuggestionsPopup();
}

View file

@ -1,12 +1,12 @@
'use strict';
"use strict";
class EventTarget {
constructor() {
this.eventTarget = document.createDocumentFragment();
for (let method of [
'addEventListener',
'dispatchEvent',
'removeEventListener'
"addEventListener",
"dispatchEvent",
"removeEventListener",
]) {
this[method] = this.eventTarget[method].bind(this.eventTarget);
}
@ -20,17 +20,19 @@ function proxyEvent(source, target, sourceEventType, targetEventType) {
if (!targetEventType) {
targetEventType = sourceEventType;
}
source.addEventListener(sourceEventType, e => {
target.dispatchEvent(new CustomEvent(targetEventType, {
detail: e.detail,
}));
source.addEventListener(sourceEventType, (e) => {
target.dispatchEvent(
new CustomEvent(targetEventType, {
detail: e.detail,
})
);
});
}
module.exports = {
Success: 'success',
Error: 'error',
Info: 'info',
Success: "success",
Error: "error",
Info: "info",
proxyEvent: proxyEvent,
EventTarget: EventTarget,

View file

@ -1,82 +1,101 @@
'use strict';
"use strict";
require('./util/polyfill.js');
const misc = require('./util/misc.js');
const views = require('./util/views.js');
const router = require('./router.js');
require("./util/polyfill.js");
const misc = require("./util/misc.js");
const views = require("./util/views.js");
const router = require("./router.js");
history.scrollRestoration = 'manual';
history.scrollRestoration = "manual";
router.exit(
null,
(ctx, next) => {
ctx.state.scrollX = window.scrollX;
ctx.state.scrollY = window.scrollY;
router.replace(router.url, ctx.state);
if (misc.confirmPageExit()) {
next();
}
});
const mousetrap = require('mousetrap');
router.enter(
null,
(ctx, next) => {
mousetrap.reset();
router.exit(null, (ctx, next) => {
ctx.state.scrollX = window.scrollX;
ctx.state.scrollY = window.scrollY;
router.replace(router.url, ctx.state);
if (misc.confirmPageExit()) {
next();
});
const tags = require('./tags.js');
const pools = require('./pools.js');
const api = require('./api.js');
api.fetchConfig().then(() => {
// register controller routes
let controllers = [];
controllers.push(require('./controllers/home_controller.js'));
controllers.push(require('./controllers/help_controller.js'));
controllers.push(require('./controllers/auth_controller.js'));
controllers.push(require('./controllers/password_reset_controller.js'));
controllers.push(require('./controllers/comments_controller.js'));
controllers.push(require('./controllers/snapshots_controller.js'));
controllers.push(require('./controllers/post_detail_controller.js'));
controllers.push(require('./controllers/post_main_controller.js'));
controllers.push(require('./controllers/post_list_controller.js'));
controllers.push(require('./controllers/post_upload_controller.js'));
controllers.push(require('./controllers/tag_controller.js'));
controllers.push(require('./controllers/tag_list_controller.js'));
controllers.push(require('./controllers/tag_categories_controller.js'));
controllers.push(require('./controllers/pool_create_controller.js'));
controllers.push(require('./controllers/pool_controller.js'));
controllers.push(require('./controllers/pool_list_controller.js'));
controllers.push(require('./controllers/pool_categories_controller.js'));
controllers.push(require('./controllers/settings_controller.js'));
controllers.push(require('./controllers/user_controller.js'));
controllers.push(require('./controllers/user_list_controller.js'));
controllers.push(require('./controllers/user_registration_controller.js'));
// 404 controller needs to be registered last
controllers.push(require('./controllers/not_found_controller.js'));
for (let controller of controllers) {
controller(router);
}
}, error => {
window.alert('Could not fetch basic configuration from server');
}).then(() => {
api.loginFromCookies().then(() => {
tags.refreshCategoryColorMap();
pools.refreshCategoryColorMap();
router.start();
}, error => {
if (window.location.href.indexOf('login') !== -1) {
api.forget();
router.start();
} else {
const ctx = router.start('/');
ctx.controller.showError(
'An error happened while trying to log you in: ' +
error.message);
}
});
});
const mousetrap = require("mousetrap");
router.enter(null, (ctx, next) => {
mousetrap.reset();
next();
});
const tags = require("./tags.js");
const pools = require("./pools.js");
const api = require("./api.js");
api.fetchConfig()
.then(
() => {
// register controller routes
let controllers = [];
controllers.push(require("./controllers/home_controller.js"));
controllers.push(require("./controllers/help_controller.js"));
controllers.push(require("./controllers/auth_controller.js"));
controllers.push(
require("./controllers/password_reset_controller.js")
);
controllers.push(require("./controllers/comments_controller.js"));
controllers.push(require("./controllers/snapshots_controller.js"));
controllers.push(
require("./controllers/post_detail_controller.js")
);
controllers.push(require("./controllers/post_main_controller.js"));
controllers.push(require("./controllers/post_list_controller.js"));
controllers.push(
require("./controllers/post_upload_controller.js")
);
controllers.push(require("./controllers/tag_controller.js"));
controllers.push(require("./controllers/tag_list_controller.js"));
controllers.push(
require("./controllers/tag_categories_controller.js")
);
controllers.push(
require("./controllers/pool_create_controller.js")
);
controllers.push(require("./controllers/pool_controller.js"));
controllers.push(require("./controllers/pool_list_controller.js"));
controllers.push(
require("./controllers/pool_categories_controller.js")
);
controllers.push(require("./controllers/settings_controller.js"));
controllers.push(require("./controllers/user_controller.js"));
controllers.push(require("./controllers/user_list_controller.js"));
controllers.push(
require("./controllers/user_registration_controller.js")
);
// 404 controller needs to be registered last
controllers.push(require("./controllers/not_found_controller.js"));
for (let controller of controllers) {
controller(router);
}
},
(error) => {
window.alert("Could not fetch basic configuration from server");
}
)
.then(() => {
api.loginFromCookies().then(
() => {
tags.refreshCategoryColorMap();
pools.refreshCategoryColorMap();
router.start();
},
(error) => {
if (window.location.href.indexOf("login") !== -1) {
api.forget();
router.start();
} else {
const ctx = router.start("/");
ctx.controller.showError(
"An error happened while trying to log you in: " +
error.message
);
}
}
);
});

View file

@ -1,6 +1,6 @@
'use strict';
"use strict";
const events = require('../events.js');
const events = require("../events.js");
class AbstractList extends events.EventTarget {
constructor() {
@ -13,13 +13,15 @@ class AbstractList extends events.EventTarget {
for (let item of response) {
const addedItem = this._itemClass.fromResponse(item);
if (addedItem.addEventListener) {
addedItem.addEventListener('delete', e => {
addedItem.addEventListener("delete", (e) => {
ret.remove(addedItem);
});
addedItem.addEventListener('change', e => {
ret.dispatchEvent(new CustomEvent('change', {
detail: e.detail,
}));
addedItem.addEventListener("change", (e) => {
ret.dispatchEvent(
new CustomEvent("change", {
detail: e.detail,
})
);
});
}
ret._list.push(addedItem);
@ -29,28 +31,32 @@ class AbstractList extends events.EventTarget {
sync(plainList) {
this.clear();
for (let item of (plainList || [])) {
for (let item of plainList || []) {
this.add(this.constructor._itemClass.fromResponse(item));
}
}
add(item) {
if (item.addEventListener) {
item.addEventListener('delete', e => {
item.addEventListener("delete", (e) => {
this.remove(item);
});
item.addEventListener('change', e => {
this.dispatchEvent(new CustomEvent('change', {
detail: e.detail,
}));
item.addEventListener("change", (e) => {
this.dispatchEvent(
new CustomEvent("change", {
detail: e.detail,
})
);
});
}
this._list.push(item);
const detail = {};
detail[this.constructor._itemName] = item;
this.dispatchEvent(new CustomEvent('add', {
detail: detail,
}));
this.dispatchEvent(
new CustomEvent("add", {
detail: detail,
})
);
}
clear() {
@ -67,9 +73,11 @@ class AbstractList extends events.EventTarget {
this._list.splice(index, 1);
const detail = {};
detail[this.constructor._itemName] = itemToRemove;
this.dispatchEvent(new CustomEvent('remove', {
detail: detail,
}));
this.dispatchEvent(
new CustomEvent("remove", {
detail: detail,
})
);
return;
}
}

View file

@ -1,8 +1,8 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const events = require('../events.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const events = require("../events.js");
class Comment extends events.EventTarget {
constructor() {
@ -31,7 +31,7 @@ class Comment extends events.EventTarget {
}
get text() {
return this._text || '';
return this._text || "";
}
get user() {
@ -63,47 +63,57 @@ class Comment extends events.EventTarget {
version: this._version,
text: this._text,
};
let promise = this._id ?
api.put(uri.formatApiLink('comment', this.id), detail) :
api.post(uri.formatApiLink('comments'),
Object.assign({postId: this._postId}, detail));
let promise = this._id
? api.put(uri.formatApiLink("comment", this.id), detail)
: api.post(
uri.formatApiLink("comments"),
Object.assign({ postId: this._postId }, detail)
);
return promise.then(response => {
return promise.then((response) => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
detail: {
comment: this,
},
}));
this.dispatchEvent(
new CustomEvent("change", {
detail: {
comment: this,
},
})
);
return Promise.resolve();
});
}
delete() {
return api.delete(
uri.formatApiLink('comment', this.id),
{version: this._version})
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
comment: this,
},
}));
return api
.delete(uri.formatApiLink("comment", this.id), {
version: this._version,
})
.then((response) => {
this.dispatchEvent(
new CustomEvent("delete", {
detail: {
comment: this,
},
})
);
return Promise.resolve();
});
}
setScore(score) {
return api.put(
uri.formatApiLink('comment', this.id, 'score'),
{score: score})
.then(response => {
return api
.put(uri.formatApiLink("comment", this.id, "score"), {
score: score,
})
.then((response) => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('changeScore', {
detail: {
comment: this,
},
}));
this.dispatchEvent(
new CustomEvent("changeScore", {
detail: {
comment: this,
},
})
);
return Promise.resolve();
});
}

View file

@ -1,12 +1,11 @@
'use strict';
"use strict";
const AbstractList = require('./abstract_list.js');
const Comment = require('./comment.js');
const AbstractList = require("./abstract_list.js");
const Comment = require("./comment.js");
class CommentList extends AbstractList {
}
class CommentList extends AbstractList {}
CommentList._itemClass = Comment;
CommentList._itemName = 'comment';
CommentList._itemName = "comment";
module.exports = CommentList;

View file

@ -1,22 +1,20 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const Post = require('./post.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const Post = require("./post.js");
class Info {
static get() {
return api.get(uri.formatApiLink('info'))
.then(response => {
return Promise.resolve(Object.assign(
{},
response,
{
featuredPost: response.featuredPost ?
Post.fromResponse(response.featuredPost) :
undefined
}));
});
return api.get(uri.formatApiLink("info")).then((response) => {
return Promise.resolve(
Object.assign({}, response, {
featuredPost: response.featuredPost
? Post.fromResponse(response.featuredPost)
: undefined,
})
);
});
}
}

View file

@ -1,13 +1,13 @@
'use strict';
"use strict";
const events = require('../events.js');
const Point = require('./point.js');
const PointList = require('./point_list.js');
const events = require("../events.js");
const Point = require("./point.js");
const PointList = require("./point_list.js");
class Note extends events.EventTarget {
constructor() {
super();
this._text = '…';
this._text = "…";
this._polygon = new PointList();
}

View file

@ -1,12 +1,11 @@
'use strict';
"use strict";
const AbstractList = require('./abstract_list.js');
const Note = require('./note.js');
const AbstractList = require("./abstract_list.js");
const Note = require("./note.js");
class NoteList extends AbstractList {
}
class NoteList extends AbstractList {}
NoteList._itemClass = Note;
NoteList._itemName = 'note';
NoteList._itemName = "note";
module.exports = NoteList;

View file

@ -1,6 +1,6 @@
'use strict';
"use strict";
const events = require('../events.js');
const events = require("../events.js");
class Point extends events.EventTarget {
constructor(x, y) {
@ -19,12 +19,16 @@ class Point extends events.EventTarget {
set x(value) {
this._x = value;
this.dispatchEvent(new CustomEvent('change', {detail: {point: this}}));
this.dispatchEvent(
new CustomEvent("change", { detail: { point: this } })
);
}
set y(value) {
this._y = value;
this.dispatchEvent(new CustomEvent('change', {detail: {point: this}}));
this.dispatchEvent(
new CustomEvent("change", { detail: { point: this } })
);
}
}

View file

@ -1,7 +1,7 @@
'use strict';
"use strict";
const AbstractList = require('./abstract_list.js');
const Point = require('./point.js');
const AbstractList = require("./abstract_list.js");
const Point = require("./point.js");
class PointList extends AbstractList {
get firstPoint() {
@ -18,6 +18,6 @@ class PointList extends AbstractList {
}
PointList._itemClass = Point;
PointList._itemName = 'point';
PointList._itemName = "point";
module.exports = PointList;

View file

@ -1,13 +1,13 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const events = require('../events.js');
const misc = require('../util/misc.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const events = require("../events.js");
const misc = require("../util/misc.js");
class Pool extends events.EventTarget {
constructor() {
const PostList = require('./post_list.js');
const PostList = require("./post_list.js");
super();
this._orig = {};
@ -70,14 +70,13 @@ class Pool extends events.EventTarget {
}
static get(id) {
return api.get(uri.formatApiLink('pool', id))
.then(response => {
return Promise.resolve(Pool.fromResponse(response));
});
return api.get(uri.formatApiLink("pool", id)).then((response) => {
return Promise.resolve(Pool.fromResponse(response));
});
}
save() {
const detail = {version: this._version};
const detail = { version: this._version };
// send only changed fields to avoid user privilege violation
if (misc.arraysDiffer(this._names, this._orig._names, true)) {
@ -90,62 +89,71 @@ class Pool extends events.EventTarget {
detail.description = this._description;
}
if (misc.arraysDiffer(this._posts, this._orig._posts)) {
detail.posts = this._posts.map(post => post.id);
detail.posts = this._posts.map((post) => post.id);
}
let promise = this._id ?
api.put(uri.formatApiLink('pool', this._id), detail) :
api.post(uri.formatApiLink('pools'), detail);
return promise
.then(response => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
let promise = this._id
? api.put(uri.formatApiLink("pool", this._id), detail)
: api.post(uri.formatApiLink("pools"), detail);
return promise.then((response) => {
this._updateFromResponse(response);
this.dispatchEvent(
new CustomEvent("change", {
detail: {
pool: this,
},
}));
return Promise.resolve();
});
})
);
return Promise.resolve();
});
}
merge(targetId, addAlias) {
return api.get(uri.formatApiLink('pool', targetId))
.then(response => {
return api.post(uri.formatApiLink('pool-merge'), {
return api
.get(uri.formatApiLink("pool", targetId))
.then((response) => {
return api.post(uri.formatApiLink("pool-merge"), {
removeVersion: this._version,
remove: this._id,
mergeToVersion: response.version,
mergeTo: targetId,
});
}).then(response => {
})
.then((response) => {
if (!addAlias) {
return Promise.resolve(response);
}
return api.put(uri.formatApiLink('pool', targetId), {
return api.put(uri.formatApiLink("pool", targetId), {
version: response.version,
names: response.names.concat(this._names),
});
}).then(response => {
})
.then((response) => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
detail: {
pool: this,
},
}));
this.dispatchEvent(
new CustomEvent("change", {
detail: {
pool: this,
},
})
);
return Promise.resolve();
});
}
delete() {
return api.delete(
uri.formatApiLink('pool', this._id),
{version: this._version})
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
pool: this,
},
}));
return api
.delete(uri.formatApiLink("pool", this._id), {
version: this._version,
})
.then((response) => {
this.dispatchEvent(
new CustomEvent("delete", {
detail: {
pool: this,
},
})
);
return Promise.resolve();
});
}

View file

@ -1,14 +1,14 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const events = require('../events.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const events = require("../events.js");
class PoolCategory extends events.EventTarget {
constructor() {
super();
this._name = '';
this._color = '#000000';
this._name = "";
this._color = "#000000";
this._poolCount = 0;
this._isDefault = false;
this._origName = null;
@ -50,7 +50,7 @@ class PoolCategory extends events.EventTarget {
}
save() {
const detail = {version: this._version};
const detail = { version: this._version };
if (this.name !== this._origName) {
detail.name = this.name;
@ -63,34 +63,39 @@ class PoolCategory extends events.EventTarget {
return Promise.resolve();
}
let promise = this._origName ?
api.put(
uri.formatApiLink('pool-category', this._origName),
detail) :
api.post(uri.formatApiLink('pool-categories'), detail);
let promise = this._origName
? api.put(
uri.formatApiLink("pool-category", this._origName),
detail
)
: api.post(uri.formatApiLink("pool-categories"), detail);
return promise
.then(response => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
return promise.then((response) => {
this._updateFromResponse(response);
this.dispatchEvent(
new CustomEvent("change", {
detail: {
poolCategory: this,
},
}));
return Promise.resolve();
});
})
);
return Promise.resolve();
});
}
delete() {
return api.delete(
uri.formatApiLink('pool-category', this._origName),
{version: this._version})
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
poolCategory: this,
},
}));
return api
.delete(uri.formatApiLink("pool-category", this._origName), {
version: this._version,
})
.then((response) => {
this.dispatchEvent(
new CustomEvent("delete", {
detail: {
poolCategory: this,
},
})
);
return Promise.resolve();
});
}

View file

@ -1,9 +1,9 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const AbstractList = require('./abstract_list.js');
const PoolCategory = require('./pool_category.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const AbstractList = require("./abstract_list.js");
const PoolCategory = require("./pool_category.js");
class PoolCategoryList extends AbstractList {
constructor() {
@ -11,7 +11,7 @@ class PoolCategoryList extends AbstractList {
this._defaultCategory = null;
this._origDefaultCategory = null;
this._deletedCategories = [];
this.addEventListener('remove', e => this._evtCategoryDeleted(e));
this.addEventListener("remove", (e) => this._evtCategoryDeleted(e));
}
static fromResponse(response) {
@ -27,12 +27,16 @@ class PoolCategoryList extends AbstractList {
}
static get() {
return api.get(uri.formatApiLink('pool-categories'))
.then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: PoolCategoryList.fromResponse(response.results)}));
return api
.get(uri.formatApiLink("pool-categories"))
.then((response) => {
return Promise.resolve(
Object.assign({}, response, {
results: PoolCategoryList.fromResponse(
response.results
),
})
);
});
}
@ -57,16 +61,18 @@ class PoolCategoryList extends AbstractList {
promises.push(
api.put(
uri.formatApiLink(
'pool-category',
"pool-category",
this._defaultCategory.name,
'default')));
"default"
)
)
);
}
return Promise.all(promises)
.then(response => {
this._deletedCategories = [];
return Promise.resolve();
});
return Promise.all(promises).then((response) => {
this._deletedCategories = [];
return Promise.resolve();
});
}
_evtCategoryDeleted(e) {
@ -77,6 +83,6 @@ class PoolCategoryList extends AbstractList {
}
PoolCategoryList._itemClass = PoolCategory;
PoolCategoryList._itemName = 'poolCategory';
PoolCategoryList._itemName = "poolCategory";
module.exports = PoolCategoryList;

View file

@ -1,25 +1,27 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const AbstractList = require('./abstract_list.js');
const Pool = require('./pool.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const AbstractList = require("./abstract_list.js");
const Pool = require("./pool.js");
class PoolList extends AbstractList {
static search(text, offset, limit, fields) {
return api.get(
uri.formatApiLink(
'pools', {
return api
.get(
uri.formatApiLink("pools", {
query: text,
offset: offset,
limit: limit,
fields: fields.join(','),
}))
.then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: PoolList.fromResponse(response.results)}));
fields: fields.join(","),
})
)
.then((response) => {
return Promise.resolve(
Object.assign({}, response, {
results: PoolList.fromResponse(response.results),
})
);
});
}
@ -42,6 +44,6 @@ class PoolList extends AbstractList {
}
PoolList._itemClass = Pool;
PoolList._itemName = 'pool';
PoolList._itemName = "pool";
module.exports = PoolList;

View file

@ -1,15 +1,15 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const tags = require('../tags.js');
const events = require('../events.js');
const TagList = require('./tag_list.js');
const NoteList = require('./note_list.js');
const CommentList = require('./comment_list.js');
const PoolList = require('./pool_list.js');
const Pool = require('./pool.js');
const misc = require('../util/misc.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const tags = require("../tags.js");
const events = require("../events.js");
const TagList = require("./tag_list.js");
const NoteList = require("./note_list.js");
const CommentList = require("./comment_list.js");
const PoolList = require("./pool_list.js");
const Pool = require("./pool.js");
const misc = require("../util/misc.js");
class Post extends events.EventTarget {
constructor() {
@ -67,7 +67,7 @@ class Post extends events.EventTarget {
}
get sourceSplit() {
return this._source.split('\n');
return this._source.split("\n");
}
get canvasWidth() {
@ -83,11 +83,11 @@ class Post extends events.EventTarget {
}
get newContent() {
throw 'Invalid operation';
throw "Invalid operation";
}
get newThumbnail() {
throw 'Invalid operation';
throw "Invalid operation";
}
get flags() {
@ -99,7 +99,7 @@ class Post extends events.EventTarget {
}
get tagNames() {
return this._tags.map(tag => tag.names[0]);
return this._tags.map((tag) => tag.names[0]);
}
get notes() {
@ -174,32 +174,31 @@ class Post extends events.EventTarget {
static reverseSearch(content) {
let apiPromise = api.post(
uri.formatApiLink('posts', 'reverse-search'),
uri.formatApiLink("posts", "reverse-search"),
{},
{content: content});
let returnedPromise = apiPromise
.then(response => {
if (response.exactPost) {
response.exactPost = Post.fromResponse(response.exactPost);
}
for (let item of response.similarPosts) {
item.post = Post.fromResponse(item.post);
}
return Promise.resolve(response);
});
{ content: content }
);
let returnedPromise = apiPromise.then((response) => {
if (response.exactPost) {
response.exactPost = Post.fromResponse(response.exactPost);
}
for (let item of response.similarPosts) {
item.post = Post.fromResponse(item.post);
}
return Promise.resolve(response);
});
returnedPromise.abort = () => apiPromise.abort();
return returnedPromise;
}
static get(id) {
return api.get(uri.formatApiLink('post', id))
.then(response => {
return Promise.resolve(Post.fromResponse(response));
});
return api.get(uri.formatApiLink("post", id)).then((response) => {
return Promise.resolve(Post.fromResponse(response));
});
}
_savePoolPosts() {
const difference = (a, b) => a.filter(post => !b.hasPoolId(post.id));
const difference = (a, b) => a.filter((post) => !b.hasPoolId(post.id));
// find the pools where the post was added or removed
const added = difference(this.pools, this._orig._pools);
@ -209,7 +208,7 @@ class Post extends events.EventTarget {
// update each pool's list of posts
for (let pool of added) {
let op = Pool.get(pool.id).then(response => {
let op = Pool.get(pool.id).then((response) => {
if (!response.posts.hasPostId(this._id)) {
response.posts.addById(this._id);
return response.save();
@ -221,7 +220,7 @@ class Post extends events.EventTarget {
}
for (let pool of removed) {
let op = Pool.get(pool.id).then(response => {
let op = Pool.get(pool.id).then((response) => {
if (response.posts.hasPostId(this._id)) {
response.posts.removeById(this._id);
return response.save();
@ -237,7 +236,7 @@ class Post extends events.EventTarget {
save(anonymous) {
const files = {};
const detail = {version: this._version};
const detail = { version: this._version };
// send only changed fields to avoid user privilege violation
if (anonymous === true) {
@ -250,14 +249,14 @@ class Post extends events.EventTarget {
detail.flags = this._flags;
}
if (misc.arraysDiffer(this._tags, this._orig._tags)) {
detail.tags = this._tags.map(tag => tag.names[0]);
detail.tags = this._tags.map((tag) => tag.names[0]);
}
if (misc.arraysDiffer(this._relations, this._orig._relations)) {
detail.relations = this._relations;
}
if (misc.arraysDiffer(this._notes, this._orig._notes)) {
detail.notes = this._notes.map(note => ({
polygon: note.polygon.map(point => [point.x, point.y]),
detail.notes = this._notes.map((note) => ({
polygon: note.polygon.map((point) => [point.x, point.y]),
text: note.text,
}));
}
@ -271,154 +270,187 @@ class Post extends events.EventTarget {
detail.source = this._source;
}
let apiPromise = this._id ?
api.put(uri.formatApiLink('post', this.id), detail, files) :
api.post(uri.formatApiLink('posts'), detail, files);
let apiPromise = this._id
? api.put(uri.formatApiLink("post", this.id), detail, files)
: api.post(uri.formatApiLink("posts"), detail, files);
return apiPromise.then(response => {
if (misc.arraysDiffer(this._pools, this._orig._pools)) {
return this._savePoolPosts()
.then(() => Promise.resolve(response));
}
return Promise.resolve(response);
}).then(response => {
this._updateFromResponse(response);
this.dispatchEvent(
new CustomEvent('change', {detail: {post: this}}));
if (this._newContent) {
this.dispatchEvent(
new CustomEvent('changeContent', {detail: {post: this}}));
}
if (this._newThumbnail) {
this.dispatchEvent(
new CustomEvent('changeThumbnail', {detail: {post: this}}));
}
return apiPromise
.then((response) => {
if (misc.arraysDiffer(this._pools, this._orig._pools)) {
return this._savePoolPosts().then(() =>
Promise.resolve(response)
);
}
return Promise.resolve(response);
})
.then(
(response) => {
this._updateFromResponse(response);
this.dispatchEvent(
new CustomEvent("change", { detail: { post: this } })
);
if (this._newContent) {
this.dispatchEvent(
new CustomEvent("changeContent", {
detail: { post: this },
})
);
}
if (this._newThumbnail) {
this.dispatchEvent(
new CustomEvent("changeThumbnail", {
detail: { post: this },
})
);
}
return Promise.resolve();
}, error => {
if (error.response &&
error.response.name === 'PostAlreadyUploadedError') {
error.message =
`Post already uploaded (@${error.response.otherPostId})`;
}
return Promise.reject(error);
});
return Promise.resolve();
},
(error) => {
if (
error.response &&
error.response.name === "PostAlreadyUploadedError"
) {
error.message = `Post already uploaded (@${error.response.otherPostId})`;
}
return Promise.reject(error);
}
);
}
feature() {
return api.post(
uri.formatApiLink('featured-post'),
{id: this._id})
.then(response => {
return api
.post(uri.formatApiLink("featured-post"), { id: this._id })
.then((response) => {
return Promise.resolve();
});
}
delete() {
return api.delete(
uri.formatApiLink('post', this.id),
{version: this._version})
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
post: this,
},
}));
return api
.delete(uri.formatApiLink("post", this.id), {
version: this._version,
})
.then((response) => {
this.dispatchEvent(
new CustomEvent("delete", {
detail: {
post: this,
},
})
);
return Promise.resolve();
});
}
merge(targetId, useOldContent) {
return api.get(uri.formatApiLink('post', targetId))
.then(response => {
return api.post(uri.formatApiLink('post-merge'), {
return api
.get(uri.formatApiLink("post", targetId))
.then((response) => {
return api.post(uri.formatApiLink("post-merge"), {
removeVersion: this._version,
remove: this._id,
mergeToVersion: response.version,
mergeTo: targetId,
replaceContent: useOldContent,
});
}).then(response => {
})
.then((response) => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
detail: {
post: this,
},
}));
this.dispatchEvent(
new CustomEvent("change", {
detail: {
post: this,
},
})
);
return Promise.resolve();
});
}
setScore(score) {
return api.put(
uri.formatApiLink('post', this.id, 'score'),
{score: score})
.then(response => {
return api
.put(uri.formatApiLink("post", this.id, "score"), { score: score })
.then((response) => {
const prevFavorite = this._ownFavorite;
this._updateFromResponse(response);
if (this._ownFavorite !== prevFavorite) {
this.dispatchEvent(new CustomEvent('changeFavorite', {
this.dispatchEvent(
new CustomEvent("changeFavorite", {
detail: {
post: this,
},
})
);
}
this.dispatchEvent(
new CustomEvent("changeScore", {
detail: {
post: this,
},
}));
}
this.dispatchEvent(new CustomEvent('changeScore', {
detail: {
post: this,
},
}));
})
);
return Promise.resolve();
});
}
addToFavorites() {
return api.post(uri.formatApiLink('post', this.id, 'favorite'))
.then(response => {
return api
.post(uri.formatApiLink("post", this.id, "favorite"))
.then((response) => {
const prevScore = this._ownScore;
this._updateFromResponse(response);
if (this._ownScore !== prevScore) {
this.dispatchEvent(new CustomEvent('changeScore', {
this.dispatchEvent(
new CustomEvent("changeScore", {
detail: {
post: this,
},
})
);
}
this.dispatchEvent(
new CustomEvent("changeFavorite", {
detail: {
post: this,
},
}));
}
this.dispatchEvent(new CustomEvent('changeFavorite', {
detail: {
post: this,
},
}));
})
);
return Promise.resolve();
});
}
removeFromFavorites() {
return api.delete(uri.formatApiLink('post', this.id, 'favorite'))
.then(response => {
return api
.delete(uri.formatApiLink("post", this.id, "favorite"))
.then((response) => {
const prevScore = this._ownScore;
this._updateFromResponse(response);
if (this._ownScore !== prevScore) {
this.dispatchEvent(new CustomEvent('changeScore', {
this.dispatchEvent(
new CustomEvent("changeScore", {
detail: {
post: this,
},
})
);
}
this.dispatchEvent(
new CustomEvent("changeFavorite", {
detail: {
post: this,
},
}));
}
this.dispatchEvent(new CustomEvent('changeFavorite', {
detail: {
post: this,
},
}));
})
);
return Promise.resolve();
});
}
mutateContentUrl() {
this._contentUrl =
this._orig._contentUrl +
'?bypass-cache=' +
Math.round(Math.random() * 1000);
this._orig._contentUrl +
"?bypass-cache=" +
Math.round(Math.random() * 1000);
}
_updateFromResponse(response) {
@ -431,15 +463,18 @@ class Post extends events.EventTarget {
_user: response.user,
_safety: response.safety,
_contentUrl: response.contentUrl,
_fullContentUrl: new URL(response.contentUrl, document.getElementsByTagName('base')[0].href).href,
_fullContentUrl: new URL(
response.contentUrl,
document.getElementsByTagName("base")[0].href
).href,
_thumbnailUrl: response.thumbnailUrl,
_source: response.source,
_canvasWidth: response.canvasWidth,
_canvasHeight: response.canvasHeight,
_fileSize: response.fileSize,
_flags: [...response.flags || []],
_relations: [...response.relations || []],
_flags: [...(response.flags || [])],
_relations: [...(response.relations || [])],
_score: response.score,
_commentCount: response.commentCount,

View file

@ -1,35 +1,37 @@
'use strict';
"use strict";
const settings = require('../models/settings.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const AbstractList = require('./abstract_list.js');
const Post = require('./post.js');
const settings = require("../models/settings.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const AbstractList = require("./abstract_list.js");
const Post = require("./post.js");
class PostList extends AbstractList {
static getAround(id, searchQuery) {
return api.get(
uri.formatApiLink(
'post', id, 'around', {
query: PostList._decorateSearchQuery(searchQuery || ''),
fields: 'id',
}));
uri.formatApiLink("post", id, "around", {
query: PostList._decorateSearchQuery(searchQuery || ""),
fields: "id",
})
);
}
static search(text, offset, limit, fields) {
return api.get(
uri.formatApiLink(
'posts', {
query: PostList._decorateSearchQuery(text || ''),
return api
.get(
uri.formatApiLink("posts", {
query: PostList._decorateSearchQuery(text || ""),
offset: offset,
limit: limit,
fields: fields.join(','),
}))
.then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: PostList.fromResponse(response.results)}));
fields: fields.join(","),
})
)
.then((response) => {
return Promise.resolve(
Object.assign({}, response, {
results: PostList.fromResponse(response.results),
})
);
});
}
@ -43,7 +45,7 @@ class PostList extends AbstractList {
}
}
if (disabledSafety.length) {
text = `-rating:${disabledSafety.join(',')} ${text}`;
text = `-rating:${disabledSafety.join(",")} ${text}`;
}
}
return text.trim();
@ -63,7 +65,7 @@ class PostList extends AbstractList {
return;
}
let post = Post.fromResponse({id: id});
let post = Post.fromResponse({ id: id });
this.add(post);
}
@ -77,6 +79,6 @@ class PostList extends AbstractList {
}
PostList._itemClass = Post;
PostList._itemName = 'post';
PostList._itemName = "post";
module.exports = PostList;

View file

@ -1,6 +1,6 @@
'use strict';
"use strict";
const events = require('../events.js');
const events = require("../events.js");
const defaultSettings = {
listPosts: {
@ -12,7 +12,7 @@ const defaultSettings = {
endlessScroll: false,
keyboardShortcuts: true,
transparencyGrid: true,
fitMode: 'fit-both',
fitMode: "fit-both",
tagSuggestions: true,
autoplayVideos: false,
postsPerPage: 42,
@ -28,7 +28,7 @@ class Settings extends events.EventTarget {
_getFromLocalStorage() {
let ret = Object.assign({}, defaultSettings);
try {
Object.assign(ret, JSON.parse(localStorage.getItem('settings')));
Object.assign(ret, JSON.parse(localStorage.getItem("settings")));
} catch (e) {
// continue regardless of error
}
@ -37,14 +37,16 @@ class Settings extends events.EventTarget {
save(newSettings, silent) {
newSettings = Object.assign(this.cache, newSettings);
localStorage.setItem('settings', JSON.stringify(newSettings));
localStorage.setItem("settings", JSON.stringify(newSettings));
this.cache = this._getFromLocalStorage();
if (silent !== true) {
this.dispatchEvent(new CustomEvent('change', {
detail: {
settings: this.cache,
},
}));
this.dispatchEvent(
new CustomEvent("change", {
detail: {
settings: this.cache,
},
})
);
}
}

View file

@ -1,7 +1,7 @@
'use strict';
"use strict";
const api = require('../api.js');
const events = require('../events.js');
const api = require("../api.js");
const events = require("../events.js");
class Snapshot extends events.EventTarget {
constructor() {

View file

@ -1,24 +1,31 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const AbstractList = require('./abstract_list.js');
const Snapshot = require('./snapshot.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const AbstractList = require("./abstract_list.js");
const Snapshot = require("./snapshot.js");
class SnapshotList extends AbstractList {
static search(text, offset, limit) {
return api.get(uri.formatApiLink(
'snapshots', {query: text, offset: offset, limit: limit}))
.then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: SnapshotList.fromResponse(response.results)}));
return api
.get(
uri.formatApiLink("snapshots", {
query: text,
offset: offset,
limit: limit,
})
)
.then((response) => {
return Promise.resolve(
Object.assign({}, response, {
results: SnapshotList.fromResponse(response.results),
})
);
});
}
}
SnapshotList._itemClass = Snapshot;
SnapshotList._itemName = 'snapshot';
SnapshotList._itemName = "snapshot";
module.exports = SnapshotList;

View file

@ -1,13 +1,13 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const events = require('../events.js');
const misc = require('../util/misc.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const events = require("../events.js");
const misc = require("../util/misc.js");
class Tag extends events.EventTarget {
constructor() {
const TagList = require('./tag_list.js');
const TagList = require("./tag_list.js");
super();
this._orig = {};
@ -71,14 +71,13 @@ class Tag extends events.EventTarget {
}
static get(name) {
return api.get(uri.formatApiLink('tag', name))
.then(response => {
return Promise.resolve(Tag.fromResponse(response));
});
return api.get(uri.formatApiLink("tag", name)).then((response) => {
return Promise.resolve(Tag.fromResponse(response));
});
}
save() {
const detail = {version: this._version};
const detail = { version: this._version };
// send only changed fields to avoid user privilege violation
if (misc.arraysDiffer(this._names, this._orig._names, true)) {
@ -92,66 +91,77 @@ class Tag extends events.EventTarget {
}
if (misc.arraysDiffer(this._implications, this._orig._implications)) {
detail.implications = this._implications.map(
relation => relation.names[0]);
(relation) => relation.names[0]
);
}
if (misc.arraysDiffer(this._suggestions, this._orig._suggestions)) {
detail.suggestions = this._suggestions.map(
relation => relation.names[0]);
(relation) => relation.names[0]
);
}
let promise = this._origName ?
api.put(uri.formatApiLink('tag', this._origName), detail) :
api.post(uri.formatApiLink('tags'), detail);
return promise
.then(response => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
let promise = this._origName
? api.put(uri.formatApiLink("tag", this._origName), detail)
: api.post(uri.formatApiLink("tags"), detail);
return promise.then((response) => {
this._updateFromResponse(response);
this.dispatchEvent(
new CustomEvent("change", {
detail: {
tag: this,
},
}));
return Promise.resolve();
});
})
);
return Promise.resolve();
});
}
merge(targetName, addAlias) {
return api.get(uri.formatApiLink('tag', targetName))
.then(response => {
return api.post(uri.formatApiLink('tag-merge'), {
return api
.get(uri.formatApiLink("tag", targetName))
.then((response) => {
return api.post(uri.formatApiLink("tag-merge"), {
removeVersion: this._version,
remove: this._origName,
mergeToVersion: response.version,
mergeTo: targetName,
});
}).then(response => {
})
.then((response) => {
if (!addAlias) {
return Promise.resolve(response);
}
return api.put(uri.formatApiLink('tag', targetName), {
return api.put(uri.formatApiLink("tag", targetName), {
version: response.version,
names: response.names.concat(this._names),
});
}).then(response => {
})
.then((response) => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
detail: {
tag: this,
},
}));
this.dispatchEvent(
new CustomEvent("change", {
detail: {
tag: this,
},
})
);
return Promise.resolve();
});
}
delete() {
return api.delete(
uri.formatApiLink('tag', this._origName),
{version: this._version})
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
tag: this,
},
}));
return api
.delete(uri.formatApiLink("tag", this._origName), {
version: this._version,
})
.then((response) => {
this.dispatchEvent(
new CustomEvent("delete", {
detail: {
tag: this,
},
})
);
return Promise.resolve();
});
}

View file

@ -1,14 +1,14 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const events = require('../events.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const events = require("../events.js");
class TagCategory extends events.EventTarget {
constructor() {
super();
this._name = '';
this._color = '#000000';
this._name = "";
this._color = "#000000";
this._tagCount = 0;
this._isDefault = false;
this._origName = null;
@ -50,7 +50,7 @@ class TagCategory extends events.EventTarget {
}
save() {
const detail = {version: this._version};
const detail = { version: this._version };
if (this.name !== this._origName) {
detail.name = this.name;
@ -63,34 +63,39 @@ class TagCategory extends events.EventTarget {
return Promise.resolve();
}
let promise = this._origName ?
api.put(
uri.formatApiLink('tag-category', this._origName),
detail) :
api.post(uri.formatApiLink('tag-categories'), detail);
let promise = this._origName
? api.put(
uri.formatApiLink("tag-category", this._origName),
detail
)
: api.post(uri.formatApiLink("tag-categories"), detail);
return promise
.then(response => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
return promise.then((response) => {
this._updateFromResponse(response);
this.dispatchEvent(
new CustomEvent("change", {
detail: {
tagCategory: this,
},
}));
return Promise.resolve();
});
})
);
return Promise.resolve();
});
}
delete() {
return api.delete(
uri.formatApiLink('tag-category', this._origName),
{version: this._version})
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
tagCategory: this,
},
}));
return api
.delete(uri.formatApiLink("tag-category", this._origName), {
version: this._version,
})
.then((response) => {
this.dispatchEvent(
new CustomEvent("delete", {
detail: {
tagCategory: this,
},
})
);
return Promise.resolve();
});
}

View file

@ -1,9 +1,9 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const AbstractList = require('./abstract_list.js');
const TagCategory = require('./tag_category.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const AbstractList = require("./abstract_list.js");
const TagCategory = require("./tag_category.js");
class TagCategoryList extends AbstractList {
constructor() {
@ -11,7 +11,7 @@ class TagCategoryList extends AbstractList {
this._defaultCategory = null;
this._origDefaultCategory = null;
this._deletedCategories = [];
this.addEventListener('remove', e => this._evtCategoryDeleted(e));
this.addEventListener("remove", (e) => this._evtCategoryDeleted(e));
}
static fromResponse(response) {
@ -27,12 +27,16 @@ class TagCategoryList extends AbstractList {
}
static get() {
return api.get(uri.formatApiLink('tag-categories'))
.then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: TagCategoryList.fromResponse(response.results)}));
return api
.get(uri.formatApiLink("tag-categories"))
.then((response) => {
return Promise.resolve(
Object.assign({}, response, {
results: TagCategoryList.fromResponse(
response.results
),
})
);
});
}
@ -57,16 +61,18 @@ class TagCategoryList extends AbstractList {
promises.push(
api.put(
uri.formatApiLink(
'tag-category',
"tag-category",
this._defaultCategory.name,
'default')));
"default"
)
)
);
}
return Promise.all(promises)
.then(response => {
this._deletedCategories = [];
return Promise.resolve();
});
return Promise.all(promises).then((response) => {
this._deletedCategories = [];
return Promise.resolve();
});
}
_evtCategoryDeleted(e) {
@ -77,6 +83,6 @@ class TagCategoryList extends AbstractList {
}
TagCategoryList._itemClass = TagCategory;
TagCategoryList._itemName = 'tagCategory';
TagCategoryList._itemName = "tagCategory";
module.exports = TagCategoryList;

View file

@ -1,25 +1,27 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const AbstractList = require('./abstract_list.js');
const Tag = require('./tag.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const AbstractList = require("./abstract_list.js");
const Tag = require("./tag.js");
class TagList extends AbstractList {
static search(text, offset, limit, fields) {
return api.get(
uri.formatApiLink(
'tags', {
return api
.get(
uri.formatApiLink("tags", {
query: text,
offset: offset,
limit: limit,
fields: fields.join(','),
}))
.then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: TagList.fromResponse(response.results)}));
fields: fields.join(","),
})
)
.then((response) => {
return Promise.resolve(
Object.assign({}, response, {
results: TagList.fromResponse(response.results),
})
);
});
}
@ -45,10 +47,12 @@ class TagList extends AbstractList {
this.add(tag);
if (addImplications !== false) {
return Tag.get(tagName).then(actualTag => {
return Tag.get(tagName).then((actualTag) => {
return Promise.all(
actualTag.implications.map(
relation => this.addByName(relation.names[0], true)));
actualTag.implications.map((relation) =>
this.addByName(relation.names[0], true)
)
);
});
}
@ -67,6 +71,6 @@ class TagList extends AbstractList {
}
TagList._itemClass = Tag;
TagList._itemName = 'tag';
TagList._itemName = "tag";
module.exports = TagList;

View file

@ -1,7 +1,7 @@
'use strict';
"use strict";
const events = require('../events.js');
const api = require('../api.js');
const events = require("../events.js");
const api = require("../api.js");
class TopNavigationItem {
constructor(accessKey, title, url, available, imageUrl) {
@ -44,18 +44,20 @@ class TopNavigation extends events.EventTarget {
activate(key) {
this.activeItem = null;
this.dispatchEvent(new CustomEvent('activate', {
detail: {
key: key,
item: key ? this.get(key) : null,
},
}));
this.dispatchEvent(
new CustomEvent("activate", {
detail: {
key: key,
item: key ? this.get(key) : null,
},
})
);
}
setTitle(title) {
api.fetchConfig().then(() => {
document.oldTitle = null;
document.title = api.getName() + (title ? (' ' + title) : '');
document.title = api.getName() + (title ? " " + title : "");
});
}
@ -76,24 +78,22 @@ class TopNavigation extends events.EventTarget {
function _makeTopNavigation() {
const ret = new TopNavigation();
ret.add('home', new TopNavigationItem('H', 'Home', ''));
ret.add('posts', new TopNavigationItem('P', 'Posts', 'posts'));
ret.add('upload', new TopNavigationItem('U', 'Upload', 'upload'));
ret.add('comments', new TopNavigationItem('C', 'Comments', 'comments'));
ret.add('tags', new TopNavigationItem('T', 'Tags', 'tags'));
ret.add('pools', new TopNavigationItem('O', 'Pools', 'pools'));
ret.add('users', new TopNavigationItem('S', 'Users', 'users'));
ret.add('account', new TopNavigationItem('A', 'Account', 'user/{me}'));
ret.add('register', new TopNavigationItem('R', 'Register', 'register'));
ret.add('login', new TopNavigationItem('L', 'Log in', 'login'));
ret.add('logout', new TopNavigationItem('O', 'Logout', 'logout'));
ret.add('help', new TopNavigationItem('E', 'Help', 'help'));
ret.add("home", new TopNavigationItem("H", "Home", ""));
ret.add("posts", new TopNavigationItem("P", "Posts", "posts"));
ret.add("upload", new TopNavigationItem("U", "Upload", "upload"));
ret.add("comments", new TopNavigationItem("C", "Comments", "comments"));
ret.add("tags", new TopNavigationItem("T", "Tags", "tags"));
ret.add("pools", new TopNavigationItem("O", "Pools", "pools"));
ret.add("users", new TopNavigationItem("S", "Users", "users"));
ret.add("account", new TopNavigationItem("A", "Account", "user/{me}"));
ret.add("register", new TopNavigationItem("R", "Register", "register"));
ret.add("login", new TopNavigationItem("L", "Log in", "login"));
ret.add("logout", new TopNavigationItem("O", "Logout", "logout"));
ret.add("help", new TopNavigationItem("E", "Help", "help"));
ret.add(
'settings',
new TopNavigationItem(
null,
'<i class=\'fa fa-cog\'></i>',
'settings'));
"settings",
new TopNavigationItem(null, "<i class='fa fa-cog'></i>", "settings")
);
return ret;
}

View file

@ -1,8 +1,8 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const events = require('../events.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const events = require("../events.js");
class User extends events.EventTarget {
constructor() {
@ -64,11 +64,11 @@ class User extends events.EventTarget {
}
get avatarContent() {
throw 'Invalid operation';
throw "Invalid operation";
}
get password() {
throw 'Invalid operation';
throw "Invalid operation";
}
set name(value) {
@ -102,15 +102,14 @@ class User extends events.EventTarget {
}
static get(name) {
return api.get(uri.formatApiLink('user', name))
.then(response => {
return Promise.resolve(User.fromResponse(response));
});
return api.get(uri.formatApiLink("user", name)).then((response) => {
return Promise.resolve(User.fromResponse(response));
});
}
save() {
const files = [];
const detail = {version: this._version};
const detail = { version: this._version };
const transient = this._orig._name;
if (this._name !== this._orig._name) {
@ -133,33 +132,40 @@ class User extends events.EventTarget {
detail.password = this._password;
}
let promise = this._orig._name ?
api.put(
uri.formatApiLink('user', this._orig._name), detail, files) :
api.post(uri.formatApiLink('users'), detail, files);
let promise = this._orig._name
? api.put(
uri.formatApiLink("user", this._orig._name),
detail,
files
)
: api.post(uri.formatApiLink("users"), detail, files);
return promise
.then(response => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
return promise.then((response) => {
this._updateFromResponse(response);
this.dispatchEvent(
new CustomEvent("change", {
detail: {
user: this,
},
}));
return Promise.resolve();
});
})
);
return Promise.resolve();
});
}
delete() {
return api.delete(
uri.formatApiLink('user', this._orig._name),
{version: this._version})
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
user: this,
},
}));
return api
.delete(uri.formatApiLink("user", this._orig._name), {
version: this._version,
})
.then((response) => {
this.dispatchEvent(
new CustomEvent("delete", {
detail: {
user: this,
},
})
);
return Promise.resolve();
});
}

View file

@ -1,25 +1,31 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const AbstractList = require('./abstract_list.js');
const User = require('./user.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const AbstractList = require("./abstract_list.js");
const User = require("./user.js");
class UserList extends AbstractList {
static search(text, offset, limit) {
return api.get(
uri.formatApiLink(
'users', {query: text, offset: offset, limit: limit}))
.then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: UserList.fromResponse(response.results)}));
return api
.get(
uri.formatApiLink("users", {
query: text,
offset: offset,
limit: limit,
})
)
.then((response) => {
return Promise.resolve(
Object.assign({}, response, {
results: UserList.fromResponse(response.results),
})
);
});
}
}
UserList._itemClass = User;
UserList._itemName = 'user';
UserList._itemName = "user";
module.exports = UserList;

View file

@ -1,8 +1,8 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const events = require('../events.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const events = require("../events.js");
class UserToken extends events.EventTarget {
constructor() {
@ -48,12 +48,12 @@ class UserToken extends events.EventTarget {
}
static fromResponse(response) {
if (typeof response.results !== 'undefined') {
if (typeof response.results !== "undefined") {
let tokenList = [];
for (let responseToken of response.results) {
const token = new UserToken();
token._updateFromResponse(responseToken);
tokenList.push(token)
tokenList.push(token);
}
return tokenList;
} else {
@ -64,15 +64,16 @@ class UserToken extends events.EventTarget {
}
static get(userName) {
return api.get(uri.formatApiLink('user-tokens', userName))
.then(response => {
return api
.get(uri.formatApiLink("user-tokens", userName))
.then((response) => {
return Promise.resolve(UserToken.fromResponse(response));
});
}
static create(userName, note, expirationTime) {
let userTokenRequest = {
enabled: true
enabled: true,
};
if (note) {
userTokenRequest.note = note;
@ -80,43 +81,54 @@ class UserToken extends events.EventTarget {
if (expirationTime) {
userTokenRequest.expirationTime = expirationTime;
}
return api.post(uri.formatApiLink('user-token', userName), userTokenRequest)
.then(response => {
return Promise.resolve(UserToken.fromResponse(response))
return api
.post(uri.formatApiLink("user-token", userName), userTokenRequest)
.then((response) => {
return Promise.resolve(UserToken.fromResponse(response));
});
}
save(userName) {
const detail = {version: this._version};
const detail = { version: this._version };
if (this._note !== this._orig._note) {
detail.note = this._note;
}
return api.put(
uri.formatApiLink('user-token', userName, this._orig._token),
detail)
.then(response => {
return api
.put(
uri.formatApiLink("user-token", userName, this._orig._token),
detail
)
.then((response) => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('change', {
detail: {
userToken: this,
},
}));
this.dispatchEvent(
new CustomEvent("change", {
detail: {
userToken: this,
},
})
);
return Promise.resolve(this);
});
}
delete(userName) {
return api.delete(
uri.formatApiLink('user-token', userName, this._orig._token),
{version: this._version})
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
userToken: this,
},
}));
return api
.delete(
uri.formatApiLink("user-token", userName, this._orig._token),
{
version: this._version,
}
)
.then((response) => {
this.dispatchEvent(
new CustomEvent("delete", {
detail: {
userToken: this,
},
})
);
return Promise.resolve();
});
}

View file

@ -1,22 +1,23 @@
'use strict';
"use strict";
const misc = require('./util/misc.js');
const PoolCategoryList = require('./models/pool_category_list.js');
const misc = require("./util/misc.js");
const PoolCategoryList = require("./models/pool_category_list.js");
let _stylesheet = null;
function refreshCategoryColorMap() {
return PoolCategoryList.get().then(response => {
return PoolCategoryList.get().then((response) => {
if (_stylesheet) {
document.head.removeChild(_stylesheet);
}
_stylesheet = document.createElement('style');
_stylesheet = document.createElement("style");
document.head.appendChild(_stylesheet);
for (let category of response.results) {
const ruleName = misc.makeCssName(category.name, 'pool');
const ruleName = misc.makeCssName(category.name, "pool");
_stylesheet.sheet.insertRule(
`.${ruleName} { color: ${category.color} }`,
_stylesheet.sheet.cssRules.length);
_stylesheet.sheet.cssRules.length
);
}
});
}

View file

@ -1,4 +1,4 @@
'use strict';
"use strict";
// modified page.js by visionmedia
// - changed regexes to components
@ -10,13 +10,17 @@
// - rename .save() to .replaceState()
// - offer .url
const clickEvent = document.ontouchstart ? 'touchstart' : 'click';
const uri = require('./util/uri.js');
const clickEvent = document.ontouchstart ? "touchstart" : "click";
const uri = require("./util/uri.js");
let location = window.history.location || window.location;
function _getOrigin() {
return location.protocol + '//' + location.hostname
+ (location.port ? (':' + location.port) : '');
return (
location.protocol +
"//" +
location.hostname +
(location.port ? ":" + location.port : "")
);
}
function _isSameOrigin(href) {
@ -24,15 +28,16 @@ function _isSameOrigin(href) {
}
function _getBaseHref() {
const bases = document.getElementsByTagName('base');
return bases.length > 0 ?
bases[0].href.replace(_getOrigin(), '').replace(/\/+$/, '') : '';
const bases = document.getElementsByTagName("base");
return bases.length > 0
? bases[0].href.replace(_getOrigin(), "").replace(/\/+$/, "")
: "";
}
class Context {
constructor(path, state) {
const base = _getBaseHref();
path = path.indexOf('/') !== 0 ? '/' + path : path;
path = path.indexOf("/") !== 0 ? "/" + path : path;
path = path.indexOf(base) !== 0 ? base + path : path;
this.canonicalPath = path;
@ -55,7 +60,7 @@ class Context {
class Route {
constructor(path) {
this.method = 'GET';
this.method = "GET";
this.path = path;
this.parameterNames = [];
@ -64,16 +69,17 @@ class Route {
} else {
let parts = [];
for (let component of this.path) {
if (component[0] === ':') {
parts.push('([^/]+)');
if (component[0] === ":") {
parts.push("([^/]+)");
this.parameterNames.push(component.substr(1));
} else { // assert [a-z]+
} else {
// assert [a-z]+
parts.push(component);
}
}
let regexString = '^/' + parts.join('/');
regexString += '(?:/*|/((?:(?:[a-z]+=[^/]+);)*(?:[a-z]+=[^/]+)))$';
this.parameterNames.push('variable');
let regexString = "^/" + parts.join("/");
regexString += "(?:/*|/((?:(?:[a-z]+=[^/]+);)*(?:[a-z]+=[^/]+)))$";
this.parameterNames.push("variable");
this.regex = new RegExp(regexString);
}
}
@ -88,7 +94,7 @@ class Route {
}
match(path, parameters) {
const qsIndex = path.indexOf('?');
const qsIndex = path.indexOf("?");
const pathname = ~qsIndex ? path.slice(0, qsIndex) : path;
const match = this.regex.exec(pathname);
@ -104,8 +110,8 @@ class Route {
continue;
}
if (name === 'variable') {
for (let word of (value || '').split(/;/)) {
if (name === "variable") {
for (let word of (value || "").split(/;/)) {
const [key, subvalue] = word.split(/=/, 2);
parameters[key] = uri.unescapeParam(subvalue);
}
@ -148,7 +154,7 @@ class Router {
this._running = true;
this._onPopState = _onPopState(this);
this._onClick = _onClick(this);
window.addEventListener('popstate', this._onPopState, false);
window.addEventListener("popstate", this._onPopState, false);
document.addEventListener(clickEvent, this._onClick, false);
const url = location.pathname + location.search + location.hash;
return this.replace(url, history.state, true);
@ -160,7 +166,7 @@ class Router {
}
this._running = false;
document.removeEventListener(clickEvent, this._onClick, false);
window.removeEventListener('popstate', this._onPopState, false);
window.removeEventListener("popstate", this._onPopState, false);
}
showNoDispatch(path, state) {
@ -199,11 +205,11 @@ class Router {
middle();
next();
};
const callChain = (this.ctx ? this._exits : [])
.concat(
[swap],
this._callbacks,
[this._unhandled, (ctx, next) => {}]);
const callChain = (this.ctx ? this._exits : []).concat(
[swap],
this._callbacks,
[this._unhandled, (ctx, next) => {}]
);
let i = 0;
let fn = () => {
@ -226,20 +232,18 @@ class Router {
}
}
const _onPopState = router => {
const _onPopState = (router) => {
let loaded = false;
if (document.readyState === 'complete') {
if (document.readyState === "complete") {
loaded = true;
} else {
window.addEventListener(
'load',
() => {
setTimeout(() => {
loaded = true;
}, 0);
});
window.addEventListener("load", () => {
setTimeout(() => {
loaded = true;
}, 0);
});
}
return e => {
return (e) => {
if (!loaded) {
return;
}
@ -247,16 +251,13 @@ const _onPopState = router => {
const path = e.state.path;
router.replace(path, e.state, true);
} else {
router.show(
location.pathname + location.hash,
undefined,
false);
router.show(location.pathname + location.hash, undefined, false);
}
};
};
const _onClick = router => {
return e => {
const _onClick = (router) => {
return (e) => {
if (1 !== _which(e)) {
return;
}
@ -268,23 +269,25 @@ const _onClick = router => {
}
let el = e.path ? e.path[0] : e.target;
while (el && el.nodeName !== 'A') {
while (el && el.nodeName !== "A") {
el = el.parentNode;
}
if (!el || el.nodeName !== 'A') {
if (!el || el.nodeName !== "A") {
return;
}
if (el.hasAttribute('download') ||
el.getAttribute('rel') === 'external') {
if (
el.hasAttribute("download") ||
el.getAttribute("rel") === "external"
) {
return;
}
const link = el.getAttribute('href');
if (el.pathname === location.pathname && (el.hash || '#' === link)) {
const link = el.getAttribute("href");
if (el.pathname === location.pathname && (el.hash || "#" === link)) {
return;
}
if (link && link.indexOf('mailto:') > -1) {
if (link && link.indexOf("mailto:") > -1) {
return;
}
if (el.target) {
@ -295,7 +298,7 @@ const _onClick = router => {
}
const base = _getBaseHref();
const orig = el.pathname + el.search + (el.hash || '');
const orig = el.pathname + el.search + (el.hash || "");
const path = !orig.indexOf(base) ? orig.slice(base.length) : orig;
if (base && orig === path) {

View file

@ -1,22 +1,23 @@
'use strict';
"use strict";
const misc = require('./util/misc.js');
const TagCategoryList = require('./models/tag_category_list.js');
const misc = require("./util/misc.js");
const TagCategoryList = require("./models/tag_category_list.js");
let _stylesheet = null;
function refreshCategoryColorMap() {
return TagCategoryList.get().then(response => {
return TagCategoryList.get().then((response) => {
if (_stylesheet) {
document.head.removeChild(_stylesheet);
}
_stylesheet = document.createElement('style');
_stylesheet = document.createElement("style");
document.head.appendChild(_stylesheet);
for (let category of response.results) {
const ruleName = misc.makeCssName(category.name, 'tag');
const ruleName = misc.makeCssName(category.name, "tag");
_stylesheet.sheet.insertRule(
`.${ruleName} { color: ${category.color} }`,
_stylesheet.sheet.cssRules.length);
_stylesheet.sheet.cssRules.length
);
}
});
}

View file

@ -1,3 +1,3 @@
'use strict';
"use strict";
module.exports = require('./.templates.autogen.js');
module.exports = require("./.templates.autogen.js");

View file

@ -1,12 +1,12 @@
'use strict';
"use strict";
const mousetrap = require('mousetrap');
const settings = require('../models/settings.js');
const mousetrap = require("mousetrap");
const settings = require("../models/settings.js");
let paused = false;
const _originalStopCallback = mousetrap.prototype.stopCallback;
// eslint-disable-next-line func-names
mousetrap.prototype.stopCallback = function(...args) {
mousetrap.prototype.stopCallback = function (...args) {
var self = this;
if (paused) {
return true;

View file

@ -1,6 +1,6 @@
'use strict';
"use strict";
const marked = require('marked');
const marked = require("marked");
class BaseMarkdownWrapper {
preprocess(text) {
@ -20,42 +20,44 @@ class SjisWrapper extends BaseMarkdownWrapper {
preprocess(text) {
return text.replace(
/\[sjis\]((?:[^\[]|\[(?!\/?sjis\]))+)\[\/sjis\]/ig,
/\[sjis\]((?:[^\[]|\[(?!\/?sjis\]))+)\[\/sjis\]/gi,
(match, capture) => {
var ret = '%%%SJIS' + this.buf.length;
var ret = "%%%SJIS" + this.buf.length;
this.buf.push(capture);
return ret;
});
}
);
}
postprocess(text) {
return text.replace(
/(?:<p>)?%%%SJIS(\d+)(?:<\/p>)?/,
(match, capture) => {
return '<div class="sjis">' + this.buf[capture] + '</div>';
});
return '<div class="sjis">' + this.buf[capture] + "</div>";
}
);
}
}
// fix \ before ~ being stripped away
class TildeWrapper extends BaseMarkdownWrapper {
preprocess(text) {
return text.replace(/\\~/g, '%%%T');
return text.replace(/\\~/g, "%%%T");
}
postprocess(text) {
return text.replace(/%%%T/g, '\\~');
return text.replace(/%%%T/g, "\\~");
}
}
// prevent ^#... from being treated as headers, due to tag permalinks
class TagPermalinkFixWrapper extends BaseMarkdownWrapper {
preprocess(text) {
return text.replace(/^#/g, '%%%#');
return text.replace(/^#/g, "%%%#");
}
postprocess(text) {
return text.replace(/%%%#/g, '#');
return text.replace(/%%%#/g, "#");
}
}
@ -63,19 +65,23 @@ class TagPermalinkFixWrapper extends BaseMarkdownWrapper {
class EntityPermalinkWrapper extends BaseMarkdownWrapper {
preprocess(text) {
// URL-based permalinks
text = text.replace(new RegExp("\\b/post/(\\d+)/?\\b", "g"), "@$1");
text = text.replace(
new RegExp('\\b/post/(\\d+)/?\\b', 'g'), '@$1');
new RegExp("\\b/tag/([a-zA-Z0-9_-]+?)/?", "g"),
"#$1"
);
text = text.replace(
new RegExp('\\b/tag/([a-zA-Z0-9_-]+?)/?', 'g'), '#$1');
text = text.replace(
new RegExp('\\b/user/([a-zA-Z0-9_-]+?)/?', 'g'), '+$1');
new RegExp("\\b/user/([a-zA-Z0-9_-]+?)/?", "g"),
"+$1"
);
text = text.replace(
/(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g,
'$1[$2]($2)');
text = text.replace(/\]\(@(\d+)\)/g, '](/post/$1)');
text = text.replace(/\]\(\+([a-zA-Z0-9_-]+)\)/g, '](/user/$1)');
text = text.replace(/\]\(#([a-zA-Z0-9_-]+)\)/g, '](/posts/query=$1)');
"$1[$2]($2)"
);
text = text.replace(/\]\(@(\d+)\)/g, "](/post/$1)");
text = text.replace(/\]\(\+([a-zA-Z0-9_-]+)\)/g, "](/user/$1)");
text = text.replace(/\]\(#([a-zA-Z0-9_-]+)\)/g, "](/posts/query=$1)");
return text;
}
}
@ -83,51 +89,58 @@ class EntityPermalinkWrapper extends BaseMarkdownWrapper {
class SearchPermalinkWrapper extends BaseMarkdownWrapper {
postprocess(text) {
return text.replace(
/\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/ig,
'<a href="/posts/query=$1"><code>$1</code></a>');
/\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/gi,
'<a href="/posts/query=$1"><code>$1</code></a>'
);
}
}
class SpoilersWrapper extends BaseMarkdownWrapper {
postprocess(text) {
return text.replace(
/\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\]))+)\[\/spoiler\]/ig,
'<span class="spoiler">$1</span>');
/\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\]))+)\[\/spoiler\]/gi,
'<span class="spoiler">$1</span>'
);
}
}
class SmallWrapper extends BaseMarkdownWrapper {
postprocess(text) {
return text.replace(
/\[small\]((?:[^\[]|\[(?!\/?small\]))+)\[\/small\]/ig,
'<small>$1</small>');
/\[small\]((?:[^\[]|\[(?!\/?small\]))+)\[\/small\]/gi,
"<small>$1</small>"
);
}
}
class StrikeThroughWrapper extends BaseMarkdownWrapper {
postprocess(text) {
text = text.replace(/(^|[^\\])(~~|~)([^~]+)\2/g, '$1<del>$3</del>');
return text.replace(/\\~/g, '~');
text = text.replace(/(^|[^\\])(~~|~)([^~]+)\2/g, "$1<del>$3</del>");
return text.replace(/\\~/g, "~");
}
}
function createRenderer() {
function sanitize(str) {
return str.replace(/&<"/g, m => {
if (m === '&') {
return '&amp;';
return str.replace(/&<"/g, (m) => {
if (m === "&") {
return "&amp;";
}
if (m === '<') {
return '&lt;';
if (m === "<") {
return "&lt;";
}
return '&quot;';
return "&quot;";
});
}
const renderer = new marked.Renderer();
renderer.image = (href, title, alt) => {
let [_, url, width, height] =
(/^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/).exec(href);
let [
_,
url,
width,
height,
] = /^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href);
let res = '<img src="' + sanitize(url) + '" alt="' + sanitize(alt);
if (width) {
res += '" width="' + width;

View file

@ -1,18 +1,18 @@
'use strict';
"use strict";
const markdown = require('./markdown.js');
const uri = require('./uri.js');
const settings = require('../models/settings.js');
const markdown = require("./markdown.js");
const uri = require("./uri.js");
const settings = require("../models/settings.js");
function decamelize(str, sep) {
sep = sep === undefined ? '-' : sep;
sep = sep === undefined ? "-" : sep;
return str
.replace(/([a-z\d])([A-Z])/g, '$1' + sep + '$2')
.replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1' + sep + '$2')
.replace(/([a-z\d])([A-Z])/g, "$1" + sep + "$2")
.replace(/([A-Z]+)([A-Z][a-z\d]+)/g, "$1" + sep + "$2")
.toLowerCase();
}
function *range(start = 0, end = null, step = 1) {
function* range(start = 0, end = null, step = 1) {
if (end === null) {
end = start;
start = 0;
@ -45,16 +45,17 @@ function formatFileSize(fileSize) {
return _formatUnits(
fileSize,
1024,
['B', 'K', 'M', 'G'],
["B", "K", "M", "G"],
(number, suffix) => {
const decimalPlaces = number < 20 && suffix !== 'B' ? 1 : 0;
const decimalPlaces = number < 20 && suffix !== "B" ? 1 : 0;
return number.toFixed(decimalPlaces) + suffix;
});
}
);
}
function formatRelativeTime(timeString) {
if (!timeString) {
return 'never';
return "never";
}
const then = Date.parse(timeString);
@ -63,17 +64,17 @@ function formatRelativeTime(timeString) {
const future = now < then;
const descriptions = [
[60, 'a few seconds', null],
[60 * 2, 'a minute', null],
[60 * 60, '% minutes', 60],
[60 * 60 * 2, 'an hour', null],
[60 * 60 * 24, '% hours', 60 * 60],
[60 * 60 * 24 * 2, 'a day', null],
[60 * 60 * 24 * 30.42, '% days', 60 * 60 * 24],
[60 * 60 * 24 * 30.42 * 2, 'a month', null],
[60 * 60 * 24 * 30.42 * 12, '% months', 60 * 60 * 24 * 30.42],
[60 * 60 * 24 * 30.42 * 12 * 2, 'a year', null],
[8640000000000000 /* max*/, '% years', 60 * 60 * 24 * 30.42 * 12],
[60, "a few seconds", null],
[60 * 2, "a minute", null],
[60 * 60, "% minutes", 60],
[60 * 60 * 2, "an hour", null],
[60 * 60 * 24, "% hours", 60 * 60],
[60 * 60 * 24 * 2, "a day", null],
[60 * 60 * 24 * 30.42, "% days", 60 * 60 * 24],
[60 * 60 * 24 * 30.42 * 2, "a month", null],
[60 * 60 * 24 * 30.42 * 12, "% months", 60 * 60 * 24 * 30.42],
[60 * 60 * 24 * 30.42 * 12 * 2, "a year", null],
[8640000000000000 /* max*/, "% years", 60 * 60 * 24 * 30.42 * 12],
];
let text = null;
@ -87,10 +88,10 @@ function formatRelativeTime(timeString) {
}
}
if (text === 'a day') {
return future ? 'tomorrow' : 'yesterday';
if (text === "a day") {
return future ? "tomorrow" : "yesterday";
}
return future ? 'in ' + text : text + ' ago';
return future ? "in " + text : text + " ago";
}
function formatMarkdown(text) {
@ -102,7 +103,7 @@ function formatInlineMarkdown(text) {
}
function splitByWhitespace(str) {
return str.split(/\s+/).filter(s => s);
return str.split(/\s+/).filter((s) => s);
}
function unindent(callSite, ...args) {
@ -110,28 +111,30 @@ function unindent(callSite, ...args) {
let size = -1;
return str.replace(/\n(\s+)/g, (m, m1) => {
if (size < 0) {
size = m1.replace(/\t/g, ' ').length;
size = m1.replace(/\t/g, " ").length;
}
return '\n' + m1.slice(Math.min(m1.length, size));
return "\n" + m1.slice(Math.min(m1.length, size));
});
}
if (typeof callSite === 'string') {
if (typeof callSite === "string") {
return format(callSite);
}
if (typeof callSite === 'function') {
if (typeof callSite === "function") {
return (...args) => format(callSite(...args));
}
let output = callSite
.slice(0, args.length + 1)
.map((text, i) => (i === 0 ? '' : args[i - 1]) + text)
.join('');
.map((text, i) => (i === 0 ? "" : args[i - 1]) + text)
.join("");
return format(output);
}
function enableExitConfirmation() {
window.onbeforeunload = e => {
return 'Are you sure you want to leave? ' +
'Data you have entered may not be saved.';
window.onbeforeunload = (e) => {
return (
"Are you sure you want to leave? " +
"Data you have entered may not be saved."
);
};
}
@ -150,16 +153,17 @@ function confirmPageExit() {
}
function makeCssName(text, suffix) {
return suffix + '-' + text.replace(/[^a-z0-9]/g, '_');
return suffix + "-" + text.replace(/[^a-z0-9]/g, "_");
}
function escapeHtml(unsafe) {
return unsafe.toString()
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
return unsafe
.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
function arraysDiffer(source1, source2, orderImportant) {
@ -177,25 +181,27 @@ function arraysDiffer(source1, source2, orderImportant) {
return false;
}
return (
source1.filter(value => !source2.includes(value)).length > 0 ||
source2.filter(value => !source1.includes(value)).length > 0);
source1.filter((value) => !source2.includes(value)).length > 0 ||
source2.filter((value) => !source1.includes(value)).length > 0
);
}
function escapeSearchTerm(text) {
return text.replace(/([a-z_-]):/g, '$1\\:');
return text.replace(/([a-z_-]):/g, "$1\\:");
}
function dataURItoBlob(dataURI) {
const chunks = dataURI.split(',');
const byteString = chunks[0].indexOf('base64') >= 0 ?
window.atob(chunks[1]) :
unescape(chunks[1]);
const mimeString = chunks[0].split(':')[1].split(';')[0];
const chunks = dataURI.split(",");
const byteString =
chunks[0].indexOf("base64") >= 0
? window.atob(chunks[1])
: unescape(chunks[1]);
const mimeString = chunks[0].split(":")[1].split(";")[0];
const data = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; i++) {
data[i] = byteString.charCodeAt(i);
}
return new Blob([data], {type: mimeString});
return new Blob([data], { type: mimeString });
}
function getPrettyTagName(tag) {

View file

@ -1,4 +1,4 @@
'use strict';
"use strict";
let callbacks = [];
let running = false;
@ -15,7 +15,7 @@ function resize() {
}
function runCallbacks() {
callbacks.forEach(callback => {
callbacks.forEach((callback) => {
callback();
});
running = false;
@ -26,8 +26,8 @@ function add(callback) {
}
function remove(callback) {
callbacks = callbacks.filter(c => c !== callback);
callbacks = callbacks.filter((c) => c !== callback);
}
window.addEventListener('resize', resize);
module.exports = {add: add, remove: remove};
window.addEventListener("resize", resize);
module.exports = { add: add, remove: remove };

View file

@ -1,11 +1,11 @@
/* eslint-disable func-names, no-extend-native */
'use strict';
"use strict";
// fix iterating over NodeList in Chrome and Opera
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
NodeList.prototype.querySelector = function(...args) {
NodeList.prototype.querySelector = function (...args) {
for (let node of this) {
if (node.nodeType === 3) {
continue;
@ -18,7 +18,7 @@ NodeList.prototype.querySelector = function(...args) {
return null;
};
NodeList.prototype.querySelectorAll = function(...args) {
NodeList.prototype.querySelectorAll = function (...args) {
let result = [];
for (let node of this) {
if (node.nodeType === 3) {
@ -32,7 +32,7 @@ NodeList.prototype.querySelectorAll = function(...args) {
};
// non standard
Node.prototype.prependChild = function(child) {
Node.prototype.prependChild = function (child) {
if (this.firstChild) {
this.insertBefore(child, this.firstChild);
} else {
@ -41,29 +41,25 @@ Node.prototype.prependChild = function(child) {
};
// non standard
Promise.prototype.always = function(onResolveOrReject) {
return this.then(
onResolveOrReject,
reason => {
onResolveOrReject(reason);
throw reason;
});
Promise.prototype.always = function (onResolveOrReject) {
return this.then(onResolveOrReject, (reason) => {
onResolveOrReject(reason);
throw reason;
});
};
// non standard
Number.prototype.between = function(a, b, inclusive) {
Number.prototype.between = function (a, b, inclusive) {
const min = Math.min(a, b);
const max = Math.max(a, b);
return inclusive ?
this >= min && this <= max :
this > min && this < max;
return inclusive ? this >= min && this <= max : this > min && this < max;
};
// non standard
Promise.prototype.abort = () => {};
// non standard
Date.prototype.addDays = function(days) {
Date.prototype.addDays = function (days) {
let dat = new Date(this.valueOf());
dat.setDate(dat.getDate() + days);
return dat;

View file

@ -1,6 +1,6 @@
'use strict';
"use strict";
const nprogress = require('nprogress');
const nprogress = require("nprogress");
let nesting = 0;

View file

@ -1,14 +1,16 @@
'use strict';
"use strict";
const misc = require('./misc.js');
const keyboard = require('../util/keyboard.js');
const views = require('./views.js');
const misc = require("./misc.js");
const keyboard = require("../util/keyboard.js");
const views = require("./views.js");
function searchInputNodeFocusHelper(inputNode) {
keyboard.bind('q', () => {
keyboard.bind("q", () => {
inputNode.focus();
inputNode.setSelectionRange(
inputNode.value.length, inputNode.value.length);
inputNode.value.length,
inputNode.value.length
);
});
}

View file

@ -1,11 +1,11 @@
'use strict';
"use strict";
const direction = {
NONE: null,
LEFT: 'left',
RIGHT: 'right',
DOWN: 'down',
UP: 'up'
LEFT: "left",
RIGHT: "right",
DOWN: "down",
UP: "up",
};
function handleTouchStart(handler, evt) {
@ -37,20 +37,20 @@ function handleTouchMove(handler, evt) {
function handleTouchEnd(handler) {
switch (handler._direction) {
case direction.NONE:
return;
case direction.LEFT:
handler._swipeLeftTask();
break;
case direction.RIGHT:
handler._swipeRightTask();
break;
case direction.DOWN:
handler._swipeDownTask();
break;
case direction.UP:
handler._swipeUpTask();
// no default
case direction.NONE:
return;
case direction.LEFT:
handler._swipeLeftTask();
break;
case direction.RIGHT:
handler._swipeRightTask();
break;
case direction.DOWN:
handler._swipeDownTask();
break;
case direction.UP:
handler._swipeUpTask();
// no default
}
handler._xStart = null;
@ -58,11 +58,13 @@ function handleTouchEnd(handler) {
}
class Touch {
constructor(target,
constructor(
target,
swipeLeft = () => {},
swipeRight = () => {},
swipeUp = () => {},
swipeDown = () => {}) {
swipeDown = () => {}
) {
this._target = target;
this._swipeLeftTask = swipeLeft;
@ -74,18 +76,15 @@ class Touch {
this._yStart = null;
this._direction = direction.NONE;
this._target.addEventListener('touchstart',
evt => {
handleTouchStart(this, evt);
});
this._target.addEventListener('touchmove',
evt => {
handleTouchMove(this, evt);
});
this._target.addEventListener('touchend',
() => {
handleTouchEnd(this);
});
this._target.addEventListener("touchstart", (evt) => {
handleTouchStart(this, evt);
});
this._target.addEventListener("touchmove", (evt) => {
handleTouchMove(this, evt);
});
this._target.addEventListener("touchend", () => {
handleTouchEnd(this);
});
}
}

View file

@ -1,4 +1,4 @@
'use strict';
"use strict";
function formatApiLink(...values) {
let parts = [];
@ -9,18 +9,19 @@ function formatApiLink(...values) {
for (let key of Object.keys(value)) {
if (value[key]) {
variableParts.push(
key + '=' + encodeURIComponent(value[key].toString()));
key + "=" + encodeURIComponent(value[key].toString())
);
}
}
if (variableParts.length) {
parts.push('?' + variableParts.join('&'));
parts.push("?" + variableParts.join("&"));
}
break;
} else {
parts.push(encodeURIComponent(value.toString()));
}
}
return '/' + parts.join('/');
return "/" + parts.join("/");
}
function escapeParam(text) {
@ -40,48 +41,52 @@ function formatClientLink(...values) {
for (let key of Object.keys(value)) {
if (value[key]) {
variableParts.push(
key + '=' + escapeParam(value[key].toString()));
key + "=" + escapeParam(value[key].toString())
);
}
}
if (variableParts.length) {
parts.push(variableParts.join(';'));
parts.push(variableParts.join(";"));
}
break;
} else {
parts.push(escapeParam(value.toString()));
}
}
return parts.join('/');
return parts.join("/");
}
function extractHostname(url) {
// https://stackoverflow.com/a/23945027
return url
.split('/')[url.indexOf("//") > -1 ? 2 : 0]
.split(':')[0]
.split('?')[0];
.split("/")
[url.indexOf("//") > -1 ? 2 : 0].split(":")[0]
.split("?")[0];
}
function extractRootDomain(url) {
// https://stackoverflow.com/a/23945027
let domain = extractHostname(url);
let splitArr = domain.split('.');
let splitArr = domain.split(".");
let arrLen = splitArr.length;
// if there is a subdomain
if (arrLen > 2) {
domain = splitArr[arrLen - 2] + '.' + splitArr[arrLen - 1];
domain = splitArr[arrLen - 2] + "." + splitArr[arrLen - 1];
// check to see if it's using a Country Code Top Level Domain (ccTLD) (i.e. ".me.uk")
if (splitArr[arrLen - 2].length === 2 && splitArr[arrLen - 1].length === 2) {
if (
splitArr[arrLen - 2].length === 2 &&
splitArr[arrLen - 1].length === 2
) {
// this is using a ccTLD
domain = splitArr[arrLen - 3] + '.' + domain;
domain = splitArr[arrLen - 3] + "." + domain;
}
}
return domain;
}
function escapeColons(text) {
return text.replace(new RegExp(':', 'g'), '\\:');
return text.replace(new RegExp(":", "g"), "\\:");
}
module.exports = {

View file

@ -1,27 +1,27 @@
'use strict';
"use strict";
require('../util/polyfill.js');
const api = require('../api.js');
const templates = require('../templates.js');
require("../util/polyfill.js");
const api = require("../api.js");
const templates = require("../templates.js");
const domParser = new DOMParser();
const misc = require('./misc.js');
const uri = require('./uri.js');
const misc = require("./misc.js");
const uri = require("./uri.js");
function _imbueId(options) {
if (!options.id) {
options.id = 'gen-' + Math.random().toString(36).substring(7);
options.id = "gen-" + Math.random().toString(36).substring(7);
}
}
function _makeLabel(options, attrs) {
if (!options.text) {
return '';
return "";
}
if (!attrs) {
attrs = {};
}
attrs.for = options.id;
return makeElement('label', attrs, options.text);
return makeElement("label", attrs, options.text);
}
function makeFileSize(fileSize) {
@ -34,251 +34,282 @@ function makeMarkdown(text) {
function makeRelativeTime(time) {
return makeElement(
'time', {datetime: time, title: time}, misc.formatRelativeTime(time));
"time",
{ datetime: time, title: time },
misc.formatRelativeTime(time)
);
}
function makeThumbnail(url) {
return makeElement(
'span',
url ?
{class: 'thumbnail', style: `background-image: url(\'${url}\')`} :
{class: 'thumbnail empty'},
makeElement('img', {alt: 'thumbnail', src: url}));
"span",
url
? {
class: "thumbnail",
style: `background-image: url(\'${url}\')`,
}
: { class: "thumbnail empty" },
makeElement("img", { alt: "thumbnail", src: url })
);
}
function makeRadio(options) {
_imbueId(options);
return makeElement(
'label',
{for: options.id},
makeElement(
'input',
{
id: options.id,
name: options.name,
value: options.value,
type: 'radio',
checked: options.selectedValue === options.value,
disabled: options.readonly,
required: options.required,
}),
makeElement('span', {class: 'radio'}, options.text));
"label",
{ for: options.id },
makeElement("input", {
id: options.id,
name: options.name,
value: options.value,
type: "radio",
checked: options.selectedValue === options.value,
disabled: options.readonly,
required: options.required,
}),
makeElement("span", { class: "radio" }, options.text)
);
}
function makeCheckbox(options) {
_imbueId(options);
return makeElement(
'label',
{for: options.id},
makeElement(
'input',
{
id: options.id,
name: options.name,
value: options.value,
type: 'checkbox',
checked: options.checked !== undefined ?
options.checked : false,
disabled: options.readonly,
required: options.required,
}),
makeElement('span', {class: 'checkbox'}, options.text));
"label",
{ for: options.id },
makeElement("input", {
id: options.id,
name: options.name,
value: options.value,
type: "checkbox",
checked: options.checked !== undefined ? options.checked : false,
disabled: options.readonly,
required: options.required,
}),
makeElement("span", { class: "checkbox" }, options.text)
);
}
function makeSelect(options) {
return _makeLabel(options) +
return (
_makeLabel(options) +
makeElement(
'select',
"select",
{
id: options.id,
name: options.name,
disabled: options.readonly,
},
...Object.keys(options.keyValues).map(key => makeElement(
'option',
{value: key, selected: key === options.selectedKey},
options.keyValues[key])));
...Object.keys(options.keyValues).map((key) =>
makeElement(
"option",
{ value: key, selected: key === options.selectedKey },
options.keyValues[key]
)
)
)
);
}
function makeInput(options) {
options.value = options.value || '';
return _makeLabel(options) + makeElement('input', options);
options.value = options.value || "";
return _makeLabel(options) + makeElement("input", options);
}
function makeButton(options) {
options.type = 'button';
options.type = "button";
return makeInput(options);
}
function makeTextInput(options) {
options.type = 'text';
options.type = "text";
return makeInput(options);
}
function makeTextarea(options) {
const value = options.value || '';
const value = options.value || "";
delete options.value;
return _makeLabel(options) + makeElement('textarea', options, value);
return _makeLabel(options) + makeElement("textarea", options, value);
}
function makePasswordInput(options) {
options.type = 'password';
options.type = "password";
return makeInput(options);
}
function makeEmailInput(options) {
options.type = 'email';
options.type = "email";
return makeInput(options);
}
function makeColorInput(options) {
const textInput = makeElement(
'input', {
type: 'text',
value: options.value || '',
required: options.required,
class: 'color',
});
const backgroundPreviewNode = makeElement(
'div',
{
class: 'preview background-preview',
style:
`border-color: ${options.value};
const textInput = makeElement("input", {
type: "text",
value: options.value || "",
required: options.required,
class: "color",
});
const backgroundPreviewNode = makeElement("div", {
class: "preview background-preview",
style: `border-color: ${options.value};
background-color: ${options.value}`,
});
const textPreviewNode = makeElement(
'div',
{
class: 'preview text-preview',
style:
`border-color: ${options.value};
});
const textPreviewNode = makeElement("div", {
class: "preview text-preview",
style: `border-color: ${options.value};
color: ${options.value}`,
});
});
return makeElement(
'label', {class: 'color'}, textInput, backgroundPreviewNode, textPreviewNode);
"label",
{ class: "color" },
textInput,
backgroundPreviewNode,
textPreviewNode
);
}
function makeNumericInput(options) {
options.type = 'number';
options.type = "number";
return makeInput(options);
}
function makeDateInput(options) {
options.type = 'date';
return makeInput(options)
options.type = "date";
return makeInput(options);
}
function getPostUrl(id, parameters) {
return uri.formatClientLink(
'post', id, parameters ? {query: parameters.query} : {});
"post",
id,
parameters ? { query: parameters.query } : {}
);
}
function getPostEditUrl(id, parameters) {
return uri.formatClientLink(
'post', id, 'edit', parameters ? {query: parameters.query} : {});
"post",
id,
"edit",
parameters ? { query: parameters.query } : {}
);
}
function makePostLink(id, includeHash) {
let text = id;
if (includeHash) {
text = '@' + id;
text = "@" + id;
}
return api.hasPrivilege('posts:view') ?
makeElement(
'a',
{href: uri.formatClientLink('post', id)},
misc.escapeHtml(text)) :
misc.escapeHtml(text);
return api.hasPrivilege("posts:view")
? makeElement(
"a",
{ href: uri.formatClientLink("post", id) },
misc.escapeHtml(text)
)
: misc.escapeHtml(text);
}
function makeTagLink(name, includeHash, includeCount, tag) {
const category = tag ? tag.category : 'unknown';
const category = tag ? tag.category : "unknown";
let text = misc.getPrettyTagName(name);
if (includeHash === true) {
text = '#' + text;
text = "#" + text;
}
if (includeCount === true) {
text += ' (' + (tag ? tag.postCount : 0) + ')';
text += " (" + (tag ? tag.postCount : 0) + ")";
}
return api.hasPrivilege('tags:view') ?
makeElement(
'a',
{
href: uri.formatClientLink('tag', name),
class: misc.makeCssName(category, 'tag'),
},
misc.escapeHtml(text)) :
makeElement(
'span',
{class: misc.makeCssName(category, 'tag')},
misc.escapeHtml(text));
return api.hasPrivilege("tags:view")
? makeElement(
"a",
{
href: uri.formatClientLink("tag", name),
class: misc.makeCssName(category, "tag"),
},
misc.escapeHtml(text)
)
: makeElement(
"span",
{ class: misc.makeCssName(category, "tag") },
misc.escapeHtml(text)
);
}
function makePoolLink(id, includeHash, includeCount, pool, name) {
const category = pool ? pool.category : 'unknown';
const category = pool ? pool.category : "unknown";
let text = name ? name : pool.names[0];
if (includeHash === true) {
text = '#' + text;
text = "#" + text;
}
if (includeCount === true) {
text += ' (' + (pool ? pool.postCount : 0) + ')';
text += " (" + (pool ? pool.postCount : 0) + ")";
}
return api.hasPrivilege('pools:view') ?
makeElement(
'a',
{
href: uri.formatClientLink('pool', id),
class: misc.makeCssName(category, 'pool'),
},
misc.escapeHtml(text)) :
makeElement(
'span',
{class: misc.makeCssName(category, 'pool')},
misc.escapeHtml(text));
return api.hasPrivilege("pools:view")
? makeElement(
"a",
{
href: uri.formatClientLink("pool", id),
class: misc.makeCssName(category, "pool"),
},
misc.escapeHtml(text)
)
: makeElement(
"span",
{ class: misc.makeCssName(category, "pool") },
misc.escapeHtml(text)
);
}
function makeUserLink(user) {
let text = makeThumbnail(user ? user.avatarUrl : null);
text += user && user.name ? misc.escapeHtml(user.name) : 'Anonymous';
const link = user && api.hasPrivilege('users:view') ?
makeElement(
'a', {href: uri.formatClientLink('user', user.name)}, text) :
text;
return makeElement('span', {class: 'user'}, link);
text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous";
const link =
user && api.hasPrivilege("users:view")
? makeElement(
"a",
{ href: uri.formatClientLink("user", user.name) },
text
)
: text;
return makeElement("span", { class: "user" }, link);
}
function makeFlexboxAlign(options) {
return [...misc.range(20)]
.map(() => '<li class="flexbox-dummy"></li>').join('');
.map(() => '<li class="flexbox-dummy"></li>')
.join("");
}
function makeAccessKey(html, key) {
const regex = new RegExp('(' + key + ')', 'i');
const regex = new RegExp("(" + key + ")", "i");
html = html.replace(
regex, '<span class="access-key" data-accesskey="$1">$1</span>');
regex,
'<span class="access-key" data-accesskey="$1">$1</span>'
);
return html;
}
function _serializeElement(name, attributes) {
return [name]
.concat(Object.keys(attributes).map(key => {
if (attributes[key] === true) {
return key;
} else if (attributes[key] === false ||
attributes[key] === undefined) {
return '';
}
const attribute = misc.escapeHtml(attributes[key] || '');
return `${key}="${attribute}"`;
}))
.join(' ');
.concat(
Object.keys(attributes).map((key) => {
if (attributes[key] === true) {
return key;
} else if (
attributes[key] === false ||
attributes[key] === undefined
) {
return "";
}
const attribute = misc.escapeHtml(attributes[key] || "");
return `${key}="${attribute}"`;
})
)
.join(" ");
}
function makeElement(name, attrs, ...content) {
return content.length !== undefined ?
`<${_serializeElement(name, attrs)}>${content.join('')}</${name}>` :
`<${_serializeElement(name, attrs)}/>`;
return content.length !== undefined
? `<${_serializeElement(name, attrs)}>${content.join("")}</${name}>`
: `<${_serializeElement(name, attrs)}/>`;
}
function emptyContent(target) {
@ -302,25 +333,25 @@ function replaceContent(target, source) {
function showMessage(target, message, className) {
if (!message) {
message = 'Unknown message';
message = "Unknown message";
}
const messagesHolderNode = target.querySelector('.messages');
const messagesHolderNode = target.querySelector(".messages");
if (!messagesHolderNode) {
return false;
}
const textNode = document.createElement('div');
textNode.innerHTML = message.replace(/\n/g, '<br/>');
textNode.classList.add('message');
const textNode = document.createElement("div");
textNode.innerHTML = message.replace(/\n/g, "<br/>");
textNode.classList.add("message");
textNode.classList.add(className);
const wrapperNode = document.createElement('div');
wrapperNode.classList.add('message-wrapper');
const wrapperNode = document.createElement("div");
wrapperNode.classList.add("message-wrapper");
wrapperNode.appendChild(textNode);
messagesHolderNode.appendChild(wrapperNode);
return true;
}
function appendExclamationMark() {
if (!document.title.startsWith('!')) {
if (!document.title.startsWith("!")) {
document.oldTitle = document.title;
document.title = `! ${document.title}`;
}
@ -328,15 +359,15 @@ function appendExclamationMark() {
function showError(target, message) {
appendExclamationMark();
return showMessage(target, misc.formatInlineMarkdown(message), 'error');
return showMessage(target, misc.formatInlineMarkdown(message), "error");
}
function showSuccess(target, message) {
return showMessage(target, misc.formatInlineMarkdown(message), 'success');
return showMessage(target, misc.formatInlineMarkdown(message), "success");
}
function showInfo(target, message) {
return showMessage(target, misc.formatInlineMarkdown(message), 'info');
return showMessage(target, misc.formatInlineMarkdown(message), "info");
}
function clearMessages(target) {
@ -344,7 +375,7 @@ function clearMessages(target) {
document.title = document.oldTitle;
document.oldTitle = null;
}
for (let messagesHolderNode of target.querySelectorAll('.messages')) {
for (let messagesHolderNode of target.querySelectorAll(".messages")) {
emptyContent(messagesHolderNode);
}
}
@ -352,15 +383,15 @@ function clearMessages(target) {
function htmlToDom(html) {
// code taken from jQuery + Krasimir Tsonev's blog
const wrapMap = {
_: [1, '<div>', '</div>'],
option: [1, '<select multiple>', '</select>'],
legend: [1, '<fieldset>', '</fieldset>'],
area: [1, '<map>', '</map>'],
param: [1, '<object>', '</object>'],
thead: [1, '<table>', '</table>'],
tr: [2, '<table><tbody>', '</tbody></table>'],
td: [3, '<table><tbody><tr>', '</tr></tbody></table>'],
col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
_: [1, "<div>", "</div>"],
option: [1, "<select multiple>", "</select>"],
legend: [1, "<fieldset>", "</fieldset>"],
area: [1, "<map>", "</map>"],
param: [1, "<object>", "</object>"],
thead: [1, "<table>", "</table>"],
tr: [2, "<table><tbody>", "</tbody></table>"],
td: [3, "<table><tbody><tr>", "</tr></tbody></table>"],
col: [2, "<table><tbody></tbody><colgroup>", "</colgroup></table>"],
};
wrapMap.optgroup = wrapMap.option;
wrapMap.tbody = wrapMap.thead;
@ -369,8 +400,8 @@ function htmlToDom(html) {
wrapMap.caption = wrapMap.thead;
wrapMap.th = wrapMap.td;
let element = document.createElement('div');
const match = (/<\s*(\w+)[^>]*?>/g).exec(html);
let element = document.createElement("div");
const match = /<\s*(\w+)[^>]*?>/g.exec(html);
if (match) {
const tag = match[1];
@ -382,9 +413,9 @@ function htmlToDom(html) {
} else {
element.innerHTML = html;
}
return element.childNodes.length > 1 ?
element.childNodes :
element.firstChild;
return element.childNodes.length > 1
? element.childNodes
: element.firstChild;
}
function getTemplate(templatePath) {
@ -392,7 +423,7 @@ function getTemplate(templatePath) {
throw `Missing template: ${templatePath}`;
}
const templateFactory = templates[templatePath];
return ctx => {
return (ctx) => {
if (!ctx) {
ctx = {};
}
@ -423,7 +454,7 @@ function getTemplate(templatePath) {
makeElement: makeElement,
makeCssName: misc.makeCssName,
makeNumericInput: makeNumericInput,
formatClientLink: uri.formatClientLink
formatClientLink: uri.formatClientLink,
});
return htmlToDom(templateFactory(ctx));
};
@ -432,49 +463,51 @@ function getTemplate(templatePath) {
function decorateValidator(form) {
// postpone showing form fields validity until user actually tries
// to submit it (seeing red/green form w/o doing anything breaks POLA)
let submitButton = form.querySelector('.buttons input');
let submitButton = form.querySelector(".buttons input");
if (!submitButton) {
submitButton = form.querySelector('input[type=submit]');
submitButton = form.querySelector("input[type=submit]");
}
if (submitButton) {
submitButton.addEventListener('click', e => {
form.classList.add('show-validation');
submitButton.addEventListener("click", (e) => {
form.classList.add("show-validation");
});
}
form.addEventListener('submit', e => {
form.classList.remove('show-validation');
form.addEventListener("submit", (e) => {
form.classList.remove("show-validation");
});
}
function disableForm(form) {
for (let input of form.querySelectorAll('input')) {
for (let input of form.querySelectorAll("input")) {
input.disabled = true;
}
}
function enableForm(form) {
for (let input of form.querySelectorAll('input')) {
for (let input of form.querySelectorAll("input")) {
input.disabled = false;
}
}
function syncScrollPosition() {
window.requestAnimationFrame(
() => {
if (history.state && Object.prototype.hasOwnProperty.call(history.state, 'scrollX')) {
window.scrollTo(history.state.scrollX, history.state.scrollY);
} else {
window.scrollTo(0, 0);
}
});
window.requestAnimationFrame(() => {
if (
history.state &&
Object.prototype.hasOwnProperty.call(history.state, "scrollX")
) {
window.scrollTo(history.state.scrollX, history.state.scrollY);
} else {
window.scrollTo(0, 0);
}
});
}
function slideDown(element) {
const duration = 500;
return new Promise((resolve, reject) => {
const height = element.getBoundingClientRect().height;
element.style.maxHeight = '0';
element.style.overflow = 'hidden';
element.style.maxHeight = "0";
element.style.overflow = "hidden";
window.setTimeout(() => {
element.style.transition = `all ${duration}ms ease`;
element.style.maxHeight = `${height}px`;
@ -489,7 +522,7 @@ function slideUp(element) {
const duration = 500;
return new Promise((resolve, reject) => {
const height = element.getBoundingClientRect().height;
element.style.overflow = 'hidden';
element.style.overflow = "hidden";
element.style.maxHeight = `${height}px`;
element.style.transition = `all ${duration}ms ease`;
window.setTimeout(() => {
@ -502,26 +535,27 @@ function slideUp(element) {
}
function monitorNodeRemoval(monitoredNode, callback) {
const mutationObserver = new MutationObserver(
mutations => {
for (let mutation of mutations) {
for (let node of mutation.removedNodes) {
if (node.contains(monitoredNode)) {
mutationObserver.disconnect();
callback();
return;
}
const mutationObserver = new MutationObserver((mutations) => {
for (let mutation of mutations) {
for (let node of mutation.removedNodes) {
if (node.contains(monitoredNode)) {
mutationObserver.disconnect();
callback();
return;
}
}
});
mutationObserver.observe(
document.body, {childList: true, subtree: true});
}
});
mutationObserver.observe(document.body, {
childList: true,
subtree: true,
});
}
document.addEventListener('input', e => {
if (e.target.classList.contains('color')) {
let bkNode = e.target.parentNode.querySelector('.background-preview');
let textNode = e.target.parentNode.querySelector('.text-preview');
document.addEventListener("input", (e) => {
if (e.target.classList.contains("color")) {
let bkNode = e.target.parentNode.querySelector(".background-preview");
let textNode = e.target.parentNode.querySelector(".text-preview");
bkNode.style.backgroundColor = e.target.value;
bkNode.style.borderColor = e.target.value;
textNode.style.color = e.target.value;
@ -530,8 +564,8 @@ document.addEventListener('input', e => {
});
// prevent opening buttons in new tabs
document.addEventListener('click', e => {
if (e.target.getAttribute('href') === '' && e.which === 2) {
document.addEventListener("click", (e) => {
if (e.target.getAttribute("href") === "" && e.which === 2) {
e.preventDefault();
}
});

View file

@ -1,10 +1,10 @@
'use strict';
"use strict";
const events = require('../events.js');
const views = require('../util/views.js');
const CommentListControl = require('../controls/comment_list_control.js');
const events = require("../events.js");
const views = require("../util/views.js");
const CommentListControl = require("../controls/comment_list_control.js");
const template = views.getTemplate('comments-page');
const template = views.getTemplate("comments-page");
class CommentsPageView extends events.EventTarget {
constructor(ctx) {
@ -16,12 +16,14 @@ class CommentsPageView extends events.EventTarget {
for (let post of ctx.response.results) {
const commentListControl = new CommentListControl(
sourceNode.querySelector(
`.comments-container[data-for="${post.id}"]`),
`.comments-container[data-for="${post.id}"]`
),
post.comments,
true);
events.proxyEvent(commentListControl, this, 'submit');
events.proxyEvent(commentListControl, this, 'score');
events.proxyEvent(commentListControl, this, 'delete');
true
);
events.proxyEvent(commentListControl, this, "submit");
events.proxyEvent(commentListControl, this, "score");
events.proxyEvent(commentListControl, this, "delete");
}
views.replaceContent(this._hostNode, sourceNode);

View file

@ -1,15 +1,16 @@
'use strict';
"use strict";
const views = require('../util/views.js');
const views = require("../util/views.js");
const template = () => {
return views.htmlToDom(
'<div class="wrapper"><div class="messages"></div></div>');
'<div class="wrapper"><div class="messages"></div></div>'
);
};
class EmptyView {
constructor() {
this._hostNode = document.getElementById('content-holder');
this._hostNode = document.getElementById("content-holder");
views.replaceContent(this._hostNode, template());
views.syncScrollPosition();
}

View file

@ -1,10 +1,10 @@
'use strict';
"use strict";
const router = require('../router.js');
const views = require('../util/views.js');
const router = require("../router.js");
const views = require("../util/views.js");
const holderTemplate = views.getTemplate('endless-pager');
const pageTemplate = views.getTemplate('endless-pager-page');
const holderTemplate = views.getTemplate("endless-pager");
const pageTemplate = views.getTemplate("endless-pager-page");
function isScrolledIntoView(element) {
let top = 0;
@ -12,14 +12,12 @@ function isScrolledIntoView(element) {
top += element.offsetTop || 0;
element = element.offsetParent;
} while (element);
return (
(top >= window.scrollY) &&
(top <= window.scrollY + window.innerHeight));
return top >= window.scrollY && top <= window.scrollY + window.innerHeight;
}
class EndlessPageView {
constructor(ctx) {
this._hostNode = document.getElementById('content-holder');
this._hostNode = document.getElementById("content-holder");
views.replaceContent(this._hostNode, holderTemplate());
}
@ -40,12 +38,13 @@ class EndlessPageView {
this.defaultLimit = parseInt(ctx.parameters.limit || ctx.defaultLimit);
const initialOffset = parseInt(ctx.parameters.offset || 0);
this._loadPage(ctx, initialOffset, this.defaultLimit, true)
.then(pageNode => {
this._loadPage(ctx, initialOffset, this.defaultLimit, true).then(
(pageNode) => {
if (initialOffset !== 0) {
pageNode.scrollIntoView();
}
});
}
);
this._timeout = window.setInterval(() => {
window.requestAnimationFrame(() => {
@ -58,19 +57,19 @@ class EndlessPageView {
}
get pageHeaderHolderNode() {
return this._hostNode.querySelector('.page-header-holder');
return this._hostNode.querySelector(".page-header-holder");
}
get topPageGuardNode() {
return this._hostNode.querySelector('.page-guard.top');
return this._hostNode.querySelector(".page-guard.top");
}
get bottomPageGuardNode() {
return this._hostNode.querySelector('.page-guard.bottom');
return this._hostNode.querySelector(".page-guard.bottom");
}
get _pagesHolderNode() {
return this._hostNode.querySelector('.pages-holder');
return this._hostNode.querySelector(".pages-holder");
}
_destroy() {
@ -82,9 +81,10 @@ class EndlessPageView {
let topPageNode = null;
let element = document.elementFromPoint(
window.innerWidth / 2,
window.innerHeight / 2);
window.innerHeight / 2
);
while (element.parentNode !== null) {
if (element.classList.contains('page')) {
if (element.classList.contains("page")) {
topPageNode = element;
break;
}
@ -93,15 +93,17 @@ class EndlessPageView {
if (!topPageNode) {
return;
}
let topOffset = parseInt(topPageNode.getAttribute('data-offset'));
let topLimit = parseInt(topPageNode.getAttribute('data-limit'));
let topOffset = parseInt(topPageNode.getAttribute("data-offset"));
let topLimit = parseInt(topPageNode.getAttribute("data-limit"));
if (topOffset !== this.currentOffset) {
router.replace(
ctx.getClientUrlForPage(
topOffset,
topLimit === ctx.defaultLimit ? null : topLimit),
topLimit === ctx.defaultLimit ? null : topLimit
),
ctx.state,
false);
false
);
this.currentOffset = topOffset;
}
}
@ -115,43 +117,47 @@ class EndlessPageView {
return;
}
if (this.minOffsetShown > 0 &&
isScrolledIntoView(this.topPageGuardNode)) {
if (
this.minOffsetShown > 0 &&
isScrolledIntoView(this.topPageGuardNode)
) {
this._loadPage(
ctx,
this.minOffsetShown - this.defaultLimit,
this.defaultLimit,
false);
false
);
}
if (this.maxOffsetShown < this.totalRecords &&
isScrolledIntoView(this.bottomPageGuardNode)) {
this._loadPage(
ctx,
this.maxOffsetShown,
this.defaultLimit,
true);
if (
this.maxOffsetShown < this.totalRecords &&
isScrolledIntoView(this.bottomPageGuardNode)
) {
this._loadPage(ctx, this.maxOffsetShown, this.defaultLimit, true);
}
}
_loadPage(ctx, offset, limit, append) {
this._runningRequests++;
return new Promise((resolve, reject) => {
ctx.requestPage(offset, limit).then(response => {
if (!this._active) {
ctx.requestPage(offset, limit).then(
(response) => {
if (!this._active) {
this._runningRequests--;
return Promise.reject();
}
window.requestAnimationFrame(() => {
let pageNode = this._renderPage(ctx, append, response);
this._runningRequests--;
resolve(pageNode);
});
},
(error) => {
this.showError(error.message);
this._runningRequests--;
return Promise.reject();
reject();
}
window.requestAnimationFrame(() => {
let pageNode = this._renderPage(ctx, append, response);
this._runningRequests--;
resolve(pageNode);
});
}, error => {
this.showError(error.message);
this._runningRequests--;
reject();
});
);
});
}
@ -162,30 +168,35 @@ class EndlessPageView {
pageNode = pageTemplate({
totalPages: Math.ceil(response.total / response.limit),
page: Math.ceil(
(response.offset + response.limit) / response.limit),
(response.offset + response.limit) / response.limit
),
});
pageNode.setAttribute('data-offset', response.offset);
pageNode.setAttribute('data-limit', response.limit);
pageNode.setAttribute("data-offset", response.offset);
pageNode.setAttribute("data-limit", response.limit);
ctx.pageRenderer({
parameters: ctx.parameters,
response: response,
hostNode: pageNode.querySelector('.page-content-holder'),
hostNode: pageNode.querySelector(".page-content-holder"),
});
this.totalRecords = response.total;
if (response.offset < this.minOffsetShown ||
this.minOffsetShown === null) {
if (
response.offset < this.minOffsetShown ||
this.minOffsetShown === null
) {
this.minOffsetShown = response.offset;
}
if (response.offset + response.results.length
> this.maxOffsetShown ||
this.maxOffsetShown === null) {
if (
response.offset + response.results.length >
this.maxOffsetShown ||
this.maxOffsetShown === null
) {
this.maxOffsetShown =
response.offset + response.results.length;
}
response.results.addEventListener('remove', e => {
response.results.addEventListener("remove", (e) => {
this.maxOffsetShown--;
this.totalRecords--;
});
@ -200,10 +211,11 @@ class EndlessPageView {
window.scroll(
window.scrollX,
window.scrollY + pageNode.offsetHeight);
window.scrollY + pageNode.offsetHeight
);
}
} else if (!response.results.length) {
this.showInfo('No data to show');
this.showInfo("No data to show");
}
this._initialPageLoad = false;

View file

@ -1,73 +1,81 @@
'use strict';
"use strict";
const api = require('../api.js');
const views = require('../util/views.js');
const api = require("../api.js");
const views = require("../util/views.js");
const template = views.getTemplate('help');
const template = views.getTemplate("help");
const sectionTemplates = {
'about': views.getTemplate('help-about'),
'keyboard': views.getTemplate('help-keyboard'),
'search': views.getTemplate('help-search'),
'comments': views.getTemplate('help-comments'),
'tos': views.getTemplate('help-tos'),
about: views.getTemplate("help-about"),
keyboard: views.getTemplate("help-keyboard"),
search: views.getTemplate("help-search"),
comments: views.getTemplate("help-comments"),
tos: views.getTemplate("help-tos"),
};
const subsectionTemplates = {
'search': {
'default': views.getTemplate('help-search-general'),
'posts': views.getTemplate('help-search-posts'),
'users': views.getTemplate('help-search-users'),
'tags': views.getTemplate('help-search-tags'),
'pools': views.getTemplate('help-search-pools'),
search: {
default: views.getTemplate("help-search-general"),
posts: views.getTemplate("help-search-posts"),
users: views.getTemplate("help-search-users"),
tags: views.getTemplate("help-search-tags"),
pools: views.getTemplate("help-search-pools"),
},
};
class HelpView {
constructor(section, subsection) {
this._hostNode = document.getElementById('content-holder');
this._hostNode = document.getElementById("content-holder");
const sourceNode = template();
const ctx = {
name: api.getName(),
};
section = section || 'about';
section = section || "about";
if (section in sectionTemplates) {
views.replaceContent(
sourceNode.querySelector('.content'),
sectionTemplates[section](ctx));
sourceNode.querySelector(".content"),
sectionTemplates[section](ctx)
);
}
subsection = subsection || 'default';
if (section in subsectionTemplates &&
subsection in subsectionTemplates[section]) {
subsection = subsection || "default";
if (
section in subsectionTemplates &&
subsection in subsectionTemplates[section]
) {
views.replaceContent(
sourceNode.querySelector('.subcontent'),
subsectionTemplates[section][subsection](ctx));
sourceNode.querySelector(".subcontent"),
subsectionTemplates[section][subsection](ctx)
);
}
views.replaceContent(this._hostNode, sourceNode);
for (let itemNode of
sourceNode.querySelectorAll('.primary [data-name]')) {
for (let itemNode of sourceNode.querySelectorAll(
".primary [data-name]"
)) {
itemNode.classList.toggle(
'active',
itemNode.getAttribute('data-name') === section);
if (itemNode.getAttribute('data-name') === section) {
"active",
itemNode.getAttribute("data-name") === section
);
if (itemNode.getAttribute("data-name") === section) {
itemNode.parentNode.scrollLeft =
itemNode.getBoundingClientRect().left -
itemNode.parentNode.getBoundingClientRect().left
itemNode.parentNode.getBoundingClientRect().left;
}
}
for (let itemNode of
sourceNode.querySelectorAll('.secondary [data-name]')) {
for (let itemNode of sourceNode.querySelectorAll(
".secondary [data-name]"
)) {
itemNode.classList.toggle(
'active',
itemNode.getAttribute('data-name') === subsection);
if (itemNode.getAttribute('data-name') === subsection) {
"active",
itemNode.getAttribute("data-name") === subsection
);
if (itemNode.getAttribute("data-name") === subsection) {
itemNode.parentNode.scrollLeft =
itemNode.getBoundingClientRect().left -
itemNode.parentNode.getBoundingClientRect().left
itemNode.parentNode.getBoundingClientRect().left;
}
}

View file

@ -1,22 +1,20 @@
'use strict';
"use strict";
const router = require('../router.js');
const uri = require('../util/uri.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
const PostContentControl = require('../controls/post_content_control.js');
const PostNotesOverlayControl
= require('../controls/post_notes_overlay_control.js');
const TagAutoCompleteControl =
require('../controls/tag_auto_complete_control.js');
const router = require("../router.js");
const uri = require("../util/uri.js");
const misc = require("../util/misc.js");
const views = require("../util/views.js");
const PostContentControl = require("../controls/post_content_control.js");
const PostNotesOverlayControl = require("../controls/post_notes_overlay_control.js");
const TagAutoCompleteControl = require("../controls/tag_auto_complete_control.js");
const template = views.getTemplate('home');
const footerTemplate = views.getTemplate('home-footer');
const featuredPostTemplate = views.getTemplate('home-featured-post');
const template = views.getTemplate("home");
const footerTemplate = views.getTemplate("home-footer");
const featuredPostTemplate = views.getTemplate("home-featured-post");
class HomeView {
constructor(ctx) {
this._hostNode = document.getElementById('content-holder');
this._hostNode = document.getElementById("content-holder");
this._ctx = ctx;
const sourceNode = template(ctx);
@ -27,11 +25,16 @@ class HomeView {
this._autoCompleteControl = new TagAutoCompleteControl(
this._searchInputNode,
{
confirm: tag => this._autoCompleteControl.replaceSelectedText(
misc.escapeSearchTerm(tag.names[0]), true),
});
this._formNode.addEventListener(
'submit', e => this._evtFormSubmit(e));
confirm: (tag) =>
this._autoCompleteControl.replaceSelectedText(
misc.escapeSearchTerm(tag.names[0]),
true
),
}
);
this._formNode.addEventListener("submit", (e) =>
this._evtFormSubmit(e)
);
}
}
@ -46,59 +49,67 @@ class HomeView {
setStats(stats) {
views.replaceContent(
this._footerContainerNode,
footerTemplate(Object.assign({}, stats, this._ctx)));
footerTemplate(Object.assign({}, stats, this._ctx))
);
}
setFeaturedPost(postInfo) {
views.replaceContent(
this._postInfoContainerNode, featuredPostTemplate(postInfo));
this._postInfoContainerNode,
featuredPostTemplate(postInfo)
);
if (this._postContainerNode && postInfo.featuredPost) {
this._postContentControl = new PostContentControl(
this._postContainerNode,
postInfo.featuredPost,
() => {
return [
window.innerWidth * 0.8,
window.innerHeight * 0.7,
];
return [window.innerWidth * 0.8, window.innerHeight * 0.7];
},
'fit-both');
"fit-both"
);
this._postNotesOverlay = new PostNotesOverlayControl(
this._postContainerNode.querySelector('.post-overlay'),
postInfo.featuredPost);
this._postContainerNode.querySelector(".post-overlay"),
postInfo.featuredPost
);
if (postInfo.featuredPost.type === 'video'
|| postInfo.featuredPost.type === 'flash') {
if (
postInfo.featuredPost.type === "video" ||
postInfo.featuredPost.type === "flash"
) {
this._postContentControl.disableOverlay();
}
}
}
get _footerContainerNode() {
return this._hostNode.querySelector('.footer-container');
return this._hostNode.querySelector(".footer-container");
}
get _postInfoContainerNode() {
return this._hostNode.querySelector('.post-info-container');
return this._hostNode.querySelector(".post-info-container");
}
get _postContainerNode() {
return this._hostNode.querySelector('.post-container');
return this._hostNode.querySelector(".post-container");
}
get _formNode() {
return this._hostNode.querySelector('form');
return this._hostNode.querySelector("form");
}
get _searchInputNode() {
return this._formNode.querySelector('input[name=search-text]');
return this._formNode.querySelector("input[name=search-text]");
}
_evtFormSubmit(e) {
e.preventDefault();
this._searchInputNode.blur();
router.show(uri.formatClientLink('posts', {query: this._searchInputNode.value}));
router.show(
uri.formatClientLink("posts", {
query: this._searchInputNode.value,
})
);
}
}

View file

@ -1,52 +1,63 @@
'use strict';
"use strict";
const events = require('../events.js');
const api = require('../api.js');
const views = require('../util/views.js');
const events = require("../events.js");
const api = require("../api.js");
const views = require("../util/views.js");
const template = views.getTemplate('login');
const template = views.getTemplate("login");
class LoginView extends events.EventTarget {
constructor() {
super();
this._hostNode = document.getElementById('content-holder');
this._hostNode = document.getElementById("content-holder");
views.replaceContent(this._hostNode, template({
userNamePattern: api.getUserNameRegex(),
passwordPattern: api.getPasswordRegex(),
canSendMails: api.canSendMails(),
}));
views.replaceContent(
this._hostNode,
template({
userNamePattern: api.getUserNameRegex(),
passwordPattern: api.getPasswordRegex(),
canSendMails: api.canSendMails(),
})
);
views.syncScrollPosition();
views.decorateValidator(this._formNode);
this._userNameInputNode.setAttribute('pattern', api.getUserNameRegex());
this._passwordInputNode.setAttribute('pattern', api.getPasswordRegex());
this._formNode.addEventListener('submit', e => {
this._userNameInputNode.setAttribute(
"pattern",
api.getUserNameRegex()
);
this._passwordInputNode.setAttribute(
"pattern",
api.getPasswordRegex()
);
this._formNode.addEventListener("submit", (e) => {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
name: this._userNameInputNode.value,
password: this._passwordInputNode.value,
remember: this._rememberInputNode.checked,
},
}));
this.dispatchEvent(
new CustomEvent("submit", {
detail: {
name: this._userNameInputNode.value,
password: this._passwordInputNode.value,
remember: this._rememberInputNode.checked,
},
})
);
});
}
get _formNode() {
return this._hostNode.querySelector('form');
return this._hostNode.querySelector("form");
}
get _userNameInputNode() {
return this._formNode.querySelector('[name=name]');
return this._formNode.querySelector("[name=name]");
}
get _passwordInputNode() {
return this._formNode.querySelector('[name=password]');
return this._formNode.querySelector("[name=password]");
}
get _rememberInputNode() {
return this._formNode.querySelector('[name=remember-user]');
return this._formNode.querySelector("[name=remember-user]");
}
disableForm() {

View file

@ -1,11 +1,11 @@
'use strict';
"use strict";
const router = require('../router.js');
const keyboard = require('../util/keyboard.js');
const views = require('../util/views.js');
const router = require("../router.js");
const keyboard = require("../util/keyboard.js");
const views = require("../util/views.js");
const holderTemplate = views.getTemplate('manual-pager');
const navTemplate = views.getTemplate('manual-pager-nav');
const holderTemplate = views.getTemplate("manual-pager");
const navTemplate = views.getTemplate("manual-pager-nav");
function _removeConsecutiveDuplicates(a) {
return a.filter((item, pos, ary) => {
@ -22,9 +22,7 @@ function _getVisiblePageNumbers(currentPage, totalPages) {
for (let i = totalPages - threshold; i <= totalPages; i++) {
pagesVisible.push(i);
}
for (let i = currentPage - threshold;
i <= currentPage + threshold;
i++) {
for (let i = currentPage - threshold; i <= currentPage + threshold; i++) {
pagesVisible.push(i);
}
pagesVisible = pagesVisible.filter((item, pos, ary) => {
@ -38,18 +36,22 @@ function _getVisiblePageNumbers(currentPage, totalPages) {
}
function _getPages(
currentPage, pageNumbers, limit, defaultLimit, removedItems) {
currentPage,
pageNumbers,
limit,
defaultLimit,
removedItems
) {
const pages = new Map();
let prevPage = 0;
for (let page of pageNumbers) {
if (page !== prevPage + 1) {
pages.set(page - 1, {ellipsis: true});
pages.set(page - 1, { ellipsis: true });
}
pages.set(page, {
number: page,
offset:
((page - 1) * limit) -
(page > currentPage ? removedItems : 0),
(page - 1) * limit - (page > currentPage ? removedItems : 0),
limit: limit === defaultLimit ? null : limit,
active: currentPage === page,
});
@ -60,7 +62,7 @@ function _getPages(
class ManualPageView {
constructor(ctx) {
this._hostNode = document.getElementById('content-holder');
this._hostNode = document.getElementById("content-holder");
views.replaceContent(this._hostNode, holderTemplate());
}
@ -70,52 +72,65 @@ class ManualPageView {
this.clearMessages();
views.emptyContent(this._pageNavNode);
ctx.requestPage(offset, limit).then(response => {
ctx.pageRenderer({
parameters: ctx.parameters,
response: response,
hostNode: this._pageContentHolderNode,
});
ctx.requestPage(offset, limit).then(
(response) => {
ctx.pageRenderer({
parameters: ctx.parameters,
response: response,
hostNode: this._pageContentHolderNode,
});
keyboard.bind(['a', 'left'], () => {
this._navigateToPrevNextPage('prev');
});
keyboard.bind(['d', 'right'], () => {
this._navigateToPrevNextPage('next');
});
keyboard.bind(["a", "left"], () => {
this._navigateToPrevNextPage("prev");
});
keyboard.bind(["d", "right"], () => {
this._navigateToPrevNextPage("next");
});
let removedItems = 0;
if (response.total) {
this._refreshNav(
offset, limit, response.total, removedItems, ctx);
let removedItems = 0;
if (response.total) {
this._refreshNav(
offset,
limit,
response.total,
removedItems,
ctx
);
}
if (!response.results.length) {
this.showInfo("No data to show");
}
response.results.addEventListener("remove", (e) => {
removedItems++;
this._refreshNav(
offset,
limit,
response.total,
removedItems,
ctx
);
});
views.syncScrollPosition();
},
(response) => {
this.showError(response.message);
}
if (!response.results.length) {
this.showInfo('No data to show');
}
response.results.addEventListener('remove', e => {
removedItems++;
this._refreshNav(
offset, limit, response.total, removedItems, ctx);
});
views.syncScrollPosition();
}, response => {
this.showError(response.message);
});
);
}
get pageHeaderHolderNode() {
return this._hostNode.querySelector('.page-header-holder');
return this._hostNode.querySelector(".page-header-holder");
}
get _pageContentHolderNode() {
return this._hostNode.querySelector('.page-content-holder');
return this._hostNode.querySelector(".page-content-holder");
}
get _pageNavNode() {
return this._hostNode.querySelector('.page-nav');
return this._hostNode.querySelector(".page-nav");
}
clearMessages() {
@ -135,11 +150,11 @@ class ManualPageView {
}
_navigateToPrevNextPage(className) {
const linkNode = this._hostNode.querySelector('a.' + className);
if (linkNode.classList.contains('disabled')) {
const linkNode = this._hostNode.querySelector("a." + className);
if (linkNode.classList.contains("disabled")) {
return;
}
router.show(linkNode.getAttribute('href'));
router.show(linkNode.getAttribute("href"));
}
_refreshNav(offset, limit, total, removedItems, ctx) {
@ -147,7 +162,12 @@ class ManualPageView {
const totalPages = Math.ceil((total - removedItems) / limit);
const pageNumbers = _getVisiblePageNumbers(currentPage, totalPages);
const pages = _getPages(
currentPage, pageNumbers, limit, ctx.defaultLimit, removedItems);
currentPage,
pageNumbers,
limit,
ctx.defaultLimit,
removedItems
);
views.replaceContent(
this._pageNavNode,
@ -158,7 +178,8 @@ class ManualPageView {
currentPage: currentPage,
totalPages: totalPages,
pages: pages,
}));
})
);
}
}

View file

@ -1,14 +1,14 @@
'use strict';
"use strict";
const views = require('../util/views.js');
const views = require("../util/views.js");
const template = views.getTemplate('not-found');
const template = views.getTemplate("not-found");
class NotFoundView {
constructor(path) {
this._hostNode = document.getElementById('content-holder');
this._hostNode = document.getElementById("content-holder");
const sourceNode = template({path: path});
const sourceNode = template({ path: path });
views.replaceContent(this._hostNode, sourceNode);
views.syncScrollPosition();
}

View file

@ -1,30 +1,35 @@
'use strict';
"use strict";
const events = require('../events.js');
const api = require('../api.js');
const views = require('../util/views.js');
const events = require("../events.js");
const api = require("../api.js");
const views = require("../util/views.js");
const template = views.getTemplate('password-reset');
const template = views.getTemplate("password-reset");
class PasswordResetView extends events.EventTarget {
constructor() {
super();
this._hostNode = document.getElementById('content-holder');
this._hostNode = document.getElementById("content-holder");
views.replaceContent(this._hostNode, template({
canSendMails: api.canSendMails(),
contactEmail: api.getContactEmail(),
}));
views.replaceContent(
this._hostNode,
template({
canSendMails: api.canSendMails(),
contactEmail: api.getContactEmail(),
})
);
views.syncScrollPosition();
views.decorateValidator(this._formNode);
this._formNode.addEventListener('submit', e => {
this._formNode.addEventListener("submit", (e) => {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
userNameOrEmail: this._userNameOrEmailFieldNode.value,
},
}));
this.dispatchEvent(
new CustomEvent("submit", {
detail: {
userNameOrEmail: this._userNameOrEmailFieldNode.value,
},
})
);
});
}
@ -49,11 +54,11 @@ class PasswordResetView extends events.EventTarget {
}
get _formNode() {
return this._hostNode.querySelector('form');
return this._hostNode.querySelector("form");
}
get _userNameOrEmailFieldNode() {
return this._formNode.querySelector('[name=user-name]');
return this._formNode.querySelector("[name=user-name]");
}
}

View file

@ -1,17 +1,17 @@
'use strict';
"use strict";
const events = require('../events.js');
const views = require('../util/views.js');
const PoolCategory = require('../models/pool_category.js');
const events = require("../events.js");
const views = require("../util/views.js");
const PoolCategory = require("../models/pool_category.js");
const template = views.getTemplate('pool-categories');
const rowTemplate = views.getTemplate('pool-category-row');
const template = views.getTemplate("pool-categories");
const rowTemplate = views.getTemplate("pool-category-row");
class PoolCategoriesView extends events.EventTarget {
constructor(ctx) {
super();
this._ctx = ctx;
this._hostNode = document.getElementById('content-holder');
this._hostNode = document.getElementById("content-holder");
views.replaceContent(this._hostNode, template(ctx));
views.syncScrollPosition();
@ -31,18 +31,22 @@ class PoolCategoriesView extends events.EventTarget {
}
if (this._addLinkNode) {
this._addLinkNode.addEventListener(
'click', e => this._evtAddButtonClick(e));
this._addLinkNode.addEventListener("click", (e) =>
this._evtAddButtonClick(e)
);
}
ctx.poolCategories.addEventListener(
'add', e => this._evtPoolCategoryAdded(e));
ctx.poolCategories.addEventListener("add", (e) =>
this._evtPoolCategoryAdded(e)
);
ctx.poolCategories.addEventListener(
'remove', e => this._evtPoolCategoryDeleted(e));
ctx.poolCategories.addEventListener("remove", (e) =>
this._evtPoolCategoryDeleted(e)
);
this._formNode.addEventListener(
'submit', e => this._evtSaveButtonClick(e, ctx));
this._formNode.addEventListener("submit", (e) =>
this._evtSaveButtonClick(e, ctx)
);
}
enableForm() {
@ -66,44 +70,48 @@ class PoolCategoriesView extends events.EventTarget {
}
get _formNode() {
return this._hostNode.querySelector('form');
return this._hostNode.querySelector("form");
}
get _tableBodyNode() {
return this._hostNode.querySelector('tbody');
return this._hostNode.querySelector("tbody");
}
get _addLinkNode() {
return this._hostNode.querySelector('a.add');
return this._hostNode.querySelector("a.add");
}
_addPoolCategoryRowNode(poolCategory) {
const rowNode = rowTemplate(
Object.assign(
{}, this._ctx, {poolCategory: poolCategory}));
Object.assign({}, this._ctx, { poolCategory: poolCategory })
);
const nameInput = rowNode.querySelector('.name input');
const nameInput = rowNode.querySelector(".name input");
if (nameInput) {
nameInput.addEventListener(
'change', e => this._evtNameChange(e, rowNode));
nameInput.addEventListener("change", (e) =>
this._evtNameChange(e, rowNode)
);
}
const colorInput = rowNode.querySelector('.color input');
const colorInput = rowNode.querySelector(".color input");
if (colorInput) {
colorInput.addEventListener(
'change', e => this._evtColorChange(e, rowNode));
colorInput.addEventListener("change", (e) =>
this._evtColorChange(e, rowNode)
);
}
const removeLinkNode = rowNode.querySelector('.remove a');
const removeLinkNode = rowNode.querySelector(".remove a");
if (removeLinkNode) {
removeLinkNode.addEventListener(
'click', e => this._evtDeleteButtonClick(e, rowNode));
removeLinkNode.addEventListener("click", (e) =>
this._evtDeleteButtonClick(e, rowNode)
);
}
const defaultLinkNode = rowNode.querySelector('.set-default a');
const defaultLinkNode = rowNode.querySelector(".set-default a");
if (defaultLinkNode) {
defaultLinkNode.addEventListener(
'click', e => this._evtSetDefaultButtonClick(e, rowNode));
defaultLinkNode.addEventListener("click", (e) =>
this._evtSetDefaultButtonClick(e, rowNode)
);
}
this._tableBodyNode.appendChild(rowNode);
@ -141,7 +149,7 @@ class PoolCategoriesView extends events.EventTarget {
_evtDeleteButtonClick(e, rowNode, link) {
e.preventDefault();
if (e.target.classList.contains('inactive')) {
if (e.target.classList.contains("inactive")) {
return;
}
this._ctx.poolCategories.remove(rowNode._poolCategory);
@ -150,16 +158,16 @@ class PoolCategoriesView extends events.EventTarget {
_evtSetDefaultButtonClick(e, rowNode) {
e.preventDefault();
this._ctx.poolCategories.defaultCategory = rowNode._poolCategory;
const oldRowNode = rowNode.parentNode.querySelector('tr.default');
const oldRowNode = rowNode.parentNode.querySelector("tr.default");
if (oldRowNode) {
oldRowNode.classList.remove('default');
oldRowNode.classList.remove("default");
}
rowNode.classList.add('default');
rowNode.classList.add("default");
}
_evtSaveButtonClick(e, ctx) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit'));
this.dispatchEvent(new CustomEvent("submit"));
}
}

View file

@ -1,41 +1,43 @@
'use strict';
"use strict";
const events = require('../events.js');
const api = require('../api.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
const Pool = require('../models/pool.js')
const events = require("../events.js");
const api = require("../api.js");
const misc = require("../util/misc.js");
const views = require("../util/views.js");
const Pool = require("../models/pool.js");
const template = views.getTemplate('pool-create');
const template = views.getTemplate("pool-create");
class PoolCreateView extends events.EventTarget {
constructor(ctx) {
super();
this._hostNode = document.getElementById('content-holder');
this._hostNode = document.getElementById("content-holder");
views.replaceContent(this._hostNode, template(ctx));
views.decorateValidator(this._formNode);
if (this._namesFieldNode) {
this._namesFieldNode.addEventListener(
'input', e => this._evtNameInput(e));
this._namesFieldNode.addEventListener("input", (e) =>
this._evtNameInput(e)
);
}
if (this._postsFieldNode) {
this._postsFieldNode.addEventListener(
'input', e => this._evtPostsInput(e));
this._postsFieldNode.addEventListener("input", (e) =>
this._evtPostsInput(e)
);
}
for (let node of this._formNode.querySelectorAll(
'input, select, textarea, posts')) {
node.addEventListener(
'change', e => {
this.dispatchEvent(new CustomEvent('change'));
});
"input, select, textarea, posts"
)) {
node.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent("change"));
});
}
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
this._formNode.addEventListener("submit", (e) => this._evtSubmit(e));
}
clearMessages() {
@ -64,19 +66,21 @@ class PoolCreateView extends events.EventTarget {
if (!list.length) {
this._namesFieldNode.setCustomValidity(
'Pools must have at least one name.');
"Pools must have at least one name."
);
return;
}
for (let item of list) {
if (!regex.test(item)) {
this._namesFieldNode.setCustomValidity(
`Pool name "${item}" contains invalid symbols.`);
`Pool name "${item}" contains invalid symbols.`
);
return;
}
}
this._namesFieldNode.setCustomValidity('');
this._namesFieldNode.setCustomValidity("");
}
_evtPostsInput(e) {
@ -86,46 +90,50 @@ class PoolCreateView extends events.EventTarget {
for (let item of list) {
if (!regex.test(item)) {
this._postsFieldNode.setCustomValidity(
`Pool ID "${item}" is not an integer.`);
`Pool ID "${item}" is not an integer.`
);
return;
}
}
this._postsFieldNode.setCustomValidity('');
this._postsFieldNode.setCustomValidity("");
}
_evtSubmit(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
names: misc.splitByWhitespace(this._namesFieldNode.value),
category: this._categoryFieldNode.value,
description: this._descriptionFieldNode.value,
posts: misc.splitByWhitespace(this._postsFieldNode.value)
.map(i => parseInt(i))
},
}));
this.dispatchEvent(
new CustomEvent("submit", {
detail: {
names: misc.splitByWhitespace(this._namesFieldNode.value),
category: this._categoryFieldNode.value,
description: this._descriptionFieldNode.value,
posts: misc
.splitByWhitespace(this._postsFieldNode.value)
.map((i) => parseInt(i)),
},
})
);
}
get _formNode() {
return this._hostNode.querySelector('form');
return this._hostNode.querySelector("form");
}
get _namesFieldNode() {
return this._formNode.querySelector('.names input');
return this._formNode.querySelector(".names input");
}
get _categoryFieldNode() {
return this._formNode.querySelector('.category select');
return this._formNode.querySelector(".category select");
}
get _descriptionFieldNode() {
return this._formNode.querySelector('.description textarea');
return this._formNode.querySelector(".description textarea");
}
get _postsFieldNode() {
return this._formNode.querySelector('.posts input');
return this._formNode.querySelector(".posts input");
}
}

View file

@ -1,9 +1,9 @@
'use strict';
"use strict";
const events = require('../events.js');
const views = require('../util/views.js');
const events = require("../events.js");
const views = require("../util/views.js");
const template = views.getTemplate('pool-delete');
const template = views.getTemplate("pool-delete");
class PoolDeleteView extends events.EventTarget {
constructor(ctx) {
@ -13,7 +13,7 @@ class PoolDeleteView extends events.EventTarget {
this._pool = ctx.pool;
views.replaceContent(this._hostNode, template(ctx));
views.decorateValidator(this._formNode);
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
this._formNode.addEventListener("submit", (e) => this._evtSubmit(e));
}
clearMessages() {
@ -38,15 +38,17 @@ class PoolDeleteView extends events.EventTarget {
_evtSubmit(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
pool: this._pool,
},
}));
this.dispatchEvent(
new CustomEvent("submit", {
detail: {
pool: this._pool,
},
})
);
}
get _formNode() {
return this._hostNode.querySelector('form');
return this._hostNode.querySelector("form");
}
}

View file

@ -1,12 +1,12 @@
'use strict';
"use strict";
const events = require('../events.js');
const api = require('../api.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
const Post = require('../models/post.js');
const events = require("../events.js");
const api = require("../api.js");
const misc = require("../util/misc.js");
const views = require("../util/views.js");
const Post = require("../models/post.js");
const template = views.getTemplate('pool-edit');
const template = views.getTemplate("pool-edit");
class PoolEditView extends events.EventTarget {
constructor(ctx) {
@ -19,24 +19,26 @@ class PoolEditView extends events.EventTarget {
views.decorateValidator(this._formNode);
if (this._namesFieldNode) {
this._namesFieldNode.addEventListener(
'input', e => this._evtNameInput(e));
this._namesFieldNode.addEventListener("input", (e) =>
this._evtNameInput(e)
);
}
if (this._postsFieldNode) {
this._postsFieldNode.addEventListener(
'input', e => this._evtPostsInput(e));
this._postsFieldNode.addEventListener("input", (e) =>
this._evtPostsInput(e)
);
}
for (let node of this._formNode.querySelectorAll(
'input, select, textarea, posts')) {
node.addEventListener(
'change', e => {
this.dispatchEvent(new CustomEvent('change'));
});
"input, select, textarea, posts"
)) {
node.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent("change"));
});
}
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
this._formNode.addEventListener("submit", (e) => this._evtSubmit(e));
}
clearMessages() {
@ -65,19 +67,21 @@ class PoolEditView extends events.EventTarget {
if (!list.length) {
this._namesFieldNode.setCustomValidity(
'Pools must have at least one name.');
"Pools must have at least one name."
);
return;
}
for (let item of list) {
if (!regex.test(item)) {
this._namesFieldNode.setCustomValidity(
`Pool name "${item}" contains invalid symbols.`);
`Pool name "${item}" contains invalid symbols.`
);
return;
}
}
this._namesFieldNode.setCustomValidity('');
this._namesFieldNode.setCustomValidity("");
}
_evtPostsInput(e) {
@ -87,57 +91,60 @@ class PoolEditView extends events.EventTarget {
for (let item of list) {
if (!regex.test(item)) {
this._postsFieldNode.setCustomValidity(
`Pool ID "${item}" is not an integer.`);
`Pool ID "${item}" is not an integer.`
);
return;
}
}
this._postsFieldNode.setCustomValidity('');
this._postsFieldNode.setCustomValidity("");
}
_evtSubmit(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
pool: this._pool,
this.dispatchEvent(
new CustomEvent("submit", {
detail: {
pool: this._pool,
names: this._namesFieldNode ?
misc.splitByWhitespace(this._namesFieldNode.value) :
undefined,
names: this._namesFieldNode
? misc.splitByWhitespace(this._namesFieldNode.value)
: undefined,
category: this._categoryFieldNode ?
this._categoryFieldNode.value :
undefined,
category: this._categoryFieldNode
? this._categoryFieldNode.value
: undefined,
description: this._descriptionFieldNode ?
this._descriptionFieldNode.value :
undefined,
description: this._descriptionFieldNode
? this._descriptionFieldNode.value
: undefined,
posts: this._postsFieldNode ?
misc.splitByWhitespace(this._postsFieldNode.value) :
undefined,
},
}));
posts: this._postsFieldNode
? misc.splitByWhitespace(this._postsFieldNode.value)
: undefined,
},
})
);
}
get _formNode() {
return this._hostNode.querySelector('form');
return this._hostNode.querySelector("form");
}
get _namesFieldNode() {
return this._formNode.querySelector('.names input');
return this._formNode.querySelector(".names input");
}
get _categoryFieldNode() {
return this._formNode.querySelector('.category select');
return this._formNode.querySelector(".category select");
}
get _descriptionFieldNode() {
return this._formNode.querySelector('.description textarea');
return this._formNode.querySelector(".description textarea");
}
get _postsFieldNode() {
return this._formNode.querySelector('.posts input');
return this._formNode.querySelector(".posts input");
}
}

View file

@ -1,12 +1,11 @@
'use strict';
"use strict";
const events = require('../events.js');
const api = require('../api.js');
const views = require('../util/views.js');
const PoolAutoCompleteControl =
require('../controls/pool_auto_complete_control.js');
const events = require("../events.js");
const api = require("../api.js");
const views = require("../util/views.js");
const PoolAutoCompleteControl = require("../controls/pool_auto_complete_control.js");
const template = views.getTemplate('pool-merge');
const template = views.getTemplate("pool-merge");
class PoolMergeView extends events.EventTarget {
constructor(ctx) {
@ -23,15 +22,18 @@ class PoolMergeView extends events.EventTarget {
this._autoCompleteControl = new PoolAutoCompleteControl(
this._targetPoolFieldNode,
{
confirm: pool => {
confirm: (pool) => {
this._targetPoolId = pool.id;
this._autoCompleteControl.replaceSelectedText(
pool.names[0], false);
}
});
pool.names[0],
false
);
},
}
);
}
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
this._formNode.addEventListener("submit", (e) => this._evtSubmit(e));
}
clearMessages() {
@ -56,24 +58,26 @@ class PoolMergeView extends events.EventTarget {
_evtSubmit(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
pool: this._pool,
targetPoolId: this._targetPoolId
},
}));
this.dispatchEvent(
new CustomEvent("submit", {
detail: {
pool: this._pool,
targetPoolId: this._targetPoolId,
},
})
);
}
get _formNode() {
return this._hostNode.querySelector('form');
return this._hostNode.querySelector("form");
}
get _targetPoolFieldNode() {
return this._formNode.querySelector('input[name=target-pool]');
return this._formNode.querySelector("input[name=target-pool]");
}
get _addAliasCheckboxNode() {
return this._formNode.querySelector('input[name=alias]');
return this._formNode.querySelector("input[name=alias]");
}
}

View file

@ -1,8 +1,8 @@
'use strict';
"use strict";
const views = require('../util/views.js');
const views = require("../util/views.js");
const template = views.getTemplate('pool-summary');
const template = views.getTemplate("pool-summary");
class PoolSummaryView {
constructor(ctx) {

View file

@ -1,26 +1,26 @@
'use strict';
"use strict";
const events = require('../events.js');
const views = require('../util/views.js');
const misc = require('../util/misc.js');
const PoolSummaryView = require('./pool_summary_view.js');
const PoolEditView = require('./pool_edit_view.js');
const PoolMergeView = require('./pool_merge_view.js');
const PoolDeleteView = require('./pool_delete_view.js');
const EmptyView = require('../views/empty_view.js');
const events = require("../events.js");
const views = require("../util/views.js");
const misc = require("../util/misc.js");
const PoolSummaryView = require("./pool_summary_view.js");
const PoolEditView = require("./pool_edit_view.js");
const PoolMergeView = require("./pool_merge_view.js");
const PoolDeleteView = require("./pool_delete_view.js");
const EmptyView = require("../views/empty_view.js");
const template = views.getTemplate('pool');
const template = views.getTemplate("pool");
class PoolView extends events.EventTarget {
constructor(ctx) {
super();
this._ctx = ctx;
ctx.pool.addEventListener('change', e => this._evtChange(e));
ctx.section = ctx.section || 'summary';
ctx.pool.addEventListener("change", (e) => this._evtChange(e));
ctx.section = ctx.section || "summary";
ctx.getPrettyPoolName = misc.getPrettyPoolName;
this._hostNode = document.getElementById('content-holder');
this._hostNode = document.getElementById("content-holder");
this._install();
}
@ -28,52 +28,54 @@ class PoolView extends events.EventTarget {
const ctx = this._ctx;
views.replaceContent(this._hostNode, template(ctx));
for (let item of this._hostNode.querySelectorAll('[data-name]')) {
for (let item of this._hostNode.querySelectorAll("[data-name]")) {
item.classList.toggle(
'active', item.getAttribute('data-name') === ctx.section);
if (item.getAttribute('data-name') === ctx.section) {
"active",
item.getAttribute("data-name") === ctx.section
);
if (item.getAttribute("data-name") === ctx.section) {
item.parentNode.scrollLeft =
item.getBoundingClientRect().left -
item.parentNode.getBoundingClientRect().left
item.parentNode.getBoundingClientRect().left;
}
}
ctx.hostNode = this._hostNode.querySelector('.pool-content-holder');
if (ctx.section === 'edit') {
ctx.hostNode = this._hostNode.querySelector(".pool-content-holder");
if (ctx.section === "edit") {
if (!this._ctx.canEditAnything) {
this._view = new EmptyView();
this._view.showError(
'You don\'t have privileges to edit pools.');
"You don't have privileges to edit pools."
);
} else {
this._view = new PoolEditView(ctx);
events.proxyEvent(this._view, this, 'submit');
events.proxyEvent(this._view, this, "submit");
}
} else if (ctx.section === 'merge') {
} else if (ctx.section === "merge") {
if (!this._ctx.canMerge) {
this._view = new EmptyView();
this._view.showError(
'You don\'t have privileges to merge pools.');
"You don't have privileges to merge pools."
);
} else {
this._view = new PoolMergeView(ctx);
events.proxyEvent(this._view, this, 'submit', 'merge');
events.proxyEvent(this._view, this, "submit", "merge");
}
} else if (ctx.section === 'delete') {
} else if (ctx.section === "delete") {
if (!this._ctx.canDelete) {
this._view = new EmptyView();
this._view.showError(
'You don\'t have privileges to delete pools.');
"You don't have privileges to delete pools."
);
} else {
this._view = new PoolDeleteView(ctx);
events.proxyEvent(this._view, this, 'submit', 'delete');
events.proxyEvent(this._view, this, "submit", "delete");
}
} else {
this._view = new PoolSummaryView(ctx);
}
events.proxyEvent(this._view, this, 'change');
events.proxyEvent(this._view, this, "change");
views.syncScrollPosition();
}

Some files were not shown because too many files have changed in this diff Show more