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: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0 rev: v2.4.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
- id: check-yaml - 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 - repo: https://github.com/pre-commit/mirrors-eslint
rev: v7.1.0 rev: v7.1.0
hooks: hooks:
- id: eslint - id: eslint
files: client/js/ files: client/js/
args: ['--fix'] args: ['--fix']
additional_dependencies:
- eslint-config-prettier
- repo: https://gitlab.com/pycqa/flake8 - repo: https://gitlab.com/pycqa/flake8
rev: '3.8.2' rev: '3.8.2'
hooks: hooks:
- id: flake8 - id: flake8
files: server/szurubooru/ files: server/szurubooru/
additional_dependencies: additional_dependencies:
- flake8-print - flake8-print
args: ['--config=server/setup.cfg'] args: ['--config=server/.flake8']
- repo: local - repo: local
hooks: 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 - id: docker-build-client
name: Test building the client in Docker name: Docker - build client
entry: bash -c 'docker build -t szurubooru-client:$(git rev-parse --short HEAD) client/' entry: bash -c 'docker build client/'
language: system language: system
types: [file] types: [file]
files: client/ files: client/
pass_filenames: false pass_filenames: false
- id: docker-build-server - id: docker-build-server
name: Test building the server in Docker name: Docker - build server
entry: bash -c 'docker build -t szurubooru-server:$(git rev-parse --short HEAD) server/' entry: bash -c 'docker build server/'
language: system language: system
types: [file] types: [file]
files: server/ files: server/
pass_filenames: false 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 exclude: LICENSE.md

View file

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

View file

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

View file

@ -1,19 +1,19 @@
'use strict'; "use strict";
const api = require('../api.js'); const api = require("../api.js");
const topNavigation = require('../models/top_navigation.js'); const topNavigation = require("../models/top_navigation.js");
const EmptyView = require('../views/empty_view.js'); const EmptyView = require("../views/empty_view.js");
class BasePostController { class BasePostController {
constructor(ctx) { constructor(ctx) {
if (!api.hasPrivilege('posts:view')) { if (!api.hasPrivilege("posts:view")) {
this._view = new EmptyView(); 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; return;
} }
topNavigation.activate('posts'); topNavigation.activate("posts");
topNavigation.setTitle('Post #' + ctx.parameters.id.toString()); topNavigation.setTitle("Post #" + ctx.parameters.id.toString());
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,28 +1,33 @@
'use strict'; "use strict";
const router = require('../router.js'); const router = require("../router.js");
const api = require('../api.js'); const api = require("../api.js");
const misc = require('../util/misc.js'); const misc = require("../util/misc.js");
const uri = require('../util/uri.js'); const uri = require("../util/uri.js");
const settings = require('../models/settings.js'); const settings = require("../models/settings.js");
const Post = require('../models/post.js'); const Post = require("../models/post.js");
const PostList = require('../models/post_list.js'); const PostList = require("../models/post_list.js");
const PostDetailView = require('../views/post_detail_view.js'); const PostDetailView = require("../views/post_detail_view.js");
const BasePostController = require('./base_post_controller.js'); const BasePostController = require("./base_post_controller.js");
const EmptyView = require('../views/empty_view.js'); const EmptyView = require("../views/empty_view.js");
class PostDetailController extends BasePostController { class PostDetailController extends BasePostController {
constructor(ctx, section) { constructor(ctx, section) {
super(ctx); super(ctx);
Post.get(ctx.parameters.id).then(post => { Post.get(ctx.parameters.id).then(
this._id = ctx.parameters.id; (post) => {
post.addEventListener('change', e => this._evtSaved(e, section)); this._id = ctx.parameters.id;
this._installView(post, section); post.addEventListener("change", (e) =>
}, error => { this._evtSaved(e, section)
this._view = new EmptyView(); );
this._view.showError(error.message); this._installView(post, section);
}); },
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
} }
showSuccess(message) { showSuccess(message) {
@ -33,58 +38,68 @@ class PostDetailController extends BasePostController {
this._view = new PostDetailView({ this._view = new PostDetailView({
post: post, post: post,
section: section, section: section,
canMerge: api.hasPrivilege('posts:merge'), canMerge: api.hasPrivilege("posts:merge"),
}); });
this._view.addEventListener('select', e => this._evtSelect(e)); this._view.addEventListener("select", (e) => this._evtSelect(e));
this._view.addEventListener('merge', e => this._evtMerge(e)); this._view.addEventListener("merge", (e) => this._evtMerge(e));
} }
_evtSelect(e) { _evtSelect(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
Post.get(e.detail.postId).then(post => { Post.get(e.detail.postId).then(
this._view.selectPost(post); (post) => {
this._view.enableForm(); this._view.selectPost(post);
}, error => { this._view.enableForm();
this._view.showError(error.message); },
this._view.enableForm(); (error) => {
}); this._view.showError(error.message);
this._view.enableForm();
}
);
} }
_evtSaved(e, section) { _evtSaved(e, section) {
misc.disableExitConfirmation(); misc.disableExitConfirmation();
if (this._id !== e.detail.post.id) { if (this._id !== e.detail.post.id) {
router.replace( router.replace(
uri.formatClientLink('post', e.detail.post.id, section), uri.formatClientLink("post", e.detail.post.id, section),
null, null,
false); false
);
} }
} }
_evtMerge(e) { _evtMerge(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
e.detail.post.merge(e.detail.targetPost.id, e.detail.useOldContent) e.detail.post
.then(() => { .merge(e.detail.targetPost.id, e.detail.useOldContent)
this._installView(e.detail.post, 'merge'); .then(
this._view.showSuccess('Post merged.'); () => {
router.replace( this._installView(e.detail.post, "merge");
uri.formatClientLink( this._view.showSuccess("Post merged.");
'post', e.detail.targetPost.id, 'merge'), router.replace(
null, uri.formatClientLink(
false); "post",
}, error => { e.detail.targetPost.id,
this._view.showError(error.message); "merge"
this._view.enableForm(); ),
}); null,
false
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
} }
} }
module.exports = router => { module.exports = (router) => {
router.enter( router.enter(["post", ":id", "merge"], (ctx, next) => {
['post', ':id', 'merge'], ctx.controller = new PostDetailController(ctx, "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 router = require("../router.js");
const api = require('../api.js'); const api = require("../api.js");
const settings = require('../models/settings.js'); const settings = require("../models/settings.js");
const uri = require('../util/uri.js'); const uri = require("../util/uri.js");
const PostList = require('../models/post_list.js'); const PostList = require("../models/post_list.js");
const topNavigation = require('../models/top_navigation.js'); const topNavigation = require("../models/top_navigation.js");
const PageController = require('../controllers/page_controller.js'); const PageController = require("../controllers/page_controller.js");
const PostsHeaderView = require('../views/posts_header_view.js'); const PostsHeaderView = require("../views/posts_header_view.js");
const PostsPageView = require('../views/posts_page_view.js'); const PostsPageView = require("../views/posts_page_view.js");
const EmptyView = require('../views/empty_view.js'); const EmptyView = require("../views/empty_view.js");
const fields = [ const fields = [
'id', 'thumbnailUrl', 'type', 'safety', "id",
'score', 'favoriteCount', 'commentCount', 'tags', 'version' "thumbnailUrl",
"type",
"safety",
"score",
"favoriteCount",
"commentCount",
"tags",
"version",
]; ];
class PostListController { class PostListController {
constructor(ctx) { constructor(ctx) {
this._pageController = new PageController(); this._pageController = new PageController();
if (!api.hasPrivilege('posts:list')) { if (!api.hasPrivilege("posts:list")) {
this._view = new EmptyView(); 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; return;
} }
this._ctx = ctx; this._ctx = ctx;
topNavigation.activate('posts'); topNavigation.activate("posts");
topNavigation.setTitle('Listing posts'); topNavigation.setTitle("Listing posts");
this._headerView = new PostsHeaderView({ this._headerView = new PostsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode, hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters, parameters: ctx.parameters,
enableSafety: api.safetyEnabled(), enableSafety: api.safetyEnabled(),
canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'), canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
canBulkEditSafety: api.hasPrivilege('posts:bulk-edit:safety'), canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"),
bulkEdit: { bulkEdit: {
tags: this._bulkEditTags tags: this._bulkEditTags,
}, },
}); });
this._headerView.addEventListener( this._headerView.addEventListener("navigate", (e) =>
'navigate', e => this._evtNavigate(e)); this._evtNavigate(e)
);
this._syncPageController(); this._syncPageController();
} }
@ -52,33 +60,35 @@ class PostListController {
} }
get _bulkEditTags() { get _bulkEditTags() {
return (this._ctx.parameters.tag || '').split(/\s+/).filter(s => s); return (this._ctx.parameters.tag || "").split(/\s+/).filter((s) => s);
} }
_evtNavigate(e) { _evtNavigate(e) {
router.showNoDispatch( router.showNoDispatch(
uri.formatClientLink('posts', e.detail.parameters)); uri.formatClientLink("posts", e.detail.parameters)
);
Object.assign(this._ctx.parameters, e.detail.parameters); Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController(); this._syncPageController();
} }
_evtTag(e) { _evtTag(e) {
Promise.all( 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()) .then(e.detail.post.save())
.catch(error => window.alert(error.message)); .catch((error) => window.alert(error.message));
} }
_evtUntag(e) { _evtUntag(e) {
for (let tag of this._bulkEditTags) { for (let tag of this._bulkEditTags) {
e.detail.post.tags.removeByName(tag); 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) { _evtChangeSafety(e) {
e.detail.post.safety = e.detail.safety; 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() { _syncPageController() {
@ -86,39 +96,45 @@ class PostListController {
parameters: this._ctx.parameters, parameters: this._ctx.parameters,
defaultLimit: parseInt(settings.get().postsPerPage), defaultLimit: parseInt(settings.get().postsPerPage),
getClientUrlForPage: (offset, limit) => { getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign( const parameters = Object.assign({}, this._ctx.parameters, {
{}, this._ctx.parameters, {offset: offset, limit: limit}); offset: offset,
return uri.formatClientLink('posts', parameters); limit: limit,
});
return uri.formatClientLink("posts", parameters);
}, },
requestPage: (offset, limit) => { requestPage: (offset, limit) => {
return PostList.search( return PostList.search(
this._ctx.parameters.query, offset, limit, fields); this._ctx.parameters.query,
offset,
limit,
fields
);
}, },
pageRenderer: pageCtx => { pageRenderer: (pageCtx) => {
Object.assign(pageCtx, { Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege('posts:view'), canViewPosts: api.hasPrivilege("posts:view"),
canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'), canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
canBulkEditSafety: canBulkEditSafety: api.hasPrivilege(
api.hasPrivilege('posts:bulk-edit:safety'), "posts:bulk-edit:safety"
),
bulkEdit: { bulkEdit: {
tags: this._bulkEditTags, tags: this._bulkEditTags,
}, },
}); });
const view = new PostsPageView(pageCtx); const view = new PostsPageView(pageCtx);
view.addEventListener('tag', e => this._evtTag(e)); view.addEventListener("tag", (e) => this._evtTag(e));
view.addEventListener('untag', e => this._evtUntag(e)); view.addEventListener("untag", (e) => this._evtUntag(e));
view.addEventListener( view.addEventListener("changeSafety", (e) =>
'changeSafety', e => this._evtChangeSafety(e)); this._evtChangeSafety(e)
);
return view; return view;
}, },
}); });
} }
} }
module.exports = router => { module.exports = (router) => {
router.enter( router.enter(["posts"], (ctx, next) => {
['posts'], ctx.controller = new PostListController(ctx);
(ctx, next) => { });
ctx.controller = new PostListController(ctx);
});
}; };

View file

@ -1,16 +1,16 @@
'use strict'; "use strict";
const router = require('../router.js'); const router = require("../router.js");
const api = require('../api.js'); const api = require("../api.js");
const uri = require('../util/uri.js'); const uri = require("../util/uri.js");
const misc = require('../util/misc.js'); const misc = require("../util/misc.js");
const settings = require('../models/settings.js'); const settings = require("../models/settings.js");
const Comment = require('../models/comment.js'); const Comment = require("../models/comment.js");
const Post = require('../models/post.js'); const Post = require("../models/post.js");
const PostList = require('../models/post_list.js'); const PostList = require("../models/post_list.js");
const PostMainView = require('../views/post_main_view.js'); const PostMainView = require("../views/post_main_view.js");
const BasePostController = require('./base_post_controller.js'); const BasePostController = require("./base_post_controller.js");
const EmptyView = require('../views/empty_view.js'); const EmptyView = require("../views/empty_view.js");
class PostMainController extends BasePostController { class PostMainController extends BasePostController {
constructor(ctx, editMode) { constructor(ctx, editMode) {
@ -21,74 +21,107 @@ class PostMainController extends BasePostController {
Post.get(ctx.parameters.id), Post.get(ctx.parameters.id),
PostList.getAround( PostList.getAround(
ctx.parameters.id, ctx.parameters.id,
parameters ? parameters.query : null), parameters ? parameters.query : null
]).then(responses => { ),
const [post, aroundResponse] = responses; ]).then(
(responses) => {
const [post, aroundResponse] = responses;
// remove junk from query, but save it into history so that it can // remove junk from query, but save it into history so that it can
// be still accessed after history navigation / page refresh // be still accessed after history navigation / page refresh
if (parameters.query) { if (parameters.query) {
ctx.state.parameters = parameters; ctx.state.parameters = parameters;
const url = editMode ? const url = editMode
uri.formatClientLink('post', ctx.parameters.id, 'edit') : ? uri.formatClientLink(
uri.formatClientLink('post', ctx.parameters.id); "post",
router.replace(url, ctx.state, false); 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) { _evtFitModeChange(e) {
@ -100,32 +133,36 @@ class PostMainController extends BasePostController {
_evtFeaturePost(e) { _evtFeaturePost(e) {
this._view.sidebarControl.disableForm(); this._view.sidebarControl.disableForm();
this._view.sidebarControl.clearMessages(); this._view.sidebarControl.clearMessages();
e.detail.post.feature() e.detail.post.feature().then(
.then(() => { () => {
this._view.sidebarControl.showSuccess('Post featured.'); this._view.sidebarControl.showSuccess("Post featured.");
this._view.sidebarControl.enableForm(); this._view.sidebarControl.enableForm();
}, error => { },
(error) => {
this._view.sidebarControl.showError(error.message); this._view.sidebarControl.showError(error.message);
this._view.sidebarControl.enableForm(); this._view.sidebarControl.enableForm();
}); }
);
} }
_evtMergePost(e) { _evtMergePost(e) {
router.show(uri.formatClientLink('post', e.detail.post.id, 'merge')); router.show(uri.formatClientLink("post", e.detail.post.id, "merge"));
} }
_evtDeletePost(e) { _evtDeletePost(e) {
this._view.sidebarControl.disableForm(); this._view.sidebarControl.disableForm();
this._view.sidebarControl.clearMessages(); this._view.sidebarControl.clearMessages();
e.detail.post.delete() e.detail.post.delete().then(
.then(() => { () => {
misc.disableExitConfirmation(); misc.disableExitConfirmation();
const ctx = router.show(uri.formatClientLink('posts')); const ctx = router.show(uri.formatClientLink("posts"));
ctx.controller.showSuccess('Post deleted.'); ctx.controller.showSuccess("Post deleted.");
}, error => { },
(error) => {
this._view.sidebarControl.showError(error.message); this._view.sidebarControl.showError(error.message);
this._view.sidebarControl.enableForm(); this._view.sidebarControl.enableForm();
}); }
);
} }
_evtUpdatePost(e) { _evtUpdatePost(e) {
@ -150,15 +187,17 @@ class PostMainController extends BasePostController {
if (e.detail.source !== undefined) { if (e.detail.source !== undefined) {
post.source = e.detail.source; post.source = e.detail.source;
} }
post.save() post.save().then(
.then(() => { () => {
this._view.sidebarControl.showSuccess('Post saved.'); this._view.sidebarControl.showSuccess("Post saved.");
this._view.sidebarControl.enableForm(); this._view.sidebarControl.enableForm();
misc.disableExitConfirmation(); misc.disableExitConfirmation();
}, error => { },
(error) => {
this._view.sidebarControl.showError(error.message); this._view.sidebarControl.showError(error.message);
this._view.sidebarControl.enableForm(); this._view.sidebarControl.enableForm();
}); }
);
} }
_evtPostChange(e) { _evtPostChange(e) {
@ -173,79 +212,82 @@ class PostMainController extends BasePostController {
this._view.commentControl.disableForm(); this._view.commentControl.disableForm();
const comment = Comment.create(this._post.id); const comment = Comment.create(this._post.id);
comment.text = e.detail.text; comment.text = e.detail.text;
comment.save() comment.save().then(
.then(() => { () => {
this._post.comments.add(comment); this._post.comments.add(comment);
this._view.commentControl.exitEditMode(); this._view.commentControl.exitEditMode();
this._view.commentControl.enableForm(); this._view.commentControl.enableForm();
misc.disableExitConfirmation(); misc.disableExitConfirmation();
}, error => { },
(error) => {
this._view.commentControl.showError(error.message); this._view.commentControl.showError(error.message);
this._view.commentControl.enableForm(); this._view.commentControl.enableForm();
}); }
);
} }
_evtUpdateComment(e) { _evtUpdateComment(e) {
// TODO: disable form // TODO: disable form
e.detail.comment.text = e.detail.text; e.detail.comment.text = e.detail.text;
e.detail.comment.save() e.detail.comment.save().catch((error) => {
.catch(error => { e.detail.target.showError(error.message);
e.detail.target.showError(error.message); // TODO: enable form
// TODO: enable form });
});
} }
_evtScoreComment(e) { _evtScoreComment(e) {
e.detail.comment.setScore(e.detail.score) e.detail.comment
.catch(error => window.alert(error.message)); .setScore(e.detail.score)
.catch((error) => window.alert(error.message));
} }
_evtDeleteComment(e) { _evtDeleteComment(e) {
e.detail.comment.delete() e.detail.comment
.catch(error => window.alert(error.message)); .delete()
.catch((error) => window.alert(error.message));
} }
_evtScorePost(e) { _evtScorePost(e) {
if (!api.hasPrivilege('posts:score')) { if (!api.hasPrivilege("posts:score")) {
return; return;
} }
e.detail.post.setScore(e.detail.score) e.detail.post
.catch(error => window.alert(error.message)); .setScore(e.detail.score)
.catch((error) => window.alert(error.message));
} }
_evtFavoritePost(e) { _evtFavoritePost(e) {
if (!api.hasPrivilege('posts:favorite')) { if (!api.hasPrivilege("posts:favorite")) {
return; return;
} }
e.detail.post.addToFavorites() e.detail.post
.catch(error => window.alert(error.message)); .addToFavorites()
.catch((error) => window.alert(error.message));
} }
_evtUnfavoritePost(e) { _evtUnfavoritePost(e) {
if (!api.hasPrivilege('posts:favorite')) { if (!api.hasPrivilege("posts:favorite")) {
return; return;
} }
e.detail.post.removeFromFavorites() e.detail.post
.catch(error => window.alert(error.message)); .removeFromFavorites()
.catch((error) => window.alert(error.message));
} }
} }
module.exports = router => { module.exports = (router) => {
router.enter(['post', ':id', 'edit'], router.enter(["post", ":id", "edit"], (ctx, next) => {
(ctx, next) => { // restore parameters from history state
// restore parameters from history state if (ctx.state.parameters) {
if (ctx.state.parameters) { Object.assign(ctx.parameters, ctx.state.parameters);
Object.assign(ctx.parameters, ctx.state.parameters); }
} ctx.controller = new PostMainController(ctx, true);
ctx.controller = new PostMainController(ctx, true); });
}); router.enter(["post", ":id"], (ctx, next) => {
router.enter( // restore parameters from history state
['post', ':id'], if (ctx.state.parameters) {
(ctx, next) => { Object.assign(ctx.parameters, ctx.state.parameters);
// restore parameters from history state }
if (ctx.state.parameters) { ctx.controller = new PostMainController(ctx, false);
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 api = require("../api.js");
const router = require('../router.js'); const router = require("../router.js");
const uri = require('../util/uri.js'); const uri = require("../util/uri.js");
const misc = require('../util/misc.js'); const misc = require("../util/misc.js");
const progress = require('../util/progress.js'); const progress = require("../util/progress.js");
const topNavigation = require('../models/top_navigation.js'); const topNavigation = require("../models/top_navigation.js");
const Post = require('../models/post.js'); const Post = require("../models/post.js");
const Tag = require('../models/tag.js'); const Tag = require("../models/tag.js");
const PostUploadView = require('../views/post_upload_view.js'); const PostUploadView = require("../views/post_upload_view.js");
const EmptyView = require('../views/empty_view.js'); const EmptyView = require("../views/empty_view.js");
const genericErrorMessage = const genericErrorMessage =
'One of the posts needs your attention; ' + "One of the posts needs your attention; " +
'click "resume upload" when you\'re ready.'; 'click "resume upload" when you\'re ready.';
class PostUploadController { class PostUploadController {
constructor() { constructor() {
this._lastCancellablePromise = null; this._lastCancellablePromise = null;
if (!api.hasPrivilege('posts:create')) { if (!api.hasPrivilege("posts:create")) {
this._view = new EmptyView(); 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; return;
} }
topNavigation.activate('upload'); topNavigation.activate("upload");
topNavigation.setTitle('Upload'); topNavigation.setTitle("Upload");
this._view = new PostUploadView({ this._view = new PostUploadView({
canUploadAnonymously: api.hasPrivilege('posts:create:anonymous'), canUploadAnonymously: api.hasPrivilege("posts:create:anonymous"),
canViewPosts: api.hasPrivilege('posts:view'), canViewPosts: api.hasPrivilege("posts:view"),
enableSafety: api.safetyEnabled(), enableSafety: api.safetyEnabled(),
}); });
this._view.addEventListener('change', e => this._evtChange(e)); this._view.addEventListener("change", (e) => this._evtChange(e));
this._view.addEventListener('submit', e => this._evtSubmit(e)); this._view.addEventListener("submit", (e) => this._evtSubmit(e));
this._view.addEventListener('cancel', e => this._evtCancel(e)); this._view.addEventListener("cancel", (e) => this._evtCancel(e));
} }
_evtChange(e) { _evtChange(e) {
@ -56,87 +56,109 @@ class PostUploadController {
this._view.disableForm(); this._view.disableForm();
this._view.clearMessages(); this._view.clearMessages();
e.detail.uploadables.reduce( e.detail.uploadables
(promise, uploadable) => promise.then(() => this._uploadSinglePost( .reduce(
uploadable, e.detail.skipDuplicates)), (promise, uploadable) =>
Promise.resolve()) promise.then(() =>
.then(() => { this._uploadSinglePost(
this._view.clearMessages(); uploadable,
misc.disableExitConfirmation(); e.detail.skipDuplicates
const ctx = router.show(uri.formatClientLink('posts')); )
ctx.controller.showSuccess('Posts uploaded.'); ),
}, error => { Promise.resolve()
if (error.uploadable) { )
if (error.similarPosts) { .then(
error.uploadable.lookalikes = error.similarPosts; () => {
this._view.updateUploadable(error.uploadable); this._view.clearMessages();
this._view.showInfo(genericErrorMessage); misc.disableExitConfirmation();
this._view.showInfo( const ctx = router.show(uri.formatClientLink("posts"));
error.message, error.uploadable); 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 { } else {
this._view.showError(genericErrorMessage); this._view.showError(error.message);
this._view.showError(
error.message, error.uploadable);
} }
} else { this._view.enableForm();
this._view.showError(error.message);
} }
this._view.enableForm(); );
});
} }
_uploadSinglePost(uploadable, skipDuplicates) { _uploadSinglePost(uploadable, skipDuplicates) {
progress.start(); progress.start();
let reverseSearchPromise = Promise.resolve(); let reverseSearchPromise = Promise.resolve();
if (!uploadable.lookalikesConfirmed) { if (!uploadable.lookalikesConfirmed) {
reverseSearchPromise = reverseSearchPromise = Post.reverseSearch(
Post.reverseSearch(uploadable.url || uploadable.file); uploadable.url || uploadable.file
);
} }
this._lastCancellablePromise = reverseSearchPromise; this._lastCancellablePromise = reverseSearchPromise;
return reverseSearchPromise.then(searchResult => { return reverseSearchPromise
if (searchResult) { .then((searchResult) => {
// notify about exact duplicate if (searchResult) {
if (searchResult.exactPost) { // notify about exact duplicate
if (skipDuplicates) { if (searchResult.exactPost) {
this._view.removeUploadable(uploadable); if (skipDuplicates) {
return Promise.resolve(); this._view.removeUploadable(uploadable);
} else { return Promise.resolve();
let error = new Error('Post already uploaded ' + } else {
`(@${searchResult.exactPost.id})`); 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.uploadable = uploadable;
error.similarPosts = searchResult.similarPosts;
return Promise.reject(error); return Promise.reject(error);
} }
} }
// notify about similar posts // no duplicates, proceed with saving
if (searchResult.similarPosts.length) { let post = this._uploadableToPost(uploadable);
let error = new Error( let savePromise = post.save(uploadable.anonymous).then(() => {
`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(() => {
this._view.removeUploadable(uploadable); this._view.removeUploadable(uploadable);
return Promise.resolve(); return Promise.resolve();
}); });
this._lastCancellablePromise = savePromise; this._lastCancellablePromise = savePromise;
return savePromise; return savePromise;
}).then(result => { })
progress.done(); .then(
return Promise.resolve(result); (result) => {
}, error => { progress.done();
error.uploadable = uploadable; return Promise.resolve(result);
progress.done(); },
return Promise.reject(error); (error) => {
}); error.uploadable = uploadable;
progress.done();
return Promise.reject(error);
}
);
} }
_uploadableToPost(uploadable) { _uploadableToPost(uploadable) {
@ -159,8 +181,8 @@ class PostUploadController {
} }
} }
module.exports = router => { module.exports = (router) => {
router.enter(['upload'], (ctx, next) => { router.enter(["upload"], (ctx, next) => {
ctx.controller = new PostUploadController(); ctx.controller = new PostUploadController();
}); });
}; };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,19 +1,20 @@
'use strict'; "use strict";
const api = require('../api.js'); const api = require("../api.js");
const topNavigation = require('../models/top_navigation.js'); const topNavigation = require("../models/top_navigation.js");
const TopNavigationView = require('../views/top_navigation_view.js'); const TopNavigationView = require("../views/top_navigation_view.js");
class TopNavigationController { class TopNavigationController {
constructor() { constructor() {
api.fetchConfig().then(() => { api.fetchConfig().then(() => {
this._topNavigationView = new TopNavigationView(); this._topNavigationView = new TopNavigationView();
topNavigation.addEventListener( topNavigation.addEventListener("activate", (e) =>
'activate', e => this._evtActivate(e)); this._evtActivate(e)
);
api.addEventListener('login', e => this._evtAuthChange(e)); api.addEventListener("login", (e) => this._evtAuthChange(e));
api.addEventListener('logout', e => this._evtAuthChange(e)); api.addEventListener("logout", (e) => this._evtAuthChange(e));
this._render(); this._render();
}); });
@ -28,37 +29,38 @@ class TopNavigationController {
} }
_updateNavigationFromPrivileges() { _updateNavigationFromPrivileges() {
topNavigation.get('account').url = 'user/' + api.userName; topNavigation.get("account").url = "user/" + api.userName;
topNavigation.get('account').imageUrl = topNavigation.get("account").imageUrl = api.user
api.user ? api.user.avatarUrl : null; ? api.user.avatarUrl
: null;
topNavigation.showAll(); topNavigation.showAll();
if (!api.hasPrivilege('posts:list')) { if (!api.hasPrivilege("posts:list")) {
topNavigation.hide('posts'); topNavigation.hide("posts");
} }
if (!api.hasPrivilege('posts:create')) { if (!api.hasPrivilege("posts:create")) {
topNavigation.hide('upload'); topNavigation.hide("upload");
} }
if (!api.hasPrivilege('comments:list')) { if (!api.hasPrivilege("comments:list")) {
topNavigation.hide('comments'); topNavigation.hide("comments");
} }
if (!api.hasPrivilege('tags:list')) { if (!api.hasPrivilege("tags:list")) {
topNavigation.hide('tags'); topNavigation.hide("tags");
} }
if (!api.hasPrivilege('users:list')) { if (!api.hasPrivilege("users:list")) {
topNavigation.hide('users'); topNavigation.hide("users");
} }
if (api.isLoggedIn()) { if (api.isLoggedIn()) {
if (!api.hasPrivilege('users:create:any')) { if (!api.hasPrivilege("users:create:any")) {
topNavigation.hide('register'); topNavigation.hide("register");
} }
topNavigation.hide('login'); topNavigation.hide("login");
} else { } else {
if (!api.hasPrivilege('users:create:self')) { if (!api.hasPrivilege("users:create:self")) {
topNavigation.hide('register'); topNavigation.hide("register");
} }
topNavigation.hide('account'); topNavigation.hide("account");
topNavigation.hide('logout'); topNavigation.hide("logout");
} }
} }
@ -66,10 +68,11 @@ class TopNavigationController {
this._updateNavigationFromPrivileges(); this._updateNavigationFromPrivileges();
this._topNavigationView.render({ this._topNavigationView.render({
items: topNavigation.getAll(), items: topNavigation.getAll(),
name: api.getName() name: api.getName(),
}); });
this._topNavigationView.activate( 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 router = require("../router.js");
const api = require('../api.js'); const api = require("../api.js");
const uri = require('../util/uri.js'); const uri = require("../util/uri.js");
const misc = require('../util/misc.js'); const misc = require("../util/misc.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const User = require('../models/user.js'); const User = require("../models/user.js");
const UserToken = require('../models/user_token.js'); const UserToken = require("../models/user_token.js");
const topNavigation = require('../models/top_navigation.js'); const topNavigation = require("../models/top_navigation.js");
const UserView = require('../views/user_view.js'); const UserView = require("../views/user_view.js");
const EmptyView = require('../views/empty_view.js'); const EmptyView = require("../views/empty_view.js");
class UserController { class UserController {
constructor(ctx, section) { constructor(ctx, section) {
const userName = ctx.parameters.name; const userName = ctx.parameters.name;
if (!api.hasPrivilege('users:view') && if (
!api.isLoggedIn({name: userName})) { !api.hasPrivilege("users:view") &&
!api.isLoggedIn({ name: userName })
) {
this._view = new EmptyView(); 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; return;
} }
@ -25,100 +27,129 @@ class UserController {
this._errorMessages = []; this._errorMessages = [];
let userTokenPromise = Promise.resolve([]); let userTokenPromise = Promise.resolve([]);
if (section === 'list-tokens') { if (section === "list-tokens") {
userTokenPromise = UserToken.get(userName) userTokenPromise = UserToken.get(userName).then(
.then(userTokens => { (userTokens) => {
return userTokens.map(token => { return userTokens.map((token) => {
token.isCurrentAuthToken = api.isCurrentAuthToken(token); token.isCurrentAuthToken = api.isCurrentAuthToken(
token
);
return token; return token;
}); });
}, error => { },
(error) => {
return []; return [];
}); }
);
} }
topNavigation.setTitle('User ' + userName); topNavigation.setTitle("User " + userName);
Promise.all([ Promise.all([userTokenPromise, User.get(userName)]).then(
userTokenPromise, (responses) => {
User.get(userName) const [userTokens, user] = responses;
]).then(responses => { const isLoggedIn = api.isLoggedIn(user);
const [userTokens, user] = responses; const infix = isLoggedIn ? "self" : "any";
const isLoggedIn = api.isLoggedIn(user);
const infix = isLoggedIn ? 'self' : 'any';
this._name = userName; this._name = userName;
user.addEventListener('change', e => this._evtSaved(e, section)); user.addEventListener("change", (e) =>
this._evtSaved(e, section)
);
const myRankIndex = api.user ? const myRankIndex = api.user
api.allRanks.indexOf(api.user.rank) : ? api.allRanks.indexOf(api.user.rank)
0; : 0;
let ranks = {}; let ranks = {};
for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) { for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) {
if (rankIdentifier === 'anonymous') { if (rankIdentifier === "anonymous") {
continue; 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) { showSuccess(message) {
if (typeof this._view === 'undefined') { if (typeof this._view === "undefined") {
this._successMessages.push(message) this._successMessages.push(message);
} else { } else {
this._view.showSuccess(message); this._view.showSuccess(message);
} }
} }
showError(message) { showError(message) {
if (typeof this._view === 'undefined') { if (typeof this._view === "undefined") {
this._errorMessages.push(message) this._errorMessages.push(message);
} else { } else {
this._view.showError(message); this._view.showError(message);
} }
@ -132,9 +163,10 @@ class UserController {
misc.disableExitConfirmation(); misc.disableExitConfirmation();
if (this._name !== e.detail.user.name) { if (this._name !== e.detail.user.name) {
router.replace( router.replace(
uri.formatClientLink('user', e.detail.user.name, section), uri.formatClientLink("user", e.detail.user.name, section),
null, null,
false); false
);
} }
} }
@ -142,7 +174,7 @@ class UserController {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
const isLoggedIn = api.isLoggedIn(e.detail.user); const isLoggedIn = api.isLoggedIn(e.detail.user);
const infix = isLoggedIn ? 'self' : 'any'; const infix = isLoggedIn ? "self" : "any";
if (e.detail.name !== undefined) { if (e.detail.name !== undefined) {
e.detail.user.name = e.detail.name; e.detail.user.name = e.detail.name;
@ -165,72 +197,105 @@ class UserController {
} }
} }
e.detail.user.save().then(() => { e.detail.user
return isLoggedIn ? .save()
api.login( .then(() => {
e.detail.name || api.userName, return isLoggedIn
e.detail.password || api.userPassword, ? api.login(
false) : e.detail.name || api.userName,
Promise.resolve(); e.detail.password || api.userPassword,
}).then(() => { false
this._view.showSuccess('Settings updated.'); )
this._view.enableForm(); : Promise.resolve();
}, error => { })
this._view.showError(error.message); .then(
this._view.enableForm(); () => {
}); this._view.showSuccess("Settings updated.");
this._view.enableForm();
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
} }
_evtDelete(e) { _evtDelete(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
const isLoggedIn = api.isLoggedIn(e.detail.user); const isLoggedIn = api.isLoggedIn(e.detail.user);
e.detail.user.delete() e.detail.user.delete().then(
.then(() => { () => {
if (isLoggedIn) { if (isLoggedIn) {
api.forget(); api.forget();
api.logout(); api.logout();
} }
if (api.hasPrivilege('users:list')) { if (api.hasPrivilege("users:list")) {
const ctx = router.show(uri.formatClientLink('users')); const ctx = router.show(uri.formatClientLink("users"));
ctx.controller.showSuccess('Account deleted.'); ctx.controller.showSuccess("Account deleted.");
} else { } else {
const ctx = router.show(uri.formatClientLink()); 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.showError(error.message);
this._view.enableForm(); this._view.enableForm();
}); }
);
} }
_evtCreateToken(e) { _evtCreateToken(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
UserToken.create(e.detail.user.name, e.detail.note, e.detail.expirationTime) UserToken.create(
.then(response => { e.detail.user.name,
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens')); e.detail.note,
ctx.controller.showSuccess('Token ' + response.token + ' created.'); e.detail.expirationTime
}, error => { ).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.showError(error.message);
this._view.enableForm(); this._view.enableForm();
}); }
);
} }
_evtDeleteToken(e) { _evtDeleteToken(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
if (api.isCurrentAuthToken(e.detail.userToken)) { if (api.isCurrentAuthToken(e.detail.userToken)) {
router.show(uri.formatClientLink('logout')); router.show(uri.formatClientLink("logout"));
} else { } else {
e.detail.userToken.delete(e.detail.user.name) e.detail.userToken.delete(e.detail.user.name).then(
.then(() => { () => {
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens')); const ctx = router.show(
ctx.controller.showSuccess('Token ' + e.detail.userToken.token + ' deleted.'); uri.formatClientLink(
}, error => { "user",
e.detail.user.name,
"list-tokens"
)
);
ctx.controller.showSuccess(
"Token " + e.detail.userToken.token + " deleted."
);
},
(error) => {
this._view.showError(error.message); this._view.showError(error.message);
this._view.enableForm(); this._view.enableForm();
}); }
);
} }
} }
@ -242,27 +307,38 @@ class UserController {
e.detail.userToken.note = e.detail.note; e.detail.userToken.note = e.detail.note;
} }
e.detail.userToken.save(e.detail.user.name).then(response => { e.detail.userToken.save(e.detail.user.name).then(
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens')); (response) => {
ctx.controller.showSuccess('Token ' + response.token + ' updated.'); const ctx = router.show(
}, error => { uri.formatClientLink(
this._view.showError(error.message); "user",
this._view.enableForm(); 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 => { module.exports = (router) => {
router.enter(['user', ':name'], (ctx, next) => { router.enter(["user", ":name"], (ctx, next) => {
ctx.controller = new UserController(ctx, 'summary'); ctx.controller = new UserController(ctx, "summary");
}); });
router.enter(['user', ':name', 'edit'], (ctx, next) => { router.enter(["user", ":name", "edit"], (ctx, next) => {
ctx.controller = new UserController(ctx, 'edit'); ctx.controller = new UserController(ctx, "edit");
}); });
router.enter(['user', ':name', 'list-tokens'], (ctx, next) => { router.enter(["user", ":name", "list-tokens"], (ctx, next) => {
ctx.controller = new UserController(ctx, 'list-tokens'); ctx.controller = new UserController(ctx, "list-tokens");
}); });
router.enter(['user', ':name', 'delete'], (ctx, next) => { router.enter(["user", ":name", "delete"], (ctx, next) => {
ctx.controller = new UserController(ctx, 'delete'); ctx.controller = new UserController(ctx, "delete");
}); });
}; };

View file

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

View file

@ -1,25 +1,25 @@
'use strict'; "use strict";
const router = require('../router.js'); const router = require("../router.js");
const api = require('../api.js'); const api = require("../api.js");
const uri = require('../util/uri.js'); const uri = require("../util/uri.js");
const User = require('../models/user.js'); const User = require("../models/user.js");
const topNavigation = require('../models/top_navigation.js'); const topNavigation = require("../models/top_navigation.js");
const RegistrationView = require('../views/registration_view.js'); const RegistrationView = require("../views/registration_view.js");
const EmptyView = require('../views/empty_view.js'); const EmptyView = require("../views/empty_view.js");
class UserRegistrationController { class UserRegistrationController {
constructor() { constructor() {
if (!api.hasPrivilege('users:create:self')) { if (!api.hasPrivilege("users:create:self")) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError('Registration is closed.'); this._view.showError("Registration is closed.");
return; return;
} }
topNavigation.activate('register'); topNavigation.activate("register");
topNavigation.setTitle('Registration'); topNavigation.setTitle("Registration");
this._view = new RegistrationView(); this._view = new RegistrationView();
this._view.addEventListener('submit', e => this._evtRegister(e)); this._view.addEventListener("submit", (e) => this._evtRegister(e));
} }
_evtRegister(e) { _evtRegister(e) {
@ -30,30 +30,35 @@ class UserRegistrationController {
user.email = e.detail.email; user.email = e.detail.email;
user.password = e.detail.password; user.password = e.detail.password;
const isLoggedIn = api.isLoggedIn(); const isLoggedIn = api.isLoggedIn();
user.save().then(() => { user.save()
if (isLoggedIn) { .then(() => {
return Promise.resolve(); if (isLoggedIn) {
} else { return Promise.resolve();
api.forget(); } else {
return api.login(e.detail.name, e.detail.password, false); api.forget();
} return api.login(e.detail.name, e.detail.password, false);
}).then(() => { }
if (isLoggedIn) { })
const ctx = router.show(uri.formatClientLink('users')); .then(
ctx.controller.showSuccess('User added!'); () => {
} else { if (isLoggedIn) {
const ctx = router.show(uri.formatClientLink()); const ctx = router.show(uri.formatClientLink("users"));
ctx.controller.showSuccess('Welcome aboard!'); ctx.controller.showSuccess("User added!");
} } else {
}, error => { const ctx = router.show(uri.formatClientLink());
this._view.showError(error.message); ctx.controller.showSuccess("Welcome aboard!");
this._view.enableForm(); }
}); },
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
} }
} }
module.exports = router => { module.exports = (router) => {
router.enter(['register'], (ctx, next) => { router.enter(["register"], (ctx, next) => {
new UserRegistrationController(); 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_TAB = 9;
const KEY_RETURN = 13; const KEY_RETURN = 13;
@ -10,14 +10,14 @@ const KEY_UP = 38;
const KEY_DOWN = 40; const KEY_DOWN = 40;
function _getSelectionStart(input) { function _getSelectionStart(input) {
if ('selectionStart' in input) { if ("selectionStart" in input) {
return input.selectionStart; return input.selectionStart;
} }
if (document.selection) { if (document.selection) {
input.focus(); input.focus();
const sel = document.selection.createRange(); const sel = document.selection.createRange();
const selLen = document.selection.createRange().text.length; 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 sel.text.length - selLen;
} }
return 0; return 0;
@ -27,18 +27,22 @@ class AutoCompleteControl {
constructor(sourceInputNode, options) { constructor(sourceInputNode, options) {
this._sourceInputNode = sourceInputNode; this._sourceInputNode = sourceInputNode;
this._options = {}; this._options = {};
Object.assign(this._options, { Object.assign(
verticalShift: 2, this._options,
maxResults: 15, {
getTextToFind: () => { verticalShift: 2,
const value = sourceInputNode.value; maxResults: 15,
const start = _getSelectionStart(sourceInputNode); getTextToFind: () => {
return value.substring(0, start).replace(/.*\s+/, ''); const value = sourceInputNode.value;
const start = _getSelectionStart(sourceInputNode);
return value.substring(0, start).replace(/.*\s+/, "");
},
confirm: null,
delete: null,
getMatches: null,
}, },
confirm: null, options
delete: null, );
getMatches: null,
}, options);
this._showTimeout = null; this._showTimeout = null;
this._results = []; this._results = [];
@ -49,22 +53,22 @@ class AutoCompleteControl {
hide() { hide() {
window.clearTimeout(this._showTimeout); window.clearTimeout(this._showTimeout);
this._suggestionDiv.style.display = 'none'; this._suggestionDiv.style.display = "none";
this._isVisible = false; this._isVisible = false;
} }
replaceSelectedText(result, addSpace) { replaceSelectedText(result, addSpace) {
const start = _getSelectionStart(this._sourceInputNode); const start = _getSelectionStart(this._sourceInputNode);
let prefix = ''; let prefix = "";
let suffix = this._sourceInputNode.value.substring(start); let suffix = this._sourceInputNode.value.substring(start);
let middle = this._sourceInputNode.value.substring(0, start); let middle = this._sourceInputNode.value.substring(0, start);
const index = middle.lastIndexOf(' '); const index = middle.lastIndexOf(" ");
if (index !== -1) { if (index !== -1) {
prefix = this._sourceInputNode.value.substring(0, index + 1); prefix = this._sourceInputNode.value.substring(0, index + 1);
middle = this._sourceInputNode.value.substring(index + 1); middle = this._sourceInputNode.value.substring(index + 1);
} }
this._sourceInputNode.value = ( this._sourceInputNode.value =
prefix + result.toString() + ' ' + suffix.trimLeft()); prefix + result.toString() + " " + suffix.trimLeft();
if (!addSpace) { if (!addSpace) {
this._sourceInputNode.value = this._sourceInputNode.value.trim(); this._sourceInputNode.value = this._sourceInputNode.value.trim();
} }
@ -86,7 +90,7 @@ class AutoCompleteControl {
} }
_show() { _show() {
this._suggestionDiv.style.display = 'block'; this._suggestionDiv.style.display = "block";
this._isVisible = true; this._isVisible = true;
} }
@ -101,29 +105,32 @@ class AutoCompleteControl {
_install() { _install() {
if (!this._sourceInputNode) { 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( 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("data-autocomplete", true);
this._sourceInputNode.setAttribute('autocomplete', 'off'); this._sourceInputNode.setAttribute("autocomplete", "off");
this._sourceInputNode.addEventListener( this._sourceInputNode.addEventListener("keydown", (e) =>
'keydown', e => this._evtKeyDown(e)); this._evtKeyDown(e)
this._sourceInputNode.addEventListener( );
'blur', e => this._evtBlur(e)); this._sourceInputNode.addEventListener("blur", (e) =>
this._evtBlur(e)
);
this._suggestionDiv = views.htmlToDom( this._suggestionDiv = views.htmlToDom(
'<div class="autocomplete"><ul></ul></div>'); '<div class="autocomplete"><ul></ul></div>'
this._suggestionList = this._suggestionDiv.querySelector('ul'); );
this._suggestionList = this._suggestionDiv.querySelector("ul");
document.body.appendChild(this._suggestionDiv); document.body.appendChild(this._suggestionDiv);
views.monitorNodeRemoval( views.monitorNodeRemoval(this._sourceInputNode, () => {
this._sourceInputNode, () => { this._uninstall();
this._uninstall(); });
});
} }
_uninstall() { _uninstall() {
@ -174,10 +181,9 @@ class AutoCompleteControl {
func(); func();
} else { } else {
window.clearTimeout(this._showTimeout); window.clearTimeout(this._showTimeout);
this._showTimeout = window.setTimeout( this._showTimeout = window.setTimeout(() => {
() => { this._showOrHide();
this._showOrHide(); }, 250);
}, 250);
} }
} }
@ -196,9 +202,11 @@ class AutoCompleteControl {
} }
_selectPrevious() { _selectPrevious() {
this._select(this._activeResult === -1 ? this._select(
this._results.length - 1 : this._activeResult === -1
this._activeResult - 1); ? this._results.length - 1
: this._activeResult - 1
);
} }
_selectNext() { _selectNext() {
@ -206,15 +214,18 @@ class AutoCompleteControl {
} }
_select(newActiveResult) { _select(newActiveResult) {
this._activeResult = this._activeResult = newActiveResult.between(
newActiveResult.between(0, this._results.length - 1, true) ? 0,
newActiveResult : this._results.length - 1,
-1; true
)
? newActiveResult
: -1;
this._refreshActiveResult(); this._refreshActiveResult();
} }
_updateResults(textToFind) { _updateResults(textToFind) {
this._options.getMatches(textToFind).then(matches => { this._options.getMatches(textToFind).then((matches) => {
const oldResults = this._results.slice(); const oldResults = this._results.slice();
this._results = matches.slice(0, this._options.maxResults); this._results = matches.slice(0, this._options.maxResults);
const oldResultsHash = JSON.stringify(oldResults); const oldResultsHash = JSON.stringify(oldResults);
@ -237,34 +248,30 @@ class AutoCompleteControl {
} }
for (let [resultIndex, resultItem] of this._results.entries()) { for (let [resultIndex, resultItem] of this._results.entries()) {
let resultIndexWorkaround = resultIndex; let resultIndexWorkaround = resultIndex;
const listItem = document.createElement('li'); const listItem = document.createElement("li");
const link = document.createElement('a'); const link = document.createElement("a");
link.innerHTML = resultItem.caption; link.innerHTML = resultItem.caption;
link.setAttribute('href', ''); link.setAttribute("href", "");
link.setAttribute('data-key', resultItem.value); link.setAttribute("data-key", resultItem.value);
link.addEventListener( link.addEventListener("mouseenter", (e) => {
'mouseenter', e.preventDefault();
e => { this._activeResult = resultIndexWorkaround;
e.preventDefault(); this._refreshActiveResult();
this._activeResult = resultIndexWorkaround; });
this._refreshActiveResult(); link.addEventListener("mousedown", (e) => {
}); e.preventDefault();
link.addEventListener( this._activeResult = resultIndexWorkaround;
'mousedown', this._confirm(this._getActiveSuggestion());
e => { this.hide();
e.preventDefault(); });
this._activeResult = resultIndexWorkaround;
this._confirm(this._getActiveSuggestion());
this.hide();
});
listItem.appendChild(link); listItem.appendChild(link);
this._suggestionList.appendChild(listItem); this._suggestionList.appendChild(listItem);
} }
this._refreshActiveResult(); this._refreshActiveResult();
// display the suggestions offscreen to get the height // display the suggestions offscreen to get the height
this._suggestionDiv.style.left = '-9999px'; this._suggestionDiv.style.left = "-9999px";
this._suggestionDiv.style.top = '-9999px'; this._suggestionDiv.style.top = "-9999px";
this._show(); this._show();
const verticalShift = this._options.verticalShift; const verticalShift = this._options.verticalShift;
const inputRect = this._sourceInputNode.getBoundingClientRect(); const inputRect = this._sourceInputNode.getBoundingClientRect();
@ -275,17 +282,23 @@ class AutoCompleteControl {
// choose where to view the suggestions: if there's more space above // choose where to view the suggestions: if there's more space above
// the input - draw the suggestions above it, otherwise below // the input - draw the suggestions above it, otherwise below
const direction = 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 x = inputRect.left - bodyRect.left;
let y = direction === 1 ? let y =
inputRect.bottom - bodyRect.top - verticalShift : direction === 1
inputRect.top - bodyRect.top - listRect.height + verticalShift; ? inputRect.bottom - bodyRect.top - verticalShift
: inputRect.top -
bodyRect.top -
listRect.height +
verticalShift;
// remove offscreen items until whole suggestion list can fit on the // remove offscreen items until whole suggestion list can fit on the
// screen // screen
while ((y < 0 || y + listRect.height > viewPortHeight) && while (
this._suggestionList.childNodes.length) { (y < 0 || y + listRect.height > viewPortHeight) &&
this._suggestionList.childNodes.length
) {
this._suggestionList.removeChild(this._suggestionList.lastChild); this._suggestionList.removeChild(this._suggestionList.lastChild);
const prevHeight = listRect.height; const prevHeight = listRect.height;
listRect = this._suggestionDiv.getBoundingClientRect(); listRect = this._suggestionDiv.getBoundingClientRect();
@ -295,19 +308,19 @@ class AutoCompleteControl {
} }
} }
this._suggestionDiv.style.left = x + 'px'; this._suggestionDiv.style.left = x + "px";
this._suggestionDiv.style.top = y + 'px'; this._suggestionDiv.style.top = y + "px";
} }
_refreshActiveResult() { _refreshActiveResult() {
let activeItem = this._suggestionList.querySelector('li.active'); let activeItem = this._suggestionList.querySelector("li.active");
if (activeItem) { if (activeItem) {
activeItem.classList.remove('active'); activeItem.classList.remove("active");
} }
if (this._activeResult >= 0) { if (this._activeResult >= 0) {
const allItems = this._suggestionList.querySelectorAll('li'); const allItems = this._suggestionList.querySelectorAll("li");
activeItem = allItems[this._activeResult]; 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 api = require("../api.js");
const misc = require('../util/misc.js'); const misc = require("../util/misc.js");
const events = require('../events.js'); const events = require("../events.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const template = views.getTemplate('comment'); const template = views.getTemplate("comment");
const scoreTemplate = views.getTemplate('score'); const scoreTemplate = views.getTemplate("score");
class CommentControl extends events.EventTarget { class CommentControl extends events.EventTarget {
constructor(hostNode, comment, onlyEditing) { constructor(hostNode, comment, onlyEditing) {
@ -16,104 +16,111 @@ class CommentControl extends events.EventTarget {
this._onlyEditing = onlyEditing; this._onlyEditing = onlyEditing;
if (comment) { if (comment) {
comment.addEventListener( comment.addEventListener("change", (e) => this._evtChange(e));
'change', e => this._evtChange(e)); comment.addEventListener("changeScore", (e) =>
comment.addEventListener( this._evtChangeScore(e)
'changeScore', e => this._evtChangeScore(e)); );
} }
const isLoggedIn = comment && api.isLoggedIn(comment.user); const isLoggedIn = comment && api.isLoggedIn(comment.user);
const infix = isLoggedIn ? 'own' : 'any'; const infix = isLoggedIn ? "own" : "any";
views.replaceContent(this._hostNode, template({ views.replaceContent(
comment: comment, this._hostNode,
user: comment ? comment.user : api.user, template({
canViewUsers: api.hasPrivilege('users:view'), comment: comment,
canEditComment: api.hasPrivilege(`comments:edit:${infix}`), user: comment ? comment.user : api.user,
canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`), canViewUsers: api.hasPrivilege("users:view"),
onlyEditing: onlyEditing, canEditComment: api.hasPrivilege(`comments:edit:${infix}`),
})); canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`),
onlyEditing: onlyEditing,
})
);
if (this._editButtonNodes) { if (this._editButtonNodes) {
for (let node of 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) { if (this._deleteButtonNode) {
this._deleteButtonNode.addEventListener( this._deleteButtonNode.addEventListener("click", (e) =>
'click', e => this._evtDeleteClick(e)); this._evtDeleteClick(e)
);
} }
if (this._previewEditingButtonNode) { if (this._previewEditingButtonNode) {
this._previewEditingButtonNode.addEventListener( this._previewEditingButtonNode.addEventListener("click", (e) =>
'click', e => this._evtPreviewEditingClick(e)); this._evtPreviewEditingClick(e)
);
} }
if (this._saveChangesButtonNode) { if (this._saveChangesButtonNode) {
this._saveChangesButtonNode.addEventListener( this._saveChangesButtonNode.addEventListener("click", (e) =>
'click', e => this._evtSaveChangesClick(e)); this._evtSaveChangesClick(e)
);
} }
if (this._cancelEditingButtonNode) { if (this._cancelEditingButtonNode) {
this._cancelEditingButtonNode.addEventListener( this._cancelEditingButtonNode.addEventListener("click", (e) =>
'click', e => this._evtCancelEditingClick(e)); this._evtCancelEditingClick(e)
);
} }
this._installScore(); this._installScore();
if (onlyEditing) { if (onlyEditing) {
this._selectNav('edit'); this._selectNav("edit");
this._selectTab('edit'); this._selectTab("edit");
} else { } else {
this._selectNav('readonly'); this._selectNav("readonly");
this._selectTab('preview'); this._selectTab("preview");
} }
} }
get _formNode() { get _formNode() {
return this._hostNode.querySelector('form'); return this._hostNode.querySelector("form");
} }
get _scoreContainerNode() { get _scoreContainerNode() {
return this._hostNode.querySelector('.score-container'); return this._hostNode.querySelector(".score-container");
} }
get _editButtonNodes() { get _editButtonNodes() {
return this._hostNode.querySelectorAll('li.edit>a, a.edit'); return this._hostNode.querySelectorAll("li.edit>a, a.edit");
} }
get _previewEditingButtonNode() { get _previewEditingButtonNode() {
return this._hostNode.querySelector('li.preview>a'); return this._hostNode.querySelector("li.preview>a");
} }
get _deleteButtonNode() { get _deleteButtonNode() {
return this._hostNode.querySelector('.delete'); return this._hostNode.querySelector(".delete");
} }
get _upvoteButtonNode() { get _upvoteButtonNode() {
return this._hostNode.querySelector('.upvote'); return this._hostNode.querySelector(".upvote");
} }
get _downvoteButtonNode() { get _downvoteButtonNode() {
return this._hostNode.querySelector('.downvote'); return this._hostNode.querySelector(".downvote");
} }
get _saveChangesButtonNode() { get _saveChangesButtonNode() {
return this._hostNode.querySelector('.save-changes'); return this._hostNode.querySelector(".save-changes");
} }
get _cancelEditingButtonNode() { get _cancelEditingButtonNode() {
return this._hostNode.querySelector('.cancel-editing'); return this._hostNode.querySelector(".cancel-editing");
} }
get _textareaNode() { get _textareaNode() {
return this._hostNode.querySelector('.tab.edit textarea'); return this._hostNode.querySelector(".tab.edit textarea");
} }
get _contentNode() { get _contentNode() {
return this._hostNode.querySelector('.tab.preview .comment-content'); return this._hostNode.querySelector(".tab.preview .comment-content");
} }
get _heightKeeperNode() { get _heightKeeperNode() {
return this._hostNode.querySelector('.keep-height'); return this._hostNode.querySelector(".keep-height");
} }
_installScore() { _installScore() {
@ -122,32 +129,35 @@ class CommentControl extends events.EventTarget {
scoreTemplate({ scoreTemplate({
score: this._comment ? this._comment.score : 0, score: this._comment ? this._comment.score : 0,
ownScore: this._comment ? this._comment.ownScore : 0, ownScore: this._comment ? this._comment.ownScore : 0,
canScore: api.hasPrivilege('comments:score'), canScore: api.hasPrivilege("comments:score"),
})); })
);
if (this._upvoteButtonNode) { if (this._upvoteButtonNode) {
this._upvoteButtonNode.addEventListener( this._upvoteButtonNode.addEventListener("click", (e) =>
'click', e => this._evtScoreClick(e, 1)); this._evtScoreClick(e, 1)
);
} }
if (this._downvoteButtonNode) { if (this._downvoteButtonNode) {
this._downvoteButtonNode.addEventListener( this._downvoteButtonNode.addEventListener("click", (e) =>
'click', e => this._evtScoreClick(e, -1)); this._evtScoreClick(e, -1)
);
} }
} }
enterEditMode() { enterEditMode() {
this._selectNav('edit'); this._selectNav("edit");
this._selectTab('edit'); this._selectTab("edit");
} }
exitEditMode() { exitEditMode() {
if (this._onlyEditing) { if (this._onlyEditing) {
this._selectNav('edit'); this._selectNav("edit");
this._selectTab('edit'); this._selectTab("edit");
this._setText(''); this._setText("");
} else { } else {
this._selectNav('readonly'); this._selectNav("readonly");
this._selectTab('preview'); this._selectTab("preview");
this._setText(this._comment.text); this._setText(this._comment.text);
} }
this._forgetHeight(); this._forgetHeight();
@ -173,27 +183,31 @@ class CommentControl extends events.EventTarget {
_evtScoreClick(e, score) { _evtScoreClick(e, score) {
e.preventDefault(); e.preventDefault();
if (!api.hasPrivilege('comments:score')) { if (!api.hasPrivilege("comments:score")) {
return; return;
} }
this.dispatchEvent(new CustomEvent('score', { this.dispatchEvent(
detail: { new CustomEvent("score", {
comment: this._comment, detail: {
score: this._comment.ownScore === score ? 0 : score, comment: this._comment,
}, score: this._comment.ownScore === score ? 0 : score,
})); },
})
);
} }
_evtDeleteClick(e) { _evtDeleteClick(e) {
e.preventDefault(); 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; return;
} }
this.dispatchEvent(new CustomEvent('delete', { this.dispatchEvent(
detail: { new CustomEvent("delete", {
comment: this._comment, detail: {
}, comment: this._comment,
})); },
})
);
} }
_evtChange(e) { _evtChange(e) {
@ -206,21 +220,24 @@ class CommentControl extends events.EventTarget {
_evtPreviewEditingClick(e) { _evtPreviewEditingClick(e) {
e.preventDefault(); e.preventDefault();
this._contentNode.innerHTML = this._contentNode.innerHTML = misc.formatMarkdown(
misc.formatMarkdown(this._textareaNode.value); this._textareaNode.value
this._selectTab('edit'); );
this._selectTab('preview'); this._selectTab("edit");
this._selectTab("preview");
} }
_evtSaveChangesClick(e) { _evtSaveChangesClick(e) {
e.preventDefault(); e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', { this.dispatchEvent(
detail: { new CustomEvent("submit", {
target: this, detail: {
comment: this._comment, target: this,
text: this._textareaNode.value, comment: this._comment,
}, text: this._textareaNode.value,
})); },
})
);
} }
_evtCancelEditingClick(e) { _evtCancelEditingClick(e) {
@ -234,22 +251,22 @@ class CommentControl extends events.EventTarget {
} }
_selectNav(modeName) { _selectNav(modeName) {
for (let node of this._hostNode.querySelectorAll('nav')) { for (let node of this._hostNode.querySelectorAll("nav")) {
node.classList.toggle('active', node.classList.contains(modeName)); node.classList.toggle("active", node.classList.contains(modeName));
} }
} }
_selectTab(tabName) { _selectTab(tabName) {
this._ensureHeight(); this._ensureHeight();
for (let node of this._hostNode.querySelectorAll('.tab, .tabs li')) { for (let node of this._hostNode.querySelectorAll(".tab, .tabs li")) {
node.classList.toggle('active', node.classList.contains(tabName)); node.classList.toggle("active", node.classList.contains(tabName));
} }
} }
_ensureHeight() { _ensureHeight() {
this._heightKeeperNode.style.minHeight = this._heightKeeperNode.style.minHeight =
this._heightKeeperNode.getBoundingClientRect().height + 'px'; this._heightKeeperNode.getBoundingClientRect().height + "px";
} }
_forgetHeight() { _forgetHeight() {

View file

@ -1,10 +1,10 @@
'use strict'; "use strict";
const events = require('../events.js'); const events = require("../events.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const CommentControl = require('../controls/comment_control.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 { class CommentListControl extends events.EventTarget {
constructor(hostNode, comments, reversed) { constructor(hostNode, comments, reversed) {
@ -13,8 +13,8 @@ class CommentListControl extends events.EventTarget {
this._comments = comments; this._comments = comments;
this._commentIdToNode = {}; this._commentIdToNode = {};
comments.addEventListener('add', e => this._evtAdd(e)); comments.addEventListener("add", (e) => this._evtAdd(e));
comments.addEventListener('remove', e => this._evtRemove(e)); comments.addEventListener("remove", (e) => this._evtRemove(e));
views.replaceContent(this._hostNode, template()); views.replaceContent(this._hostNode, template());
@ -28,16 +28,19 @@ class CommentListControl extends events.EventTarget {
} }
get _commentListNode() { get _commentListNode() {
return this._hostNode.querySelector('ul'); return this._hostNode.querySelector("ul");
} }
_installCommentNode(comment) { _installCommentNode(comment) {
const commentListItemNode = document.createElement('li'); const commentListItemNode = document.createElement("li");
const commentControl = new CommentControl( const commentControl = new CommentControl(
commentListItemNode, comment, false); commentListItemNode,
events.proxyEvent(commentControl, this, 'submit'); comment,
events.proxyEvent(commentControl, this, 'score'); false
events.proxyEvent(commentControl, this, 'delete'); );
events.proxyEvent(commentControl, this, "submit");
events.proxyEvent(commentControl, this, "score");
events.proxyEvent(commentControl, this, "delete");
this._commentIdToNode[comment.id] = commentListItemNode; this._commentIdToNode[comment.id] = commentListItemNode;
this._commentListNode.appendChild(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_OPENED = "fa-chevron-down";
const ICON_CLASS_CLOSED = 'fa-chevron-up'; 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 { class ExpanderControl {
constructor(name, title, nodes) { constructor(name, title, nodes) {
this._name = name; this._name = name;
nodes = Array.from(nodes).filter(n => n); nodes = Array.from(nodes).filter((n) => n);
if (!nodes.length) { if (!nodes.length) {
return; return;
} }
const expanderNode = template({title: title}); const expanderNode = template({ title: title });
const toggleLinkNode = expanderNode.querySelector('a'); const toggleLinkNode = expanderNode.querySelector("a");
const toggleIconNode = expanderNode.querySelector('i'); const toggleIconNode = expanderNode.querySelector("i");
const expanderContentNode = expanderNode.querySelector('div'); const expanderContentNode = expanderNode.querySelector("div");
toggleLinkNode.addEventListener('click', e => this._evtToggleClick(e)); toggleLinkNode.addEventListener("click", (e) =>
this._evtToggleClick(e)
);
nodes[0].parentNode.insertBefore(expanderNode, nodes[0]); nodes[0].parentNode.insertBefore(expanderNode, nodes[0]);
@ -32,29 +34,30 @@ class ExpanderControl {
this._toggleIconNode = toggleIconNode; this._toggleIconNode = toggleIconNode;
expanderNode.classList.toggle( expanderNode.classList.toggle(
'collapsed', "collapsed",
this._allStates[this._name] === undefined ? this._allStates[this._name] === undefined
false : ? false
!this._allStates[this._name]); : !this._allStates[this._name]
);
this._syncIcon(); this._syncIcon();
} }
// eslint-disable-next-line accessor-pairs // eslint-disable-next-line accessor-pairs
set title(newTitle) { set title(newTitle) {
if (this._expanderNode) { if (this._expanderNode) {
this._expanderNode this._expanderNode.querySelector(
.querySelector('header span') "header span"
.textContent = newTitle; ).textContent = newTitle;
} }
} }
get _isOpened() { get _isOpened() {
return !this._expanderNode.classList.contains('collapsed'); return !this._expanderNode.classList.contains("collapsed");
} }
get _allStates() { get _allStates() {
try { try {
return JSON.parse(localStorage.getItem('expander')) || {}; return JSON.parse(localStorage.getItem("expander")) || {};
} catch (e) { } catch (e) {
return {}; return {};
} }
@ -63,12 +66,12 @@ class ExpanderControl {
_save() { _save() {
const newStates = Object.assign({}, this._allStates); const newStates = Object.assign({}, this._allStates);
newStates[this._name] = this._isOpened; newStates[this._name] = this._isOpened;
localStorage.setItem('expander', JSON.stringify(newStates)); localStorage.setItem("expander", JSON.stringify(newStates));
} }
_evtToggleClick(e) { _evtToggleClick(e) {
e.preventDefault(); e.preventDefault();
this._expanderNode.classList.toggle('collapsed'); this._expanderNode.classList.toggle("collapsed");
this._save(); this._save();
this._syncIcon(); this._syncIcon();
} }

View file

@ -1,9 +1,9 @@
'use strict'; "use strict";
const events = require('../events.js'); const events = require("../events.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const template = views.getTemplate('file-dropper'); const template = views.getTemplate("file-dropper");
const KEY_RETURN = 13; const KEY_RETURN = 13;
@ -17,37 +17,42 @@ class FileDropperControl extends events.EventTarget {
allowMultiple: options.allowMultiple, allowMultiple: options.allowMultiple,
allowUrls: options.allowUrls, allowUrls: options.allowUrls,
lock: options.lock, lock: options.lock,
id: 'file-' + Math.random().toString(36).substring(7), id: "file-" + Math.random().toString(36).substring(7),
urlPlaceholder: urlPlaceholder:
options.urlPlaceholder || 'Alternatively, paste an URL here.', options.urlPlaceholder || "Alternatively, paste an URL here.",
}); });
this._dropperNode = source.querySelector('.file-dropper'); this._dropperNode = source.querySelector(".file-dropper");
this._urlInputNode = source.querySelector('input[type=text]'); this._urlInputNode = source.querySelector("input[type=text]");
this._urlConfirmButtonNode = source.querySelector('button'); this._urlConfirmButtonNode = source.querySelector("button");
this._fileInputNode = source.querySelector('input[type=file]'); this._fileInputNode = source.querySelector("input[type=file]");
this._fileInputNode.style.display = 'none'; this._fileInputNode.style.display = "none";
this._fileInputNode.multiple = options.allowMultiple || false; this._fileInputNode.multiple = options.allowMultiple || false;
this._counter = 0; this._counter = 0;
this._dropperNode.addEventListener( this._dropperNode.addEventListener("dragenter", (e) =>
'dragenter', e => this._evtDragEnter(e)); this._evtDragEnter(e)
this._dropperNode.addEventListener( );
'dragleave', e => this._evtDragLeave(e)); this._dropperNode.addEventListener("dragleave", (e) =>
this._dropperNode.addEventListener( this._evtDragLeave(e)
'dragover', e => this._evtDragOver(e)); );
this._dropperNode.addEventListener( this._dropperNode.addEventListener("dragover", (e) =>
'drop', e => this._evtDrop(e)); this._evtDragOver(e)
this._fileInputNode.addEventListener( );
'change', e => this._evtFileChange(e)); this._dropperNode.addEventListener("drop", (e) => this._evtDrop(e));
this._fileInputNode.addEventListener("change", (e) =>
this._evtFileChange(e)
);
if (this._urlInputNode) { if (this._urlInputNode) {
this._urlInputNode.addEventListener( this._urlInputNode.addEventListener("keydown", (e) =>
'keydown', e => this._evtUrlInputKeyDown(e)); this._evtUrlInputKeyDown(e)
);
} }
if (this._urlConfirmButtonNode) { if (this._urlConfirmButtonNode) {
this._urlConfirmButtonNode.addEventListener( this._urlConfirmButtonNode.addEventListener("click", (e) =>
'click', e => this._evtUrlConfirmButtonClick(e)); this._evtUrlConfirmButtonClick(e)
);
} }
this._originalHtml = this._dropperNode.innerHTML; this._originalHtml = this._dropperNode.innerHTML;
@ -56,24 +61,27 @@ class FileDropperControl extends events.EventTarget {
reset() { reset() {
this._dropperNode.innerHTML = this._originalHtml; this._dropperNode.innerHTML = this._originalHtml;
this.dispatchEvent(new CustomEvent('reset')); this.dispatchEvent(new CustomEvent("reset"));
} }
_emitFiles(files) { _emitFiles(files) {
files = Array.from(files); files = Array.from(files);
if (this._options.lock) { if (this._options.lock) {
this._dropperNode.innerText = this._dropperNode.innerText = files
files.map(file => file.name).join(', '); .map((file) => file.name)
.join(", ");
} }
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('fileadd', {detail: {files: files}})); new CustomEvent("fileadd", { detail: { files: files } })
);
} }
_emitUrls(urls) { _emitUrls(urls) {
urls = Array.from(urls).map(url => url.trim()); urls = Array.from(urls).map((url) => url.trim());
if (this._options.lock) { if (this._options.lock) {
this._dropperNode.innerText = this._dropperNode.innerText = urls
urls.map(url => url.split(/\//).reverse()[0]).join(', '); .map((url) => url.split(/\//).reverse()[0])
.join(", ");
} }
for (let url of urls) { for (let url of urls) {
if (!url) { if (!url) {
@ -84,18 +92,20 @@ class FileDropperControl extends events.EventTarget {
return; return;
} }
} }
this.dispatchEvent(new CustomEvent('urladd', {detail: {urls: urls}})); this.dispatchEvent(
new CustomEvent("urladd", { detail: { urls: urls } })
);
} }
_evtDragEnter(e) { _evtDragEnter(e) {
this._dropperNode.classList.add('active'); this._dropperNode.classList.add("active");
this._counter++; this._counter++;
} }
_evtDragLeave(e) { _evtDragLeave(e) {
this._counter--; this._counter--;
if (this._counter === 0) { 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) { _evtDrop(e) {
e.preventDefault(); e.preventDefault();
this._dropperNode.classList.remove('active'); this._dropperNode.classList.remove("active");
if (!e.dataTransfer.files.length) { 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) { 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); this._emitFiles(e.dataTransfer.files);
} }
@ -124,16 +134,16 @@ class FileDropperControl extends events.EventTarget {
return; return;
} }
e.preventDefault(); e.preventDefault();
this._dropperNode.classList.remove('active'); this._dropperNode.classList.remove("active");
this._emitUrls(this._urlInputNode.value.split(/[\r\n]/)); this._emitUrls(this._urlInputNode.value.split(/[\r\n]/));
this._urlInputNode.value = ''; this._urlInputNode.value = "";
} }
_evtUrlConfirmButtonClick(e) { _evtUrlConfirmButtonClick(e) {
e.preventDefault(); e.preventDefault();
this._dropperNode.classList.remove('active'); this._dropperNode.classList.remove("active");
this._emitUrls(this._urlInputNode.value.split(/[\r\n]/)); 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 misc = require("../util/misc.js");
const PoolList = require('../models/pool_list.js'); const PoolList = require("../models/pool_list.js");
const AutoCompleteControl = require('./auto_complete_control.js'); const AutoCompleteControl = require("./auto_complete_control.js");
function _poolListToMatches(pools, options) { function _poolListToMatches(pools, options) {
return [...pools].sort((pool1, pool2) => { return [...pools]
return pool2.postCount - pool1.postCount; .sort((pool1, pool2) => {
}).map(pool => { return pool2.postCount - pool1.postCount;
let cssName = misc.makeCssName(pool.category, 'pool'); })
const caption = ( .map((pool) => {
'<span class="' + cssName + '">' let cssName = misc.makeCssName(pool.category, "pool");
+ misc.escapeHtml(pool.names[0] + ' (' + pool.postCount + ')') const caption =
+ '</span>'); '<span class="' +
return { cssName +
caption: caption, '">' +
value: pool, misc.escapeHtml(pool.names[0] + " (" + pool.postCount + ")") +
}; "</span>";
}); return {
caption: caption,
value: pool,
};
});
} }
class PoolAutoCompleteControl extends AutoCompleteControl { class PoolAutoCompleteControl extends AutoCompleteControl {
constructor(input, options) { constructor(input, options) {
const minLengthForPartialSearch = 3; const minLengthForPartialSearch = 3;
options.getMatches = text => { options.getMatches = (text) => {
const term = misc.escapeSearchTerm(text); const term = misc.escapeSearchTerm(text);
const query = ( const query =
text.length < minLengthForPartialSearch (text.length < minLengthForPartialSearch
? term + '*' ? term + "*"
: '*' + term + '*') + ' sort:post-count'; : "*" + term + "*") + " sort:post-count";
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
PoolList.search( PoolList.search(query, 0, this._options.maxResults, [
query, 0, this._options.maxResults, ['id', 'names', 'category', 'postCount', 'version']) "id",
.then( "names",
response => resolve( "category",
_poolListToMatches(response.results, this._options)), "postCount",
reject); "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 api = require("../api.js");
const pools = require('../pools.js'); const pools = require("../pools.js");
const misc = require('../util/misc.js'); const misc = require("../util/misc.js");
const uri = require('../util/uri.js'); const uri = require("../util/uri.js");
const Pool = require('../models/pool.js'); const Pool = require("../models/pool.js");
const settings = require('../models/settings.js'); const settings = require("../models/settings.js");
const events = require('../events.js'); const events = require("../events.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const PoolAutoCompleteControl = require('./pool_auto_complete_control.js'); const PoolAutoCompleteControl = require("./pool_auto_complete_control.js");
const KEY_SPACE = 32; const KEY_SPACE = 32;
const KEY_RETURN = 13; const KEY_RETURN = 13;
const SOURCE_INIT = 'init'; const SOURCE_INIT = "init";
const SOURCE_IMPLICATION = 'implication'; const SOURCE_IMPLICATION = "implication";
const SOURCE_USER_INPUT = 'user-input'; const SOURCE_USER_INPUT = "user-input";
const SOURCE_CLIPBOARD = 'clipboard'; const SOURCE_CLIPBOARD = "clipboard";
const template = views.getTemplate('pool-input'); const template = views.getTemplate("pool-input");
function _fadeOutListItemNodeStatus(listItemNode) { function _fadeOutListItemNodeStatus(listItemNode) {
if (listItemNode.classList.length) { if (listItemNode.classList.length) {
@ -27,8 +27,7 @@ function _fadeOutListItemNodeStatus(listItemNode) {
} }
listItemNode.fadeTimeout = window.setTimeout(() => { listItemNode.fadeTimeout = window.setTimeout(() => {
while (listItemNode.classList.length) { while (listItemNode.classList.length) {
listItemNode.classList.remove( listItemNode.classList.remove(listItemNode.classList.item(0));
listItemNode.classList.item(0));
} }
listItemNode.fadeTimeout = null; listItemNode.fadeTimeout = null;
}, 2500); }, 2500);
@ -45,29 +44,33 @@ class PoolInputControl extends events.EventTarget {
// dom // dom
const editAreaNode = template(); const editAreaNode = template();
this._editAreaNode = editAreaNode; this._editAreaNode = editAreaNode;
this._poolInputNode = editAreaNode.querySelector('input'); this._poolInputNode = editAreaNode.querySelector("input");
this._poolListNode = editAreaNode.querySelector('ul.compact-pools'); this._poolListNode = editAreaNode.querySelector("ul.compact-pools");
this._autoCompleteControl = new PoolAutoCompleteControl( this._autoCompleteControl = new PoolAutoCompleteControl(
this._poolInputNode, { this._poolInputNode,
{
getTextToFind: () => { getTextToFind: () => {
return this._poolInputNode.value; return this._poolInputNode.value;
}, },
confirm: pool => { confirm: (pool) => {
this._poolInputNode.value = ''; this._poolInputNode.value = "";
this.addPool(pool, SOURCE_USER_INPUT); this.addPool(pool, SOURCE_USER_INPUT);
}, },
delete: pool => { delete: (pool) => {
this._poolInputNode.value = ''; this._poolInputNode.value = "";
this.deletePool(pool); this.deletePool(pool);
}, },
verticalShift: -2 verticalShift: -2,
}); }
);
// show // show
this._hostNode.style.display = 'none'; this._hostNode.style.display = "none";
this._hostNode.parentNode.insertBefore( this._hostNode.parentNode.insertBefore(
this._editAreaNode, hostNode.nextSibling); this._editAreaNode,
hostNode.nextSibling
);
// add existing pools // add existing pools
for (let pool of [...this.pools]) { for (let pool of [...this.pools]) {
@ -81,19 +84,21 @@ class PoolInputControl extends events.EventTarget {
return Promise.resolve(); return Promise.resolve();
} }
this.pools.add(pool, false) this.pools.add(pool, false);
const listItemNode = this._createListItemNode(pool); const listItemNode = this._createListItemNode(pool);
if (!pool.category) { if (!pool.category) {
listItemNode.classList.add('new'); listItemNode.classList.add("new");
} }
this._poolListNode.prependChild(listItemNode); this._poolListNode.prependChild(listItemNode);
_fadeOutListItemNodeStatus(listItemNode); _fadeOutListItemNodeStatus(listItemNode);
this.dispatchEvent(new CustomEvent('add', { this.dispatchEvent(
detail: {pool: pool, source: source}, new CustomEvent("add", {
})); detail: { pool: pool, source: source },
this.dispatchEvent(new CustomEvent('change')); })
);
this.dispatchEvent(new CustomEvent("change"));
return Promise.resolve(); return Promise.resolve();
} }
@ -107,52 +112,57 @@ class PoolInputControl extends events.EventTarget {
this._deleteListItemNode(pool); this._deleteListItemNode(pool);
this.dispatchEvent(new CustomEvent('remove', { this.dispatchEvent(
detail: {pool: pool}, new CustomEvent("remove", {
})); detail: { pool: pool },
this.dispatchEvent(new CustomEvent('change')); })
);
this.dispatchEvent(new CustomEvent("change"));
} }
_createListItemNode(pool) { _createListItemNode(pool) {
const className = pool.category ? const className = pool.category
misc.makeCssName(pool.category, 'pool') : ? misc.makeCssName(pool.category, "pool")
null; : null;
const poolLinkNode = document.createElement('a'); const poolLinkNode = document.createElement("a");
if (className) { if (className) {
poolLinkNode.classList.add(className); poolLinkNode.classList.add(className);
} }
poolLinkNode.setAttribute( poolLinkNode.setAttribute(
'href', uri.formatClientLink('pool', pool.names[0])); "href",
uri.formatClientLink("pool", pool.names[0])
);
const poolIconNode = document.createElement('i'); const poolIconNode = document.createElement("i");
poolIconNode.classList.add('fa'); poolIconNode.classList.add("fa");
poolIconNode.classList.add('fa-pool'); poolIconNode.classList.add("fa-pool");
poolLinkNode.appendChild(poolIconNode); poolLinkNode.appendChild(poolIconNode);
const searchLinkNode = document.createElement('a'); const searchLinkNode = document.createElement("a");
if (className) { if (className) {
searchLinkNode.classList.add(className); searchLinkNode.classList.add(className);
} }
searchLinkNode.setAttribute( searchLinkNode.setAttribute(
'href', uri.formatClientLink( "href",
'posts', {query: "pool:" + pool.id})); uri.formatClientLink("posts", { query: "pool:" + pool.id })
searchLinkNode.textContent = pool.names[0] + ' '; );
searchLinkNode.textContent = pool.names[0] + " ";
const usagesNode = document.createElement('span'); const usagesNode = document.createElement("span");
usagesNode.classList.add('pool-usages'); usagesNode.classList.add("pool-usages");
usagesNode.setAttribute('data-pseudo-content', pool.postCount); usagesNode.setAttribute("data-pseudo-content", pool.postCount);
const removalLinkNode = document.createElement('a'); const removalLinkNode = document.createElement("a");
removalLinkNode.classList.add('remove-pool'); removalLinkNode.classList.add("remove-pool");
removalLinkNode.setAttribute('href', ''); removalLinkNode.setAttribute("href", "");
removalLinkNode.setAttribute('data-pseudo-content', '×'); removalLinkNode.setAttribute("data-pseudo-content", "×");
removalLinkNode.addEventListener('click', e => { removalLinkNode.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
this.deletePool(pool); this.deletePool(pool);
}); });
const listItemNode = document.createElement('li'); const listItemNode = document.createElement("li");
listItemNode.appendChild(removalLinkNode); listItemNode.appendChild(removalLinkNode);
listItemNode.appendChild(poolLinkNode); listItemNode.appendChild(poolLinkNode);
listItemNode.appendChild(searchLinkNode); listItemNode.appendChild(searchLinkNode);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,82 +1,101 @@
'use strict'; "use strict";
require('./util/polyfill.js'); require("./util/polyfill.js");
const misc = require('./util/misc.js'); const misc = require("./util/misc.js");
const views = require('./util/views.js'); const views = require("./util/views.js");
const router = require('./router.js'); const router = require("./router.js");
history.scrollRestoration = 'manual'; history.scrollRestoration = "manual";
router.exit( router.exit(null, (ctx, next) => {
null, ctx.state.scrollX = window.scrollX;
(ctx, next) => { ctx.state.scrollY = window.scrollY;
ctx.state.scrollX = window.scrollX; router.replace(router.url, ctx.state);
ctx.state.scrollY = window.scrollY; if (misc.confirmPageExit()) {
router.replace(router.url, ctx.state);
if (misc.confirmPageExit()) {
next();
}
});
const mousetrap = require('mousetrap');
router.enter(
null,
(ctx, next) => {
mousetrap.reset();
next(); 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 { class AbstractList extends events.EventTarget {
constructor() { constructor() {
@ -13,13 +13,15 @@ class AbstractList extends events.EventTarget {
for (let item of response) { for (let item of response) {
const addedItem = this._itemClass.fromResponse(item); const addedItem = this._itemClass.fromResponse(item);
if (addedItem.addEventListener) { if (addedItem.addEventListener) {
addedItem.addEventListener('delete', e => { addedItem.addEventListener("delete", (e) => {
ret.remove(addedItem); ret.remove(addedItem);
}); });
addedItem.addEventListener('change', e => { addedItem.addEventListener("change", (e) => {
ret.dispatchEvent(new CustomEvent('change', { ret.dispatchEvent(
detail: e.detail, new CustomEvent("change", {
})); detail: e.detail,
})
);
}); });
} }
ret._list.push(addedItem); ret._list.push(addedItem);
@ -29,28 +31,32 @@ class AbstractList extends events.EventTarget {
sync(plainList) { sync(plainList) {
this.clear(); this.clear();
for (let item of (plainList || [])) { for (let item of plainList || []) {
this.add(this.constructor._itemClass.fromResponse(item)); this.add(this.constructor._itemClass.fromResponse(item));
} }
} }
add(item) { add(item) {
if (item.addEventListener) { if (item.addEventListener) {
item.addEventListener('delete', e => { item.addEventListener("delete", (e) => {
this.remove(item); this.remove(item);
}); });
item.addEventListener('change', e => { item.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent('change', { this.dispatchEvent(
detail: e.detail, new CustomEvent("change", {
})); detail: e.detail,
})
);
}); });
} }
this._list.push(item); this._list.push(item);
const detail = {}; const detail = {};
detail[this.constructor._itemName] = item; detail[this.constructor._itemName] = item;
this.dispatchEvent(new CustomEvent('add', { this.dispatchEvent(
detail: detail, new CustomEvent("add", {
})); detail: detail,
})
);
} }
clear() { clear() {
@ -67,9 +73,11 @@ class AbstractList extends events.EventTarget {
this._list.splice(index, 1); this._list.splice(index, 1);
const detail = {}; const detail = {};
detail[this.constructor._itemName] = itemToRemove; detail[this.constructor._itemName] = itemToRemove;
this.dispatchEvent(new CustomEvent('remove', { this.dispatchEvent(
detail: detail, new CustomEvent("remove", {
})); detail: detail,
})
);
return; return;
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,12 +1,11 @@
'use strict'; "use strict";
const AbstractList = require('./abstract_list.js'); const AbstractList = require("./abstract_list.js");
const Note = require('./note.js'); const Note = require("./note.js");
class NoteList extends AbstractList { class NoteList extends AbstractList {}
}
NoteList._itemClass = Note; NoteList._itemClass = Note;
NoteList._itemName = 'note'; NoteList._itemName = "note";
module.exports = NoteList; 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 { class Point extends events.EventTarget {
constructor(x, y) { constructor(x, y) {
@ -19,12 +19,16 @@ class Point extends events.EventTarget {
set x(value) { set x(value) {
this._x = value; this._x = value;
this.dispatchEvent(new CustomEvent('change', {detail: {point: this}})); this.dispatchEvent(
new CustomEvent("change", { detail: { point: this } })
);
} }
set y(value) { set y(value) {
this._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 AbstractList = require("./abstract_list.js");
const Point = require('./point.js'); const Point = require("./point.js");
class PointList extends AbstractList { class PointList extends AbstractList {
get firstPoint() { get firstPoint() {
@ -18,6 +18,6 @@ class PointList extends AbstractList {
} }
PointList._itemClass = Point; PointList._itemClass = Point;
PointList._itemName = 'point'; PointList._itemName = "point";
module.exports = PointList; module.exports = PointList;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,35 +1,37 @@
'use strict'; "use strict";
const settings = require('../models/settings.js'); const settings = require("../models/settings.js");
const api = require('../api.js'); const api = require("../api.js");
const uri = require('../util/uri.js'); const uri = require("../util/uri.js");
const AbstractList = require('./abstract_list.js'); const AbstractList = require("./abstract_list.js");
const Post = require('./post.js'); const Post = require("./post.js");
class PostList extends AbstractList { class PostList extends AbstractList {
static getAround(id, searchQuery) { static getAround(id, searchQuery) {
return api.get( return api.get(
uri.formatApiLink( uri.formatApiLink("post", id, "around", {
'post', id, 'around', { query: PostList._decorateSearchQuery(searchQuery || ""),
query: PostList._decorateSearchQuery(searchQuery || ''), fields: "id",
fields: 'id', })
})); );
} }
static search(text, offset, limit, fields) { static search(text, offset, limit, fields) {
return api.get( return api
uri.formatApiLink( .get(
'posts', { uri.formatApiLink("posts", {
query: PostList._decorateSearchQuery(text || ''), query: PostList._decorateSearchQuery(text || ""),
offset: offset, offset: offset,
limit: limit, limit: limit,
fields: fields.join(','), fields: fields.join(","),
})) })
.then(response => { )
return Promise.resolve(Object.assign( .then((response) => {
{}, return Promise.resolve(
response, Object.assign({}, response, {
{results: PostList.fromResponse(response.results)})); results: PostList.fromResponse(response.results),
})
);
}); });
} }
@ -43,7 +45,7 @@ class PostList extends AbstractList {
} }
} }
if (disabledSafety.length) { if (disabledSafety.length) {
text = `-rating:${disabledSafety.join(',')} ${text}`; text = `-rating:${disabledSafety.join(",")} ${text}`;
} }
} }
return text.trim(); return text.trim();
@ -63,7 +65,7 @@ class PostList extends AbstractList {
return; return;
} }
let post = Post.fromResponse({id: id}); let post = Post.fromResponse({ id: id });
this.add(post); this.add(post);
} }
@ -77,6 +79,6 @@ class PostList extends AbstractList {
} }
PostList._itemClass = Post; PostList._itemClass = Post;
PostList._itemName = 'post'; PostList._itemName = "post";
module.exports = PostList; 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 = { const defaultSettings = {
listPosts: { listPosts: {
@ -12,7 +12,7 @@ const defaultSettings = {
endlessScroll: false, endlessScroll: false,
keyboardShortcuts: true, keyboardShortcuts: true,
transparencyGrid: true, transparencyGrid: true,
fitMode: 'fit-both', fitMode: "fit-both",
tagSuggestions: true, tagSuggestions: true,
autoplayVideos: false, autoplayVideos: false,
postsPerPage: 42, postsPerPage: 42,
@ -28,7 +28,7 @@ class Settings extends events.EventTarget {
_getFromLocalStorage() { _getFromLocalStorage() {
let ret = Object.assign({}, defaultSettings); let ret = Object.assign({}, defaultSettings);
try { try {
Object.assign(ret, JSON.parse(localStorage.getItem('settings'))); Object.assign(ret, JSON.parse(localStorage.getItem("settings")));
} catch (e) { } catch (e) {
// continue regardless of error // continue regardless of error
} }
@ -37,14 +37,16 @@ class Settings extends events.EventTarget {
save(newSettings, silent) { save(newSettings, silent) {
newSettings = Object.assign(this.cache, newSettings); newSettings = Object.assign(this.cache, newSettings);
localStorage.setItem('settings', JSON.stringify(newSettings)); localStorage.setItem("settings", JSON.stringify(newSettings));
this.cache = this._getFromLocalStorage(); this.cache = this._getFromLocalStorage();
if (silent !== true) { if (silent !== true) {
this.dispatchEvent(new CustomEvent('change', { this.dispatchEvent(
detail: { new CustomEvent("change", {
settings: this.cache, detail: {
}, settings: this.cache,
})); },
})
);
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,22 +1,23 @@
'use strict'; "use strict";
const misc = require('./util/misc.js'); const misc = require("./util/misc.js");
const TagCategoryList = require('./models/tag_category_list.js'); const TagCategoryList = require("./models/tag_category_list.js");
let _stylesheet = null; let _stylesheet = null;
function refreshCategoryColorMap() { function refreshCategoryColorMap() {
return TagCategoryList.get().then(response => { return TagCategoryList.get().then((response) => {
if (_stylesheet) { if (_stylesheet) {
document.head.removeChild(_stylesheet); document.head.removeChild(_stylesheet);
} }
_stylesheet = document.createElement('style'); _stylesheet = document.createElement("style");
document.head.appendChild(_stylesheet); document.head.appendChild(_stylesheet);
for (let category of response.results) { for (let category of response.results) {
const ruleName = misc.makeCssName(category.name, 'tag'); const ruleName = misc.makeCssName(category.name, "tag");
_stylesheet.sheet.insertRule( _stylesheet.sheet.insertRule(
`.${ruleName} { color: ${category.color} }`, `.${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 mousetrap = require("mousetrap");
const settings = require('../models/settings.js'); const settings = require("../models/settings.js");
let paused = false; let paused = false;
const _originalStopCallback = mousetrap.prototype.stopCallback; const _originalStopCallback = mousetrap.prototype.stopCallback;
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
mousetrap.prototype.stopCallback = function(...args) { mousetrap.prototype.stopCallback = function (...args) {
var self = this; var self = this;
if (paused) { if (paused) {
return true; return true;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +1,16 @@
'use strict'; "use strict";
const misc = require('./misc.js'); const misc = require("./misc.js");
const keyboard = require('../util/keyboard.js'); const keyboard = require("../util/keyboard.js");
const views = require('./views.js'); const views = require("./views.js");
function searchInputNodeFocusHelper(inputNode) { function searchInputNodeFocusHelper(inputNode) {
keyboard.bind('q', () => { keyboard.bind("q", () => {
inputNode.focus(); inputNode.focus();
inputNode.setSelectionRange( 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 = { const direction = {
NONE: null, NONE: null,
LEFT: 'left', LEFT: "left",
RIGHT: 'right', RIGHT: "right",
DOWN: 'down', DOWN: "down",
UP: 'up' UP: "up",
}; };
function handleTouchStart(handler, evt) { function handleTouchStart(handler, evt) {
@ -37,20 +37,20 @@ function handleTouchMove(handler, evt) {
function handleTouchEnd(handler) { function handleTouchEnd(handler) {
switch (handler._direction) { switch (handler._direction) {
case direction.NONE: case direction.NONE:
return; return;
case direction.LEFT: case direction.LEFT:
handler._swipeLeftTask(); handler._swipeLeftTask();
break; break;
case direction.RIGHT: case direction.RIGHT:
handler._swipeRightTask(); handler._swipeRightTask();
break; break;
case direction.DOWN: case direction.DOWN:
handler._swipeDownTask(); handler._swipeDownTask();
break; break;
case direction.UP: case direction.UP:
handler._swipeUpTask(); handler._swipeUpTask();
// no default // no default
} }
handler._xStart = null; handler._xStart = null;
@ -58,11 +58,13 @@ function handleTouchEnd(handler) {
} }
class Touch { class Touch {
constructor(target, constructor(
target,
swipeLeft = () => {}, swipeLeft = () => {},
swipeRight = () => {}, swipeRight = () => {},
swipeUp = () => {}, swipeUp = () => {},
swipeDown = () => {}) { swipeDown = () => {}
) {
this._target = target; this._target = target;
this._swipeLeftTask = swipeLeft; this._swipeLeftTask = swipeLeft;
@ -74,18 +76,15 @@ class Touch {
this._yStart = null; this._yStart = null;
this._direction = direction.NONE; this._direction = direction.NONE;
this._target.addEventListener('touchstart', this._target.addEventListener("touchstart", (evt) => {
evt => { handleTouchStart(this, evt);
handleTouchStart(this, evt); });
}); this._target.addEventListener("touchmove", (evt) => {
this._target.addEventListener('touchmove', handleTouchMove(this, evt);
evt => { });
handleTouchMove(this, evt); this._target.addEventListener("touchend", () => {
}); handleTouchEnd(this);
this._target.addEventListener('touchend', });
() => {
handleTouchEnd(this);
});
} }
} }

View file

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

View file

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

View file

@ -1,10 +1,10 @@
'use strict'; "use strict";
const events = require('../events.js'); const events = require("../events.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const CommentListControl = require('../controls/comment_list_control.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 { class CommentsPageView extends events.EventTarget {
constructor(ctx) { constructor(ctx) {
@ -16,12 +16,14 @@ class CommentsPageView extends events.EventTarget {
for (let post of ctx.response.results) { for (let post of ctx.response.results) {
const commentListControl = new CommentListControl( const commentListControl = new CommentListControl(
sourceNode.querySelector( sourceNode.querySelector(
`.comments-container[data-for="${post.id}"]`), `.comments-container[data-for="${post.id}"]`
),
post.comments, post.comments,
true); true
events.proxyEvent(commentListControl, this, 'submit'); );
events.proxyEvent(commentListControl, this, 'score'); events.proxyEvent(commentListControl, this, "submit");
events.proxyEvent(commentListControl, this, 'delete'); events.proxyEvent(commentListControl, this, "score");
events.proxyEvent(commentListControl, this, "delete");
} }
views.replaceContent(this._hostNode, sourceNode); 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 = () => { const template = () => {
return views.htmlToDom( return views.htmlToDom(
'<div class="wrapper"><div class="messages"></div></div>'); '<div class="wrapper"><div class="messages"></div></div>'
);
}; };
class EmptyView { class EmptyView {
constructor() { constructor() {
this._hostNode = document.getElementById('content-holder'); this._hostNode = document.getElementById("content-holder");
views.replaceContent(this._hostNode, template()); views.replaceContent(this._hostNode, template());
views.syncScrollPosition(); views.syncScrollPosition();
} }

View file

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

View file

@ -1,73 +1,81 @@
'use strict'; "use strict";
const api = require('../api.js'); const api = require("../api.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const template = views.getTemplate('help'); const template = views.getTemplate("help");
const sectionTemplates = { const sectionTemplates = {
'about': views.getTemplate('help-about'), about: views.getTemplate("help-about"),
'keyboard': views.getTemplate('help-keyboard'), keyboard: views.getTemplate("help-keyboard"),
'search': views.getTemplate('help-search'), search: views.getTemplate("help-search"),
'comments': views.getTemplate('help-comments'), comments: views.getTemplate("help-comments"),
'tos': views.getTemplate('help-tos'), tos: views.getTemplate("help-tos"),
}; };
const subsectionTemplates = { const subsectionTemplates = {
'search': { search: {
'default': views.getTemplate('help-search-general'), default: views.getTemplate("help-search-general"),
'posts': views.getTemplate('help-search-posts'), posts: views.getTemplate("help-search-posts"),
'users': views.getTemplate('help-search-users'), users: views.getTemplate("help-search-users"),
'tags': views.getTemplate('help-search-tags'), tags: views.getTemplate("help-search-tags"),
'pools': views.getTemplate('help-search-pools'), pools: views.getTemplate("help-search-pools"),
}, },
}; };
class HelpView { class HelpView {
constructor(section, subsection) { constructor(section, subsection) {
this._hostNode = document.getElementById('content-holder'); this._hostNode = document.getElementById("content-holder");
const sourceNode = template(); const sourceNode = template();
const ctx = { const ctx = {
name: api.getName(), name: api.getName(),
}; };
section = section || 'about'; section = section || "about";
if (section in sectionTemplates) { if (section in sectionTemplates) {
views.replaceContent( views.replaceContent(
sourceNode.querySelector('.content'), sourceNode.querySelector(".content"),
sectionTemplates[section](ctx)); sectionTemplates[section](ctx)
);
} }
subsection = subsection || 'default'; subsection = subsection || "default";
if (section in subsectionTemplates && if (
subsection in subsectionTemplates[section]) { section in subsectionTemplates &&
subsection in subsectionTemplates[section]
) {
views.replaceContent( views.replaceContent(
sourceNode.querySelector('.subcontent'), sourceNode.querySelector(".subcontent"),
subsectionTemplates[section][subsection](ctx)); subsectionTemplates[section][subsection](ctx)
);
} }
views.replaceContent(this._hostNode, sourceNode); views.replaceContent(this._hostNode, sourceNode);
for (let itemNode of for (let itemNode of sourceNode.querySelectorAll(
sourceNode.querySelectorAll('.primary [data-name]')) { ".primary [data-name]"
)) {
itemNode.classList.toggle( itemNode.classList.toggle(
'active', "active",
itemNode.getAttribute('data-name') === section); itemNode.getAttribute("data-name") === section
if (itemNode.getAttribute('data-name') === section) { );
if (itemNode.getAttribute("data-name") === section) {
itemNode.parentNode.scrollLeft = itemNode.parentNode.scrollLeft =
itemNode.getBoundingClientRect().left - itemNode.getBoundingClientRect().left -
itemNode.parentNode.getBoundingClientRect().left itemNode.parentNode.getBoundingClientRect().left;
} }
} }
for (let itemNode of for (let itemNode of sourceNode.querySelectorAll(
sourceNode.querySelectorAll('.secondary [data-name]')) { ".secondary [data-name]"
)) {
itemNode.classList.toggle( itemNode.classList.toggle(
'active', "active",
itemNode.getAttribute('data-name') === subsection); itemNode.getAttribute("data-name") === subsection
if (itemNode.getAttribute('data-name') === subsection) { );
if (itemNode.getAttribute("data-name") === subsection) {
itemNode.parentNode.scrollLeft = itemNode.parentNode.scrollLeft =
itemNode.getBoundingClientRect().left - 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 router = require("../router.js");
const uri = require('../util/uri.js'); const uri = require("../util/uri.js");
const misc = require('../util/misc.js'); const misc = require("../util/misc.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const PostContentControl = require('../controls/post_content_control.js'); const PostContentControl = require("../controls/post_content_control.js");
const PostNotesOverlayControl const PostNotesOverlayControl = require("../controls/post_notes_overlay_control.js");
= require('../controls/post_notes_overlay_control.js'); const TagAutoCompleteControl = require("../controls/tag_auto_complete_control.js");
const TagAutoCompleteControl =
require('../controls/tag_auto_complete_control.js');
const template = views.getTemplate('home'); const template = views.getTemplate("home");
const footerTemplate = views.getTemplate('home-footer'); const footerTemplate = views.getTemplate("home-footer");
const featuredPostTemplate = views.getTemplate('home-featured-post'); const featuredPostTemplate = views.getTemplate("home-featured-post");
class HomeView { class HomeView {
constructor(ctx) { constructor(ctx) {
this._hostNode = document.getElementById('content-holder'); this._hostNode = document.getElementById("content-holder");
this._ctx = ctx; this._ctx = ctx;
const sourceNode = template(ctx); const sourceNode = template(ctx);
@ -27,11 +25,16 @@ class HomeView {
this._autoCompleteControl = new TagAutoCompleteControl( this._autoCompleteControl = new TagAutoCompleteControl(
this._searchInputNode, this._searchInputNode,
{ {
confirm: tag => this._autoCompleteControl.replaceSelectedText( confirm: (tag) =>
misc.escapeSearchTerm(tag.names[0]), true), this._autoCompleteControl.replaceSelectedText(
}); misc.escapeSearchTerm(tag.names[0]),
this._formNode.addEventListener( true
'submit', e => this._evtFormSubmit(e)); ),
}
);
this._formNode.addEventListener("submit", (e) =>
this._evtFormSubmit(e)
);
} }
} }
@ -46,59 +49,67 @@ class HomeView {
setStats(stats) { setStats(stats) {
views.replaceContent( views.replaceContent(
this._footerContainerNode, this._footerContainerNode,
footerTemplate(Object.assign({}, stats, this._ctx))); footerTemplate(Object.assign({}, stats, this._ctx))
);
} }
setFeaturedPost(postInfo) { setFeaturedPost(postInfo) {
views.replaceContent( views.replaceContent(
this._postInfoContainerNode, featuredPostTemplate(postInfo)); this._postInfoContainerNode,
featuredPostTemplate(postInfo)
);
if (this._postContainerNode && postInfo.featuredPost) { if (this._postContainerNode && postInfo.featuredPost) {
this._postContentControl = new PostContentControl( this._postContentControl = new PostContentControl(
this._postContainerNode, this._postContainerNode,
postInfo.featuredPost, postInfo.featuredPost,
() => { () => {
return [ return [window.innerWidth * 0.8, window.innerHeight * 0.7];
window.innerWidth * 0.8,
window.innerHeight * 0.7,
];
}, },
'fit-both'); "fit-both"
);
this._postNotesOverlay = new PostNotesOverlayControl( this._postNotesOverlay = new PostNotesOverlayControl(
this._postContainerNode.querySelector('.post-overlay'), this._postContainerNode.querySelector(".post-overlay"),
postInfo.featuredPost); postInfo.featuredPost
);
if (postInfo.featuredPost.type === 'video' if (
|| postInfo.featuredPost.type === 'flash') { postInfo.featuredPost.type === "video" ||
postInfo.featuredPost.type === "flash"
) {
this._postContentControl.disableOverlay(); this._postContentControl.disableOverlay();
} }
} }
} }
get _footerContainerNode() { get _footerContainerNode() {
return this._hostNode.querySelector('.footer-container'); return this._hostNode.querySelector(".footer-container");
} }
get _postInfoContainerNode() { get _postInfoContainerNode() {
return this._hostNode.querySelector('.post-info-container'); return this._hostNode.querySelector(".post-info-container");
} }
get _postContainerNode() { get _postContainerNode() {
return this._hostNode.querySelector('.post-container'); return this._hostNode.querySelector(".post-container");
} }
get _formNode() { get _formNode() {
return this._hostNode.querySelector('form'); return this._hostNode.querySelector("form");
} }
get _searchInputNode() { get _searchInputNode() {
return this._formNode.querySelector('input[name=search-text]'); return this._formNode.querySelector("input[name=search-text]");
} }
_evtFormSubmit(e) { _evtFormSubmit(e) {
e.preventDefault(); e.preventDefault();
this._searchInputNode.blur(); 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 events = require("../events.js");
const api = require('../api.js'); const api = require("../api.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const template = views.getTemplate('login'); const template = views.getTemplate("login");
class LoginView extends events.EventTarget { class LoginView extends events.EventTarget {
constructor() { constructor() {
super(); super();
this._hostNode = document.getElementById('content-holder'); this._hostNode = document.getElementById("content-holder");
views.replaceContent(this._hostNode, template({ views.replaceContent(
userNamePattern: api.getUserNameRegex(), this._hostNode,
passwordPattern: api.getPasswordRegex(), template({
canSendMails: api.canSendMails(), userNamePattern: api.getUserNameRegex(),
})); passwordPattern: api.getPasswordRegex(),
canSendMails: api.canSendMails(),
})
);
views.syncScrollPosition(); views.syncScrollPosition();
views.decorateValidator(this._formNode); views.decorateValidator(this._formNode);
this._userNameInputNode.setAttribute('pattern', api.getUserNameRegex()); this._userNameInputNode.setAttribute(
this._passwordInputNode.setAttribute('pattern', api.getPasswordRegex()); "pattern",
this._formNode.addEventListener('submit', e => { api.getUserNameRegex()
);
this._passwordInputNode.setAttribute(
"pattern",
api.getPasswordRegex()
);
this._formNode.addEventListener("submit", (e) => {
e.preventDefault(); e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', { this.dispatchEvent(
detail: { new CustomEvent("submit", {
name: this._userNameInputNode.value, detail: {
password: this._passwordInputNode.value, name: this._userNameInputNode.value,
remember: this._rememberInputNode.checked, password: this._passwordInputNode.value,
}, remember: this._rememberInputNode.checked,
})); },
})
);
}); });
} }
get _formNode() { get _formNode() {
return this._hostNode.querySelector('form'); return this._hostNode.querySelector("form");
} }
get _userNameInputNode() { get _userNameInputNode() {
return this._formNode.querySelector('[name=name]'); return this._formNode.querySelector("[name=name]");
} }
get _passwordInputNode() { get _passwordInputNode() {
return this._formNode.querySelector('[name=password]'); return this._formNode.querySelector("[name=password]");
} }
get _rememberInputNode() { get _rememberInputNode() {
return this._formNode.querySelector('[name=remember-user]'); return this._formNode.querySelector("[name=remember-user]");
} }
disableForm() { disableForm() {

View file

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

View file

@ -1,30 +1,35 @@
'use strict'; "use strict";
const events = require('../events.js'); const events = require("../events.js");
const api = require('../api.js'); const api = require("../api.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const template = views.getTemplate('password-reset'); const template = views.getTemplate("password-reset");
class PasswordResetView extends events.EventTarget { class PasswordResetView extends events.EventTarget {
constructor() { constructor() {
super(); super();
this._hostNode = document.getElementById('content-holder'); this._hostNode = document.getElementById("content-holder");
views.replaceContent(this._hostNode, template({ views.replaceContent(
canSendMails: api.canSendMails(), this._hostNode,
contactEmail: api.getContactEmail(), template({
})); canSendMails: api.canSendMails(),
contactEmail: api.getContactEmail(),
})
);
views.syncScrollPosition(); views.syncScrollPosition();
views.decorateValidator(this._formNode); views.decorateValidator(this._formNode);
this._formNode.addEventListener('submit', e => { this._formNode.addEventListener("submit", (e) => {
e.preventDefault(); e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', { this.dispatchEvent(
detail: { new CustomEvent("submit", {
userNameOrEmail: this._userNameOrEmailFieldNode.value, detail: {
}, userNameOrEmail: this._userNameOrEmailFieldNode.value,
})); },
})
);
}); });
} }
@ -49,11 +54,11 @@ class PasswordResetView extends events.EventTarget {
} }
get _formNode() { get _formNode() {
return this._hostNode.querySelector('form'); return this._hostNode.querySelector("form");
} }
get _userNameOrEmailFieldNode() { 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 events = require("../events.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const PoolCategory = require('../models/pool_category.js'); const PoolCategory = require("../models/pool_category.js");
const template = views.getTemplate('pool-categories'); const template = views.getTemplate("pool-categories");
const rowTemplate = views.getTemplate('pool-category-row'); const rowTemplate = views.getTemplate("pool-category-row");
class PoolCategoriesView extends events.EventTarget { class PoolCategoriesView extends events.EventTarget {
constructor(ctx) { constructor(ctx) {
super(); super();
this._ctx = ctx; this._ctx = ctx;
this._hostNode = document.getElementById('content-holder'); this._hostNode = document.getElementById("content-holder");
views.replaceContent(this._hostNode, template(ctx)); views.replaceContent(this._hostNode, template(ctx));
views.syncScrollPosition(); views.syncScrollPosition();
@ -31,18 +31,22 @@ class PoolCategoriesView extends events.EventTarget {
} }
if (this._addLinkNode) { if (this._addLinkNode) {
this._addLinkNode.addEventListener( this._addLinkNode.addEventListener("click", (e) =>
'click', e => this._evtAddButtonClick(e)); this._evtAddButtonClick(e)
);
} }
ctx.poolCategories.addEventListener( ctx.poolCategories.addEventListener("add", (e) =>
'add', e => this._evtPoolCategoryAdded(e)); this._evtPoolCategoryAdded(e)
);
ctx.poolCategories.addEventListener( ctx.poolCategories.addEventListener("remove", (e) =>
'remove', e => this._evtPoolCategoryDeleted(e)); this._evtPoolCategoryDeleted(e)
);
this._formNode.addEventListener( this._formNode.addEventListener("submit", (e) =>
'submit', e => this._evtSaveButtonClick(e, ctx)); this._evtSaveButtonClick(e, ctx)
);
} }
enableForm() { enableForm() {
@ -66,44 +70,48 @@ class PoolCategoriesView extends events.EventTarget {
} }
get _formNode() { get _formNode() {
return this._hostNode.querySelector('form'); return this._hostNode.querySelector("form");
} }
get _tableBodyNode() { get _tableBodyNode() {
return this._hostNode.querySelector('tbody'); return this._hostNode.querySelector("tbody");
} }
get _addLinkNode() { get _addLinkNode() {
return this._hostNode.querySelector('a.add'); return this._hostNode.querySelector("a.add");
} }
_addPoolCategoryRowNode(poolCategory) { _addPoolCategoryRowNode(poolCategory) {
const rowNode = rowTemplate( const rowNode = rowTemplate(
Object.assign( Object.assign({}, this._ctx, { poolCategory: poolCategory })
{}, this._ctx, {poolCategory: poolCategory})); );
const nameInput = rowNode.querySelector('.name input'); const nameInput = rowNode.querySelector(".name input");
if (nameInput) { if (nameInput) {
nameInput.addEventListener( nameInput.addEventListener("change", (e) =>
'change', e => this._evtNameChange(e, rowNode)); this._evtNameChange(e, rowNode)
);
} }
const colorInput = rowNode.querySelector('.color input'); const colorInput = rowNode.querySelector(".color input");
if (colorInput) { if (colorInput) {
colorInput.addEventListener( colorInput.addEventListener("change", (e) =>
'change', e => this._evtColorChange(e, rowNode)); this._evtColorChange(e, rowNode)
);
} }
const removeLinkNode = rowNode.querySelector('.remove a'); const removeLinkNode = rowNode.querySelector(".remove a");
if (removeLinkNode) { if (removeLinkNode) {
removeLinkNode.addEventListener( removeLinkNode.addEventListener("click", (e) =>
'click', e => this._evtDeleteButtonClick(e, rowNode)); this._evtDeleteButtonClick(e, rowNode)
);
} }
const defaultLinkNode = rowNode.querySelector('.set-default a'); const defaultLinkNode = rowNode.querySelector(".set-default a");
if (defaultLinkNode) { if (defaultLinkNode) {
defaultLinkNode.addEventListener( defaultLinkNode.addEventListener("click", (e) =>
'click', e => this._evtSetDefaultButtonClick(e, rowNode)); this._evtSetDefaultButtonClick(e, rowNode)
);
} }
this._tableBodyNode.appendChild(rowNode); this._tableBodyNode.appendChild(rowNode);
@ -141,7 +149,7 @@ class PoolCategoriesView extends events.EventTarget {
_evtDeleteButtonClick(e, rowNode, link) { _evtDeleteButtonClick(e, rowNode, link) {
e.preventDefault(); e.preventDefault();
if (e.target.classList.contains('inactive')) { if (e.target.classList.contains("inactive")) {
return; return;
} }
this._ctx.poolCategories.remove(rowNode._poolCategory); this._ctx.poolCategories.remove(rowNode._poolCategory);
@ -150,16 +158,16 @@ class PoolCategoriesView extends events.EventTarget {
_evtSetDefaultButtonClick(e, rowNode) { _evtSetDefaultButtonClick(e, rowNode) {
e.preventDefault(); e.preventDefault();
this._ctx.poolCategories.defaultCategory = rowNode._poolCategory; this._ctx.poolCategories.defaultCategory = rowNode._poolCategory;
const oldRowNode = rowNode.parentNode.querySelector('tr.default'); const oldRowNode = rowNode.parentNode.querySelector("tr.default");
if (oldRowNode) { if (oldRowNode) {
oldRowNode.classList.remove('default'); oldRowNode.classList.remove("default");
} }
rowNode.classList.add('default'); rowNode.classList.add("default");
} }
_evtSaveButtonClick(e, ctx) { _evtSaveButtonClick(e, ctx) {
e.preventDefault(); 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 events = require("../events.js");
const api = require('../api.js'); const api = require("../api.js");
const misc = require('../util/misc.js'); const misc = require("../util/misc.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const Pool = require('../models/pool.js') const Pool = require("../models/pool.js");
const template = views.getTemplate('pool-create'); const template = views.getTemplate("pool-create");
class PoolCreateView extends events.EventTarget { class PoolCreateView extends events.EventTarget {
constructor(ctx) { constructor(ctx) {
super(); super();
this._hostNode = document.getElementById('content-holder'); this._hostNode = document.getElementById("content-holder");
views.replaceContent(this._hostNode, template(ctx)); views.replaceContent(this._hostNode, template(ctx));
views.decorateValidator(this._formNode); views.decorateValidator(this._formNode);
if (this._namesFieldNode) { if (this._namesFieldNode) {
this._namesFieldNode.addEventListener( this._namesFieldNode.addEventListener("input", (e) =>
'input', e => this._evtNameInput(e)); this._evtNameInput(e)
);
} }
if (this._postsFieldNode) { if (this._postsFieldNode) {
this._postsFieldNode.addEventListener( this._postsFieldNode.addEventListener("input", (e) =>
'input', e => this._evtPostsInput(e)); this._evtPostsInput(e)
);
} }
for (let node of this._formNode.querySelectorAll( for (let node of this._formNode.querySelectorAll(
'input, select, textarea, posts')) { "input, select, textarea, posts"
node.addEventListener( )) {
'change', e => { node.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent('change')); this.dispatchEvent(new CustomEvent("change"));
}); });
} }
this._formNode.addEventListener('submit', e => this._evtSubmit(e)); this._formNode.addEventListener("submit", (e) => this._evtSubmit(e));
} }
clearMessages() { clearMessages() {
@ -64,19 +66,21 @@ class PoolCreateView extends events.EventTarget {
if (!list.length) { if (!list.length) {
this._namesFieldNode.setCustomValidity( this._namesFieldNode.setCustomValidity(
'Pools must have at least one name.'); "Pools must have at least one name."
);
return; return;
} }
for (let item of list) { for (let item of list) {
if (!regex.test(item)) { if (!regex.test(item)) {
this._namesFieldNode.setCustomValidity( this._namesFieldNode.setCustomValidity(
`Pool name "${item}" contains invalid symbols.`); `Pool name "${item}" contains invalid symbols.`
);
return; return;
} }
} }
this._namesFieldNode.setCustomValidity(''); this._namesFieldNode.setCustomValidity("");
} }
_evtPostsInput(e) { _evtPostsInput(e) {
@ -86,46 +90,50 @@ class PoolCreateView extends events.EventTarget {
for (let item of list) { for (let item of list) {
if (!regex.test(item)) { if (!regex.test(item)) {
this._postsFieldNode.setCustomValidity( this._postsFieldNode.setCustomValidity(
`Pool ID "${item}" is not an integer.`); `Pool ID "${item}" is not an integer.`
);
return; return;
} }
} }
this._postsFieldNode.setCustomValidity(''); this._postsFieldNode.setCustomValidity("");
} }
_evtSubmit(e) { _evtSubmit(e) {
e.preventDefault(); e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', { this.dispatchEvent(
detail: { new CustomEvent("submit", {
names: misc.splitByWhitespace(this._namesFieldNode.value), detail: {
category: this._categoryFieldNode.value, names: misc.splitByWhitespace(this._namesFieldNode.value),
description: this._descriptionFieldNode.value, category: this._categoryFieldNode.value,
posts: misc.splitByWhitespace(this._postsFieldNode.value) description: this._descriptionFieldNode.value,
.map(i => parseInt(i)) posts: misc
}, .splitByWhitespace(this._postsFieldNode.value)
})); .map((i) => parseInt(i)),
},
})
);
} }
get _formNode() { get _formNode() {
return this._hostNode.querySelector('form'); return this._hostNode.querySelector("form");
} }
get _namesFieldNode() { get _namesFieldNode() {
return this._formNode.querySelector('.names input'); return this._formNode.querySelector(".names input");
} }
get _categoryFieldNode() { get _categoryFieldNode() {
return this._formNode.querySelector('.category select'); return this._formNode.querySelector(".category select");
} }
get _descriptionFieldNode() { get _descriptionFieldNode() {
return this._formNode.querySelector('.description textarea'); return this._formNode.querySelector(".description textarea");
} }
get _postsFieldNode() { 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 events = require("../events.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const template = views.getTemplate('pool-delete'); const template = views.getTemplate("pool-delete");
class PoolDeleteView extends events.EventTarget { class PoolDeleteView extends events.EventTarget {
constructor(ctx) { constructor(ctx) {
@ -13,7 +13,7 @@ class PoolDeleteView extends events.EventTarget {
this._pool = ctx.pool; this._pool = ctx.pool;
views.replaceContent(this._hostNode, template(ctx)); views.replaceContent(this._hostNode, template(ctx));
views.decorateValidator(this._formNode); views.decorateValidator(this._formNode);
this._formNode.addEventListener('submit', e => this._evtSubmit(e)); this._formNode.addEventListener("submit", (e) => this._evtSubmit(e));
} }
clearMessages() { clearMessages() {
@ -38,15 +38,17 @@ class PoolDeleteView extends events.EventTarget {
_evtSubmit(e) { _evtSubmit(e) {
e.preventDefault(); e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', { this.dispatchEvent(
detail: { new CustomEvent("submit", {
pool: this._pool, detail: {
}, pool: this._pool,
})); },
})
);
} }
get _formNode() { 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 events = require("../events.js");
const api = require('../api.js'); const api = require("../api.js");
const misc = require('../util/misc.js'); const misc = require("../util/misc.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const Post = require('../models/post.js'); const Post = require("../models/post.js");
const template = views.getTemplate('pool-edit'); const template = views.getTemplate("pool-edit");
class PoolEditView extends events.EventTarget { class PoolEditView extends events.EventTarget {
constructor(ctx) { constructor(ctx) {
@ -19,24 +19,26 @@ class PoolEditView extends events.EventTarget {
views.decorateValidator(this._formNode); views.decorateValidator(this._formNode);
if (this._namesFieldNode) { if (this._namesFieldNode) {
this._namesFieldNode.addEventListener( this._namesFieldNode.addEventListener("input", (e) =>
'input', e => this._evtNameInput(e)); this._evtNameInput(e)
);
} }
if (this._postsFieldNode) { if (this._postsFieldNode) {
this._postsFieldNode.addEventListener( this._postsFieldNode.addEventListener("input", (e) =>
'input', e => this._evtPostsInput(e)); this._evtPostsInput(e)
);
} }
for (let node of this._formNode.querySelectorAll( for (let node of this._formNode.querySelectorAll(
'input, select, textarea, posts')) { "input, select, textarea, posts"
node.addEventListener( )) {
'change', e => { node.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent('change')); this.dispatchEvent(new CustomEvent("change"));
}); });
} }
this._formNode.addEventListener('submit', e => this._evtSubmit(e)); this._formNode.addEventListener("submit", (e) => this._evtSubmit(e));
} }
clearMessages() { clearMessages() {
@ -65,19 +67,21 @@ class PoolEditView extends events.EventTarget {
if (!list.length) { if (!list.length) {
this._namesFieldNode.setCustomValidity( this._namesFieldNode.setCustomValidity(
'Pools must have at least one name.'); "Pools must have at least one name."
);
return; return;
} }
for (let item of list) { for (let item of list) {
if (!regex.test(item)) { if (!regex.test(item)) {
this._namesFieldNode.setCustomValidity( this._namesFieldNode.setCustomValidity(
`Pool name "${item}" contains invalid symbols.`); `Pool name "${item}" contains invalid symbols.`
);
return; return;
} }
} }
this._namesFieldNode.setCustomValidity(''); this._namesFieldNode.setCustomValidity("");
} }
_evtPostsInput(e) { _evtPostsInput(e) {
@ -87,57 +91,60 @@ class PoolEditView extends events.EventTarget {
for (let item of list) { for (let item of list) {
if (!regex.test(item)) { if (!regex.test(item)) {
this._postsFieldNode.setCustomValidity( this._postsFieldNode.setCustomValidity(
`Pool ID "${item}" is not an integer.`); `Pool ID "${item}" is not an integer.`
);
return; return;
} }
} }
this._postsFieldNode.setCustomValidity(''); this._postsFieldNode.setCustomValidity("");
} }
_evtSubmit(e) { _evtSubmit(e) {
e.preventDefault(); e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', { this.dispatchEvent(
detail: { new CustomEvent("submit", {
pool: this._pool, detail: {
pool: this._pool,
names: this._namesFieldNode ? names: this._namesFieldNode
misc.splitByWhitespace(this._namesFieldNode.value) : ? misc.splitByWhitespace(this._namesFieldNode.value)
undefined, : undefined,
category: this._categoryFieldNode ? category: this._categoryFieldNode
this._categoryFieldNode.value : ? this._categoryFieldNode.value
undefined, : undefined,
description: this._descriptionFieldNode ? description: this._descriptionFieldNode
this._descriptionFieldNode.value : ? this._descriptionFieldNode.value
undefined, : undefined,
posts: this._postsFieldNode ? posts: this._postsFieldNode
misc.splitByWhitespace(this._postsFieldNode.value) : ? misc.splitByWhitespace(this._postsFieldNode.value)
undefined, : undefined,
}, },
})); })
);
} }
get _formNode() { get _formNode() {
return this._hostNode.querySelector('form'); return this._hostNode.querySelector("form");
} }
get _namesFieldNode() { get _namesFieldNode() {
return this._formNode.querySelector('.names input'); return this._formNode.querySelector(".names input");
} }
get _categoryFieldNode() { get _categoryFieldNode() {
return this._formNode.querySelector('.category select'); return this._formNode.querySelector(".category select");
} }
get _descriptionFieldNode() { get _descriptionFieldNode() {
return this._formNode.querySelector('.description textarea'); return this._formNode.querySelector(".description textarea");
} }
get _postsFieldNode() { 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 events = require("../events.js");
const api = require('../api.js'); const api = require("../api.js");
const views = require('../util/views.js'); const views = require("../util/views.js");
const PoolAutoCompleteControl = const PoolAutoCompleteControl = require("../controls/pool_auto_complete_control.js");
require('../controls/pool_auto_complete_control.js');
const template = views.getTemplate('pool-merge'); const template = views.getTemplate("pool-merge");
class PoolMergeView extends events.EventTarget { class PoolMergeView extends events.EventTarget {
constructor(ctx) { constructor(ctx) {
@ -23,15 +22,18 @@ class PoolMergeView extends events.EventTarget {
this._autoCompleteControl = new PoolAutoCompleteControl( this._autoCompleteControl = new PoolAutoCompleteControl(
this._targetPoolFieldNode, this._targetPoolFieldNode,
{ {
confirm: pool => { confirm: (pool) => {
this._targetPoolId = pool.id; this._targetPoolId = pool.id;
this._autoCompleteControl.replaceSelectedText( 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() { clearMessages() {
@ -56,24 +58,26 @@ class PoolMergeView extends events.EventTarget {
_evtSubmit(e) { _evtSubmit(e) {
e.preventDefault(); e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', { this.dispatchEvent(
detail: { new CustomEvent("submit", {
pool: this._pool, detail: {
targetPoolId: this._targetPoolId pool: this._pool,
}, targetPoolId: this._targetPoolId,
})); },
})
);
} }
get _formNode() { get _formNode() {
return this._hostNode.querySelector('form'); return this._hostNode.querySelector("form");
} }
get _targetPoolFieldNode() { get _targetPoolFieldNode() {
return this._formNode.querySelector('input[name=target-pool]'); return this._formNode.querySelector("input[name=target-pool]");
} }
get _addAliasCheckboxNode() { 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 { class PoolSummaryView {
constructor(ctx) { constructor(ctx) {

View file

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

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