mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
prettier
This commit is contained in:
parent
4ae468142d
commit
33ede2722b
76 changed files with 1394 additions and 1119 deletions
|
@ -2,6 +2,13 @@ tabWidth: 2
|
||||||
useTabs: false
|
useTabs: false
|
||||||
printWidth: 120
|
printWidth: 120
|
||||||
semi: true
|
semi: true
|
||||||
singleQuote: false
|
singleQuote: true
|
||||||
bracketSpacing: true
|
bracketSpacing: true
|
||||||
endOfLine: lf
|
endOfLine: lf
|
||||||
|
quoteProps: as-needed
|
||||||
|
trailingComma: all
|
||||||
|
arrowParens: always
|
||||||
|
overrides:
|
||||||
|
- files: "*.css"
|
||||||
|
options:
|
||||||
|
singleQuote: false
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import tsEslint from 'typescript-eslint';
|
import tsEslint from 'typescript-eslint';
|
||||||
import vitestPlugin from 'eslint-plugin-vitest';
|
import vitestPlugin from 'eslint-plugin-vitest';
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
import globals from 'globals';
|
import globals from 'globals';
|
||||||
|
|
||||||
export default tsEslint.config(
|
export default tsEslint.config(
|
||||||
...tsEslint.configs.recommended,
|
...tsEslint.configs.recommended,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
{
|
{
|
||||||
name: 'PhilomenaConfig',
|
name: 'PhilomenaConfig',
|
||||||
files: ['**/*.js', '**/*.ts'],
|
files: ['**/*.js', '**/*.ts'],
|
||||||
|
@ -12,24 +14,22 @@ export default tsEslint.config(
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 6,
|
ecmaVersion: 6,
|
||||||
sourceType: 'module'
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
...globals.browser
|
...globals.browser,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'accessor-pairs': 2,
|
'accessor-pairs': 2,
|
||||||
'array-bracket-spacing': 0,
|
'array-bracket-spacing': 0,
|
||||||
'array-callback-return': 2,
|
'array-callback-return': 2,
|
||||||
'arrow-body-style': 0,
|
'arrow-body-style': 0,
|
||||||
'arrow-parens': [2, 'as-needed'],
|
|
||||||
'arrow-spacing': 2,
|
'arrow-spacing': 2,
|
||||||
'block-scoped-var': 2,
|
'block-scoped-var': 2,
|
||||||
'block-spacing': 2,
|
'block-spacing': 2,
|
||||||
'brace-style': [2, 'stroustrup', {allowSingleLine: true}],
|
|
||||||
'callback-return': 0,
|
'callback-return': 0,
|
||||||
camelcase: [2, {allow: ['camo_url', 'spoiler_image_uri', 'image_ids']}],
|
camelcase: [2, { allow: ['camo_url', 'spoiler_image_uri', 'image_ids'] }],
|
||||||
'class-methods-use-this': 0,
|
'class-methods-use-this': 0,
|
||||||
'comma-dangle': [2, 'only-multiline'],
|
'comma-dangle': [2, 'only-multiline'],
|
||||||
'comma-spacing': 2,
|
'comma-spacing': 2,
|
||||||
|
@ -42,7 +42,7 @@ export default tsEslint.config(
|
||||||
curly: [2, 'multi-line', 'consistent'],
|
curly: [2, 'multi-line', 'consistent'],
|
||||||
'default-case': 2,
|
'default-case': 2,
|
||||||
'dot-location': [2, 'property'],
|
'dot-location': [2, 'property'],
|
||||||
'dot-notation': [2, {allowKeywords: true}],
|
'dot-notation': [2, { allowKeywords: true }],
|
||||||
'eol-last': 2,
|
'eol-last': 2,
|
||||||
eqeqeq: 2,
|
eqeqeq: 2,
|
||||||
'func-call-spacing': 0,
|
'func-call-spacing': 0,
|
||||||
|
@ -56,7 +56,6 @@ export default tsEslint.config(
|
||||||
'id-blacklist': 0,
|
'id-blacklist': 0,
|
||||||
'id-length': 0,
|
'id-length': 0,
|
||||||
'id-match': 2,
|
'id-match': 2,
|
||||||
indent: [2, 2, {SwitchCase: 1, VariableDeclarator: {var: 2, let: 2, const: 3}}],
|
|
||||||
'init-declarations': 0,
|
'init-declarations': 0,
|
||||||
'jsx-quotes': 0,
|
'jsx-quotes': 0,
|
||||||
'key-spacing': 0,
|
'key-spacing': 0,
|
||||||
|
@ -110,7 +109,7 @@ export default tsEslint.config(
|
||||||
'no-extra-bind': 2,
|
'no-extra-bind': 2,
|
||||||
'no-extra-boolean-cast': 2,
|
'no-extra-boolean-cast': 2,
|
||||||
'no-extra-label': 2,
|
'no-extra-label': 2,
|
||||||
'no-extra-parens': [2, 'all', {nestedBinaryExpressions: false}],
|
'no-extra-parens': [2, 'all', { nestedBinaryExpressions: false }],
|
||||||
'no-extra-semi': 2,
|
'no-extra-semi': 2,
|
||||||
'no-fallthrough': 2,
|
'no-fallthrough': 2,
|
||||||
'no-floating-decimal': 2,
|
'no-floating-decimal': 2,
|
||||||
|
@ -136,7 +135,7 @@ export default tsEslint.config(
|
||||||
'no-mixed-spaces-and-tabs': 2,
|
'no-mixed-spaces-and-tabs': 2,
|
||||||
'no-multi-spaces': 0,
|
'no-multi-spaces': 0,
|
||||||
'no-multi-str': 2,
|
'no-multi-str': 2,
|
||||||
'no-multiple-empty-lines': [2, {max: 3, maxBOF: 0, maxEOF: 1}],
|
'no-multiple-empty-lines': [2, { max: 3, maxBOF: 0, maxEOF: 1 }],
|
||||||
'no-native-reassign': 2,
|
'no-native-reassign': 2,
|
||||||
'no-negated-condition': 0,
|
'no-negated-condition': 0,
|
||||||
'no-negated-in-lhs': 2,
|
'no-negated-in-lhs': 2,
|
||||||
|
@ -190,9 +189,9 @@ export default tsEslint.config(
|
||||||
'no-unreachable': 2,
|
'no-unreachable': 2,
|
||||||
'no-unsafe-finally': 2,
|
'no-unsafe-finally': 2,
|
||||||
'no-unsafe-negation': 2,
|
'no-unsafe-negation': 2,
|
||||||
'no-unused-expressions': [2, {allowShortCircuit: true, allowTernary: true}],
|
'no-unused-expressions': [2, { allowShortCircuit: true, allowTernary: true }],
|
||||||
'no-unused-labels': 2,
|
'no-unused-labels': 2,
|
||||||
'no-unused-vars': [2, {vars: 'all', args: 'after-used', varsIgnorePattern: '^_', argsIgnorePattern: '^_'}],
|
'no-unused-vars': [2, { vars: 'all', args: 'after-used', varsIgnorePattern: '^_', argsIgnorePattern: '^_' }],
|
||||||
'no-use-before-define': [2, 'nofunc'],
|
'no-use-before-define': [2, 'nofunc'],
|
||||||
'no-useless-call': 2,
|
'no-useless-call': 2,
|
||||||
'no-useless-computed-key': 2,
|
'no-useless-computed-key': 2,
|
||||||
|
@ -222,21 +221,19 @@ export default tsEslint.config(
|
||||||
'prefer-spread': 0,
|
'prefer-spread': 0,
|
||||||
'prefer-template': 2,
|
'prefer-template': 2,
|
||||||
'quote-props': [2, 'as-needed'],
|
'quote-props': [2, 'as-needed'],
|
||||||
quotes: [2, 'single'],
|
|
||||||
radix: 2,
|
radix: 2,
|
||||||
'require-jsdoc': 0,
|
'require-jsdoc': 0,
|
||||||
'require-yield': 2,
|
'require-yield': 2,
|
||||||
'rest-spread-spacing': 2,
|
'rest-spread-spacing': 2,
|
||||||
'semi-spacing': [2, {before: false, after: true}],
|
'semi-spacing': [2, { before: false, after: true }],
|
||||||
semi: 2,
|
semi: 2,
|
||||||
'sort-imports': 0,
|
'sort-imports': 0,
|
||||||
'sort-keys': 0,
|
'sort-keys': 0,
|
||||||
'sort-vars': 0,
|
'sort-vars': 0,
|
||||||
'space-before-blocks': [2, 'always'],
|
'space-before-blocks': [2, 'always'],
|
||||||
'space-before-function-paren': [2, 'never'],
|
|
||||||
'space-in-parens': [2, 'never'],
|
'space-in-parens': [2, 'never'],
|
||||||
'space-infix-ops': 2,
|
'space-infix-ops': 2,
|
||||||
'space-unary-ops': [2, {words: true, nonwords: false}],
|
'space-unary-ops': [2, { words: true, nonwords: false }],
|
||||||
'spaced-comment': 0,
|
'spaced-comment': 0,
|
||||||
strict: [2, 'function'],
|
strict: [2, 'function'],
|
||||||
'symbol-description': 2,
|
'symbol-description': 2,
|
||||||
|
@ -251,18 +248,15 @@ export default tsEslint.config(
|
||||||
'yield-star-spacing': 2,
|
'yield-star-spacing': 2,
|
||||||
yoda: [2, 'never'],
|
yoda: [2, 'never'],
|
||||||
},
|
},
|
||||||
ignores: [
|
ignores: ['js/vendor/*', 'vite.config.ts'],
|
||||||
'js/vendor/*',
|
|
||||||
'vite.config.ts'
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.js'],
|
files: ['**/*.js'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
'@typescript-eslint/no-unused-expressions': 'off',
|
'@typescript-eslint/no-unused-expressions': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': 'off'
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.ts'],
|
files: ['**/*.ts'],
|
||||||
|
@ -271,15 +265,18 @@ export default tsEslint.config(
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
'no-redeclare': 'off',
|
'no-redeclare': 'off',
|
||||||
'no-shadow': 'off',
|
'no-shadow': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': [2, {vars: 'all', args: 'after-used', varsIgnorePattern: '^_.*', argsIgnorePattern: '^_.*'}],
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
2,
|
||||||
|
{ vars: 'all', args: 'after-used', varsIgnorePattern: '^_.*', argsIgnorePattern: '^_.*' },
|
||||||
|
],
|
||||||
'@typescript-eslint/no-redeclare': 2,
|
'@typescript-eslint/no-redeclare': 2,
|
||||||
'@typescript-eslint/no-shadow': 2
|
'@typescript-eslint/no-shadow': 2,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.spec.ts', '**/test/*.ts'],
|
files: ['**/*.spec.ts', '**/test/*.ts'],
|
||||||
plugins: {
|
plugins: {
|
||||||
vitest: vitestPlugin
|
vitest: vitestPlugin,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...vitestPlugin.configs.recommended.rules,
|
...vitestPlugin.configs.recommended.rules,
|
||||||
|
@ -287,7 +284,7 @@ export default tsEslint.config(
|
||||||
'no-undefined': 'off',
|
'no-undefined': 'off',
|
||||||
'no-unused-expressions': 0,
|
'no-unused-expressions': 0,
|
||||||
'vitest/valid-expect': 0,
|
'vitest/valid-expect': 0,
|
||||||
'@typescript-eslint/no-unused-expressions': 0
|
'@typescript-eslint/no-unused-expressions': 0,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,11 +23,11 @@ describe('filterNode', () => {
|
||||||
<picture><img src=""/></picture>
|
<picture><img src=""/></picture>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return [ element, assertNotNull($<HTMLDivElement>('.js-spoiler-info-overlay', element)) ];
|
return [element, assertNotNull($<HTMLDivElement>('.js-spoiler-info-overlay', element))];
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should show image media boxes not matching any filter', () => {
|
it('should show image media boxes not matching any filter', () => {
|
||||||
const [ container, spoilerOverlay ] = makeMediaContainer();
|
const [container, spoilerOverlay] = makeMediaContainer();
|
||||||
|
|
||||||
filterNode(container);
|
filterNode(container);
|
||||||
expect(spoilerOverlay).not.toContainHTML('(Complex Filter)');
|
expect(spoilerOverlay).not.toContainHTML('(Complex Filter)');
|
||||||
|
@ -36,7 +36,7 @@ describe('filterNode', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should spoiler media boxes spoilered by a tag filter', () => {
|
it('should spoiler media boxes spoilered by a tag filter', () => {
|
||||||
const [ container, spoilerOverlay ] = makeMediaContainer();
|
const [container, spoilerOverlay] = makeMediaContainer();
|
||||||
window.booru.spoileredTagList = [1];
|
window.booru.spoileredTagList = [1];
|
||||||
|
|
||||||
filterNode(container);
|
filterNode(container);
|
||||||
|
@ -45,7 +45,7 @@ describe('filterNode', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should spoiler media boxes spoilered by a complex filter', () => {
|
it('should spoiler media boxes spoilered by a complex filter', () => {
|
||||||
const [ container, spoilerOverlay ] = makeMediaContainer();
|
const [container, spoilerOverlay] = makeMediaContainer();
|
||||||
window.booru.spoileredFilter = parseSearch('id:1');
|
window.booru.spoileredFilter = parseSearch('id:1');
|
||||||
|
|
||||||
filterNode(container);
|
filterNode(container);
|
||||||
|
@ -54,7 +54,7 @@ describe('filterNode', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hide media boxes hidden by a tag filter', () => {
|
it('should hide media boxes hidden by a tag filter', () => {
|
||||||
const [ container, spoilerOverlay ] = makeMediaContainer();
|
const [container, spoilerOverlay] = makeMediaContainer();
|
||||||
window.booru.hiddenTagList = [1];
|
window.booru.hiddenTagList = [1];
|
||||||
|
|
||||||
filterNode(container);
|
filterNode(container);
|
||||||
|
@ -64,7 +64,7 @@ describe('filterNode', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hide media boxes hidden by a complex filter', () => {
|
it('should hide media boxes hidden by a complex filter', () => {
|
||||||
const [ container, spoilerOverlay ] = makeMediaContainer();
|
const [container, spoilerOverlay] = makeMediaContainer();
|
||||||
window.booru.hiddenFilter = parseSearch('id:1');
|
window.booru.hiddenFilter = parseSearch('id:1');
|
||||||
|
|
||||||
filterNode(container);
|
filterNode(container);
|
||||||
|
@ -90,12 +90,12 @@ describe('filterNode', () => {
|
||||||
element,
|
element,
|
||||||
assertNotNull($<HTMLDivElement>('.image-filtered', element)),
|
assertNotNull($<HTMLDivElement>('.image-filtered', element)),
|
||||||
assertNotNull($<HTMLDivElement>('.image-show', element)),
|
assertNotNull($<HTMLDivElement>('.image-show', element)),
|
||||||
assertNotNull($<HTMLSpanElement>('.filter-explanation', element))
|
assertNotNull($<HTMLSpanElement>('.filter-explanation', element)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should show image blocks not matching any filter', () => {
|
it('should show image blocks not matching any filter', () => {
|
||||||
const [ container, imageFiltered, imageShow ] = makeImageBlock();
|
const [container, imageFiltered, imageShow] = makeImageBlock();
|
||||||
|
|
||||||
filterNode(container);
|
filterNode(container);
|
||||||
expect(imageFiltered).toHaveClass('hidden');
|
expect(imageFiltered).toHaveClass('hidden');
|
||||||
|
@ -104,7 +104,7 @@ describe('filterNode', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should spoiler image blocks spoilered by a tag filter', () => {
|
it('should spoiler image blocks spoilered by a tag filter', () => {
|
||||||
const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
|
const [container, imageFiltered, imageShow, filterExplanation] = makeImageBlock();
|
||||||
window.booru.spoileredTagList = [1];
|
window.booru.spoileredTagList = [1];
|
||||||
|
|
||||||
filterNode(container);
|
filterNode(container);
|
||||||
|
@ -116,7 +116,7 @@ describe('filterNode', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should spoiler image blocks spoilered by a complex filter', () => {
|
it('should spoiler image blocks spoilered by a complex filter', () => {
|
||||||
const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
|
const [container, imageFiltered, imageShow, filterExplanation] = makeImageBlock();
|
||||||
window.booru.spoileredFilter = parseSearch('id:1');
|
window.booru.spoileredFilter = parseSearch('id:1');
|
||||||
|
|
||||||
filterNode(container);
|
filterNode(container);
|
||||||
|
@ -128,7 +128,7 @@ describe('filterNode', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hide image blocks hidden by a tag filter', () => {
|
it('should hide image blocks hidden by a tag filter', () => {
|
||||||
const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
|
const [container, imageFiltered, imageShow, filterExplanation] = makeImageBlock();
|
||||||
window.booru.hiddenTagList = [1];
|
window.booru.hiddenTagList = [1];
|
||||||
|
|
||||||
filterNode(container);
|
filterNode(container);
|
||||||
|
@ -140,7 +140,7 @@ describe('filterNode', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hide image blocks hidden by a complex filter', () => {
|
it('should hide image blocks hidden by a complex filter', () => {
|
||||||
const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
|
const [container, imageFiltered, imageShow, filterExplanation] = makeImageBlock();
|
||||||
window.booru.hiddenFilter = parseSearch('id:1');
|
window.booru.hiddenFilter = parseSearch('id:1');
|
||||||
|
|
||||||
filterNode(container);
|
filterNode(container);
|
||||||
|
@ -150,7 +150,6 @@ describe('filterNode', () => {
|
||||||
expect(filterExplanation).toContainHTML('complex tag expression');
|
expect(filterExplanation).toContainHTML('complex tag expression');
|
||||||
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('initImagesClientside', () => {
|
describe('initImagesClientside', () => {
|
||||||
|
|
|
@ -5,7 +5,9 @@ import { fireEvent } from '@testing-library/dom';
|
||||||
|
|
||||||
describe('Input duplicator functionality', () => {
|
describe('Input duplicator functionality', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
document.documentElement.insertAdjacentHTML('beforeend', `<form action="/">
|
document.documentElement.insertAdjacentHTML(
|
||||||
|
'beforeend',
|
||||||
|
`<form action="/">
|
||||||
<div class="js-max-input-count">3</div>
|
<div class="js-max-input-count">3</div>
|
||||||
<div class="js-input-source">
|
<div class="js-input-source">
|
||||||
<input id="0" name="0" class="js-input" type="text"/>
|
<input id="0" name="0" class="js-input" type="text"/>
|
||||||
|
@ -16,7 +18,8 @@ describe('Input duplicator functionality', () => {
|
||||||
<div class="js-button-container">
|
<div class="js-button-container">
|
||||||
<button type="button" class="js-add-input">Add input</button>
|
<button type="button" class="js-add-input">Add input</button>
|
||||||
</div>
|
</div>
|
||||||
</form>`);
|
</form>`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
|
@ -29,7 +29,7 @@ describe('Remote utilities', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('a[data-remote]', () => {
|
describe('a[data-remote]', () => {
|
||||||
const submitA = ({ setMethod }: { setMethod: boolean; }) => {
|
const submitA = ({ setMethod }: { setMethod: boolean }) => {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = mockEndpoint;
|
a.href = mockEndpoint;
|
||||||
a.dataset.remote = 'remote';
|
a.dataset.remote = 'remote';
|
||||||
|
@ -51,8 +51,8 @@ describe('Remote utilities', () => {
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: {
|
headers: {
|
||||||
'x-csrf-token': window.booru.csrfToken,
|
'x-csrf-token': window.booru.csrfToken,
|
||||||
'x-requested-with': 'XMLHttpRequest'
|
'x-requested-with': 'XMLHttpRequest',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -64,15 +64,16 @@ describe('Remote utilities', () => {
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: {
|
headers: {
|
||||||
'x-csrf-token': window.booru.csrfToken,
|
'x-csrf-token': window.booru.csrfToken,
|
||||||
'x-requested-with': 'XMLHttpRequest'
|
'x-requested-with': 'XMLHttpRequest',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit fetchcomplete event', () => new Promise<void>(resolve => {
|
it('should emit fetchcomplete event', () =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
let a: HTMLAnchorElement | null = null;
|
let a: HTMLAnchorElement | null = null;
|
||||||
|
|
||||||
addOneShotEventListener('fetchcomplete', event => {
|
addOneShotEventListener('fetchcomplete', (event) => {
|
||||||
expect(event.target).toBe(a);
|
expect(event.target).toBe(a);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
@ -93,12 +94,13 @@ describe('Remote utilities', () => {
|
||||||
return a;
|
return a;
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should submit a form with the given action', () => new Promise<void>(resolve => {
|
it('should submit a form with the given action', () =>
|
||||||
addOneShotEventListener('submit', event => {
|
new Promise<void>((resolve) => {
|
||||||
|
addOneShotEventListener('submit', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const target = assertType(event.target, HTMLFormElement);
|
const target = assertType(event.target, HTMLFormElement);
|
||||||
const [ csrf, method ] = target.querySelectorAll('input');
|
const [csrf, method] = target.querySelectorAll('input');
|
||||||
|
|
||||||
expect(csrf.name).toBe('_csrf_token');
|
expect(csrf.name).toBe('_csrf_token');
|
||||||
expect(csrf.value).toBe(window.booru.csrfToken);
|
expect(csrf.value).toBe(window.booru.csrfToken);
|
||||||
|
@ -167,7 +169,7 @@ describe('Remote utilities', () => {
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: {
|
headers: {
|
||||||
'x-csrf-token': window.booru.csrfToken,
|
'x-csrf-token': window.booru.csrfToken,
|
||||||
'x-requested-with': 'XMLHttpRequest'
|
'x-requested-with': 'XMLHttpRequest',
|
||||||
},
|
},
|
||||||
body: new FormData(),
|
body: new FormData(),
|
||||||
});
|
});
|
||||||
|
@ -183,16 +185,17 @@ describe('Remote utilities', () => {
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: {
|
headers: {
|
||||||
'x-csrf-token': window.booru.csrfToken,
|
'x-csrf-token': window.booru.csrfToken,
|
||||||
'x-requested-with': 'XMLHttpRequest'
|
'x-requested-with': 'XMLHttpRequest',
|
||||||
},
|
},
|
||||||
body: new FormData(),
|
body: new FormData(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit fetchcomplete event', () => new Promise<void>(resolve => {
|
it('should emit fetchcomplete event', () =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
let form: HTMLFormElement | null = null;
|
let form: HTMLFormElement | null = null;
|
||||||
|
|
||||||
addOneShotEventListener('fetchcomplete', event => {
|
addOneShotEventListener('fetchcomplete', (event) => {
|
||||||
expect(event.target).toBe(form);
|
expect(event.target).toBe(form);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
@ -201,7 +204,7 @@ describe('Remote utilities', () => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should reload the page on 300 multiple choices response', () => {
|
it('should reload the page on 300 multiple choices response', () => {
|
||||||
vi.spyOn(global, 'fetch').mockResolvedValue(new Response('', { status: 300}));
|
vi.spyOn(global, 'fetch').mockResolvedValue(new Response('', { status: 300 }));
|
||||||
|
|
||||||
submitForm();
|
submitForm();
|
||||||
return waitFor(() => expect(window.location.reload).toHaveBeenCalledTimes(1));
|
return waitFor(() => expect(window.location.reload).toHaveBeenCalledTimes(1));
|
||||||
|
@ -211,7 +214,7 @@ describe('Remote utilities', () => {
|
||||||
|
|
||||||
describe('Form utilities', () => {
|
describe('Form utilities', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => {
|
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
|
||||||
cb(1);
|
cb(1);
|
||||||
return 1;
|
return 1;
|
||||||
});
|
});
|
||||||
|
@ -257,7 +260,7 @@ describe('Form utilities', () => {
|
||||||
|
|
||||||
// jsdom has no implementation for HTMLFormElement.prototype.submit
|
// jsdom has no implementation for HTMLFormElement.prototype.submit
|
||||||
// and will return an error if the event's default isn't prevented
|
// and will return an error if the event's default isn't prevented
|
||||||
form.addEventListener('submit', event => event.preventDefault());
|
form.addEventListener('submit', (event) => event.preventDefault());
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.type = 'submit';
|
button.type = 'submit';
|
||||||
|
@ -267,7 +270,7 @@ describe('Form utilities', () => {
|
||||||
form.insertAdjacentElement('beforeend', button);
|
form.insertAdjacentElement('beforeend', button);
|
||||||
document.documentElement.insertAdjacentElement('beforeend', form);
|
document.documentElement.insertAdjacentElement('beforeend', form);
|
||||||
|
|
||||||
return [ form, button ];
|
return [form, button];
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitText = 'Submit';
|
const submitText = 'Submit';
|
||||||
|
@ -276,7 +279,7 @@ describe('Form utilities', () => {
|
||||||
const loadingMarkup = '<em>Loading...</em>';
|
const loadingMarkup = '<em>Loading...</em>';
|
||||||
|
|
||||||
it('should disable submit button containing a text child on click', () => {
|
it('should disable submit button containing a text child on click', () => {
|
||||||
const [ , button ] = createFormAndButton(submitText, loadingText);
|
const [, button] = createFormAndButton(submitText, loadingText);
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
|
|
||||||
expect(button.textContent).toEqual(' Loading...');
|
expect(button.textContent).toEqual(' Loading...');
|
||||||
|
@ -284,7 +287,7 @@ describe('Form utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disable submit button containing element children on click', () => {
|
it('should disable submit button containing element children on click', () => {
|
||||||
const [ , button ] = createFormAndButton(submitMarkup, loadingMarkup);
|
const [, button] = createFormAndButton(submitMarkup, loadingMarkup);
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
|
|
||||||
expect(button.innerHTML).toEqual(loadingMarkup);
|
expect(button.innerHTML).toEqual(loadingMarkup);
|
||||||
|
@ -292,7 +295,7 @@ describe('Form utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not disable anything when the form is invalid', () => {
|
it('should not disable anything when the form is invalid', () => {
|
||||||
const [ form, button ] = createFormAndButton(submitText, loadingText);
|
const [form, button] = createFormAndButton(submitText, loadingText);
|
||||||
form.insertAdjacentHTML('afterbegin', '<input type="text" name="valid" required="true" />');
|
form.insertAdjacentHTML('afterbegin', '<input type="text" name="valid" required="true" />');
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
|
|
||||||
|
@ -301,7 +304,7 @@ describe('Form utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reset submit button containing a text child on completion', () => {
|
it('should reset submit button containing a text child on completion', () => {
|
||||||
const [ form, button ] = createFormAndButton(submitText, loadingText);
|
const [form, button] = createFormAndButton(submitText, loadingText);
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
fireEvent(form, new CustomEvent('reset', { bubbles: true }));
|
fireEvent(form, new CustomEvent('reset', { bubbles: true }));
|
||||||
|
|
||||||
|
@ -310,7 +313,7 @@ describe('Form utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reset submit button containing element children on completion', () => {
|
it('should reset submit button containing element children on completion', () => {
|
||||||
const [ form, button ] = createFormAndButton(submitMarkup, loadingMarkup);
|
const [form, button] = createFormAndButton(submitMarkup, loadingMarkup);
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
fireEvent(form, new CustomEvent('reset', { bubbles: true }));
|
fireEvent(form, new CustomEvent('reset', { bubbles: true }));
|
||||||
|
|
||||||
|
@ -319,7 +322,7 @@ describe('Form utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reset disabled form elements on pageshow', () => {
|
it('should reset disabled form elements on pageshow', () => {
|
||||||
const [ , button ] = createFormAndButton(submitText, loadingText);
|
const [, button] = createFormAndButton(submitText, loadingText);
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
fireEvent(window, new CustomEvent('pageshow'));
|
fireEvent(window, new CustomEvent('pageshow'));
|
||||||
|
|
||||||
|
|
|
@ -29,12 +29,16 @@ describe('Image upload form', () => {
|
||||||
let mockPng: File;
|
let mockPng: File;
|
||||||
let mockWebm: File;
|
let mockWebm: File;
|
||||||
|
|
||||||
beforeAll(async() => {
|
beforeAll(async () => {
|
||||||
const mockPngPath = join(__dirname, 'upload-test.png');
|
const mockPngPath = join(__dirname, 'upload-test.png');
|
||||||
const mockWebmPath = join(__dirname, 'upload-test.webm');
|
const mockWebmPath = join(__dirname, 'upload-test.webm');
|
||||||
|
|
||||||
mockPng = new File([(await promises.readFile(mockPngPath, { encoding: null })).buffer], 'upload-test.png', { type: 'image/png' });
|
mockPng = new File([(await promises.readFile(mockPngPath, { encoding: null })).buffer], 'upload-test.png', {
|
||||||
mockWebm = new File([(await promises.readFile(mockWebmPath, { encoding: null })).buffer], 'upload-test.webm', { type: 'video/webm' });
|
type: 'image/png',
|
||||||
|
});
|
||||||
|
mockWebm = new File([(await promises.readFile(mockWebmPath, { encoding: null })).buffer], 'upload-test.webm', {
|
||||||
|
type: 'video/webm',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
@ -47,7 +51,6 @@ describe('Image upload form', () => {
|
||||||
|
|
||||||
fixEventListeners(window);
|
fixEventListeners(window);
|
||||||
|
|
||||||
|
|
||||||
let form: HTMLFormElement;
|
let form: HTMLFormElement;
|
||||||
let imgPreviews: HTMLDivElement;
|
let imgPreviews: HTMLDivElement;
|
||||||
let fileField: HTMLInputElement;
|
let fileField: HTMLInputElement;
|
||||||
|
@ -63,7 +66,9 @@ describe('Image upload form', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
document.documentElement.insertAdjacentHTML('beforeend', `
|
document.documentElement.insertAdjacentHTML(
|
||||||
|
'beforeend',
|
||||||
|
`
|
||||||
<form action="/images">
|
<form action="/images">
|
||||||
<div id="js-image-upload-previews"></div>
|
<div id="js-image-upload-previews"></div>
|
||||||
<input id="image_image" name="image[image]" type="file" class="js-scraper" />
|
<input id="image_image" name="image[image]" type="file" class="js-scraper" />
|
||||||
|
@ -75,7 +80,8 @@ describe('Image upload form', () => {
|
||||||
<textarea id="image_tag_input" name="image[tag_input]" class="js-image-tags-input"></textarea>
|
<textarea id="image_tag_input" name="image[tag_input]" class="js-image-tags-input"></textarea>
|
||||||
<textarea id="image_description" name="image[description]" class="js-image-descr-input"></textarea>
|
<textarea id="image_description" name="image[description]" class="js-image-descr-input"></textarea>
|
||||||
</form>
|
</form>
|
||||||
`);
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
form = assertNotNull($<HTMLFormElement>('form'));
|
form = assertNotNull($<HTMLFormElement>('form'));
|
||||||
imgPreviews = assertNotNull($<HTMLDivElement>('#js-image-upload-previews'));
|
imgPreviews = assertNotNull($<HTMLDivElement>('#js-image-upload-previews'));
|
||||||
|
@ -121,7 +127,7 @@ describe('Image upload form', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should block navigation away after an image file is attached, but not after form submission', async() => {
|
it('should block navigation away after an image file is attached, but not after form submission', async () => {
|
||||||
fireEvent.change(fileField, { target: { files: [mockPng] } });
|
fireEvent.change(fileField, { target: { files: [mockPng] } });
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
assertFetchButtonIsDisabled();
|
assertFetchButtonIsDisabled();
|
||||||
|
@ -131,8 +137,8 @@ describe('Image upload form', () => {
|
||||||
const failedUnloadEvent = new Event('beforeunload', { cancelable: true });
|
const failedUnloadEvent = new Event('beforeunload', { cancelable: true });
|
||||||
expect(fireEvent(window, failedUnloadEvent)).toBe(false);
|
expect(fireEvent(window, failedUnloadEvent)).toBe(false);
|
||||||
|
|
||||||
await new Promise<void>(resolve => {
|
await new Promise<void>((resolve) => {
|
||||||
form.addEventListener('submit', event => {
|
form.addEventListener('submit', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
@ -143,11 +149,11 @@ describe('Image upload form', () => {
|
||||||
expect(fireEvent(window, succeededUnloadEvent)).toBe(true);
|
expect(fireEvent(window, succeededUnloadEvent)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should scrape images when the fetch button is clicked', async() => {
|
it('should scrape images when the fetch button is clicked', async () => {
|
||||||
fetchMock.mockResolvedValue(new Response(JSON.stringify(scrapeResponse), { status: 200 }));
|
fetchMock.mockResolvedValue(new Response(JSON.stringify(scrapeResponse), { status: 200 }));
|
||||||
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } });
|
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } });
|
||||||
|
|
||||||
await new Promise<void>(resolve => {
|
await new Promise<void>((resolve) => {
|
||||||
tagsEl.addEventListener('addtag', (event: Event) => {
|
tagsEl.addEventListener('addtag', (event: Event) => {
|
||||||
expect((event as CustomEvent).detail).toEqual({ name: 'artist:test' });
|
expect((event as CustomEvent).detail).toEqual({ name: 'artist:test' });
|
||||||
resolve();
|
resolve();
|
||||||
|
|
|
@ -52,15 +52,16 @@ function applySelectedValue(selection) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeSelected(firstOrLast, current, sibling) {
|
function changeSelected(firstOrLast, current, sibling) {
|
||||||
if (current && sibling) { // if the currently selected item has a sibling, move selection to it
|
if (current && sibling) {
|
||||||
|
// if the currently selected item has a sibling, move selection to it
|
||||||
current.classList.remove('autocomplete__item--selected');
|
current.classList.remove('autocomplete__item--selected');
|
||||||
sibling.classList.add('autocomplete__item--selected');
|
sibling.classList.add('autocomplete__item--selected');
|
||||||
}
|
} else if (current) {
|
||||||
else if (current) { // if the next keypress will take the user outside the list, restore the unautocompleted term
|
// if the next keypress will take the user outside the list, restore the unautocompleted term
|
||||||
restoreOriginalValue();
|
restoreOriginalValue();
|
||||||
removeSelected();
|
removeSelected();
|
||||||
}
|
} else if (firstOrLast) {
|
||||||
else if (firstOrLast) { // if no item in the list is selected, select the first or last
|
// if no item in the list is selected, select the first or last
|
||||||
firstOrLast.classList.add('autocomplete__item--selected');
|
firstOrLast.classList.add('autocomplete__item--selected');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +83,8 @@ function keydownHandler(event) {
|
||||||
if (selected && event.keyCode === 13) event.preventDefault(); // Enter
|
if (selected && event.keyCode === 13) event.preventDefault(); // Enter
|
||||||
|
|
||||||
// Close autocompletion popup when text cursor is outside current tag
|
// Close autocompletion popup when text cursor is outside current tag
|
||||||
if (selectedTerm && firstItem && (event.keyCode === 37 || event.keyCode === 39)) { // ArrowLeft || ArrowRight
|
if (selectedTerm && firstItem && (event.keyCode === 37 || event.keyCode === 39)) {
|
||||||
|
// ArrowLeft || ArrowRight
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (isSelectionOutsideCurrentTerm()) removeParent();
|
if (isSelectionOutsideCurrentTerm()) removeParent();
|
||||||
});
|
});
|
||||||
|
@ -92,7 +94,8 @@ function keydownHandler(event) {
|
||||||
if (event.keyCode === 38) changeSelected(lastItem, selected, selected && selected.previousSibling); // ArrowUp
|
if (event.keyCode === 38) changeSelected(lastItem, selected, selected && selected.previousSibling); // ArrowUp
|
||||||
if (event.keyCode === 40) changeSelected(firstItem, selected, selected && selected.nextSibling); // ArrowDown
|
if (event.keyCode === 40) changeSelected(firstItem, selected, selected && selected.nextSibling); // ArrowDown
|
||||||
if (event.keyCode === 13 || event.keyCode === 27 || event.keyCode === 188) removeParent(); // Enter || Esc || Comma
|
if (event.keyCode === 13 || event.keyCode === 27 || event.keyCode === 188) removeParent(); // Enter || Esc || Comma
|
||||||
if (event.keyCode === 38 || event.keyCode === 40) { // ArrowUp || ArrowDown
|
if (event.keyCode === 38 || event.keyCode === 40) {
|
||||||
|
// ArrowUp || ArrowDown
|
||||||
const newSelected = document.querySelector('.autocomplete__item--selected');
|
const newSelected = document.querySelector('.autocomplete__item--selected');
|
||||||
if (newSelected) applySelectedValue(newSelected.dataset.value);
|
if (newSelected) applySelectedValue(newSelected.dataset.value);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -123,8 +126,8 @@ function createItem(list, suggestion) {
|
||||||
type: 'click',
|
type: 'click',
|
||||||
label: suggestion.label,
|
label: suggestion.label,
|
||||||
value: suggestion.value,
|
value: suggestion.value,
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -136,7 +139,7 @@ function createList(suggestions) {
|
||||||
list = document.createElement('ul');
|
list = document.createElement('ul');
|
||||||
list.className = 'autocomplete__list';
|
list.className = 'autocomplete__list';
|
||||||
|
|
||||||
suggestions.forEach(suggestion => createItem(list, suggestion));
|
suggestions.forEach((suggestion) => createItem(list, suggestion));
|
||||||
|
|
||||||
parent.appendChild(list);
|
parent.appendChild(list);
|
||||||
}
|
}
|
||||||
|
@ -173,7 +176,7 @@ function showAutocomplete(suggestions, fetchedTerm, targetInput) {
|
||||||
function getSuggestions(term) {
|
function getSuggestions(term) {
|
||||||
// In case source URL was not given at all, do not try sending the request.
|
// In case source URL was not given at all, do not try sending the request.
|
||||||
if (!inputField.dataset.acSource) return [];
|
if (!inputField.dataset.acSource) return [];
|
||||||
return fetch(`${inputField.dataset.acSource}${term}`).then(response => response.json());
|
return fetch(`${inputField.dataset.acSource}${term}`).then((response) => response.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedTerm() {
|
function getSelectedTerm() {
|
||||||
|
@ -193,8 +196,7 @@ function toggleSearchAutocomplete() {
|
||||||
for (const searchField of document.querySelectorAll('input[data-ac-mode=search]')) {
|
for (const searchField of document.querySelectorAll('input[data-ac-mode=search]')) {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
searchField.autocomplete = 'off';
|
searchField.autocomplete = 'off';
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
searchField.removeAttribute('data-ac');
|
searchField.removeAttribute('data-ac');
|
||||||
searchField.autocomplete = 'on';
|
searchField.autocomplete = 'on';
|
||||||
}
|
}
|
||||||
|
@ -210,7 +212,7 @@ function listenAutocomplete() {
|
||||||
|
|
||||||
document.addEventListener('focusin', fetchLocalAutocomplete);
|
document.addEventListener('focusin', fetchLocalAutocomplete);
|
||||||
|
|
||||||
document.addEventListener('input', event => {
|
document.addEventListener('input', (event) => {
|
||||||
removeParent();
|
removeParent();
|
||||||
fetchLocalAutocomplete(event);
|
fetchLocalAutocomplete(event);
|
||||||
window.clearTimeout(timeout);
|
window.clearTimeout(timeout);
|
||||||
|
@ -230,12 +232,13 @@ function listenAutocomplete() {
|
||||||
}
|
}
|
||||||
|
|
||||||
originalTerm = selectedTerm[1].toLowerCase();
|
originalTerm = selectedTerm[1].toLowerCase();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
originalTerm = `${inputField.value}`.toLowerCase();
|
originalTerm = `${inputField.value}`.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
const suggestions = localAc.topK(originalTerm, suggestionsCount).map(({ name, imageCount }) => ({ label: `${name} (${imageCount})`, value: name }));
|
const suggestions = localAc
|
||||||
|
.topK(originalTerm, suggestionsCount)
|
||||||
|
.map(({ name, imageCount }) => ({ label: `${name} (${imageCount})`, value: name }));
|
||||||
|
|
||||||
if (suggestions.length) {
|
if (suggestions.length) {
|
||||||
return showAutocomplete(suggestions, originalTerm, event.target);
|
return showAutocomplete(suggestions, originalTerm, event.target);
|
||||||
|
@ -248,15 +251,14 @@ function listenAutocomplete() {
|
||||||
originalTerm = inputField.value;
|
originalTerm = inputField.value;
|
||||||
|
|
||||||
const fetchedTerm = inputField.value;
|
const fetchedTerm = inputField.value;
|
||||||
const {ac, acMinLength, acSource} = inputField.dataset;
|
const { ac, acMinLength, acSource } = inputField.dataset;
|
||||||
|
|
||||||
if (ac && acSource && (fetchedTerm.length >= acMinLength)) {
|
if (ac && acSource && fetchedTerm.length >= acMinLength) {
|
||||||
if (cache[fetchedTerm]) {
|
if (cache[fetchedTerm]) {
|
||||||
showAutocomplete(cache[fetchedTerm], fetchedTerm, event.target);
|
showAutocomplete(cache[fetchedTerm], fetchedTerm, event.target);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// inputField could get overwritten while the suggestions are being fetched - use event.target
|
// inputField could get overwritten while the suggestions are being fetched - use event.target
|
||||||
getSuggestions(fetchedTerm).then(suggestions => {
|
getSuggestions(fetchedTerm).then((suggestions) => {
|
||||||
if (fetchedTerm === event.target.value) {
|
if (fetchedTerm === event.target.value) {
|
||||||
showAutocomplete(suggestions, fetchedTerm, event.target);
|
showAutocomplete(suggestions, fetchedTerm, event.target);
|
||||||
}
|
}
|
||||||
|
@ -267,7 +269,7 @@ function listenAutocomplete() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// If there's a click outside the inputField, remove autocomplete
|
// If there's a click outside the inputField, remove autocomplete
|
||||||
document.addEventListener('click', event => {
|
document.addEventListener('click', (event) => {
|
||||||
if (event.target && event.target !== inputField) removeParent();
|
if (event.target && event.target !== inputField) removeParent();
|
||||||
if (event.target === inputField && isSearchField() && isSelectionOutsideCurrentTerm()) removeParent();
|
if (event.target === inputField && isSearchField() && isSelectionOutsideCurrentTerm()) removeParent();
|
||||||
});
|
});
|
||||||
|
@ -281,8 +283,10 @@ function listenAutocomplete() {
|
||||||
|
|
||||||
fetch(`/autocomplete/compiled?vsn=2&key=${cacheKey}`, { credentials: 'omit', cache: 'force-cache' })
|
fetch(`/autocomplete/compiled?vsn=2&key=${cacheKey}`, { credentials: 'omit', cache: 'force-cache' })
|
||||||
.then(handleError)
|
.then(handleError)
|
||||||
.then(resp => resp.arrayBuffer())
|
.then((resp) => resp.arrayBuffer())
|
||||||
.then(buf => localAc = new LocalAutocompleter(buf));
|
.then((buf) => {
|
||||||
|
localAc = new LocalAutocompleter(buf);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,11 +23,11 @@ function persistTag(tagData) {
|
||||||
*/
|
*/
|
||||||
function isStale(tag) {
|
function isStale(tag) {
|
||||||
const now = new Date().getTime() / 1000;
|
const now = new Date().getTime() / 1000;
|
||||||
return tag.fetchedAt === null || tag.fetchedAt < (now - 604800);
|
return tag.fetchedAt === null || tag.fetchedAt < now - 604800;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearTags() {
|
function clearTags() {
|
||||||
Object.keys(localStorage).forEach(key => {
|
Object.keys(localStorage).forEach((key) => {
|
||||||
if (key.substring(0, 9) === 'bor_tags_') {
|
if (key.substring(0, 9) === 'bor_tags_') {
|
||||||
store.remove(key);
|
store.remove(key);
|
||||||
}
|
}
|
||||||
|
@ -40,11 +40,13 @@ function clearTags() {
|
||||||
*/
|
*/
|
||||||
function isValidStoredTag(value) {
|
function isValidStoredTag(value) {
|
||||||
if (value !== null && 'id' in value && 'name' in value && 'images' in value && 'spoiler_image_uri' in value) {
|
if (value !== null && 'id' in value && 'name' in value && 'images' in value && 'spoiler_image_uri' in value) {
|
||||||
return typeof value.id === 'number'
|
return (
|
||||||
&& typeof value.name === 'string'
|
typeof value.id === 'number' &&
|
||||||
&& typeof value.images === 'number'
|
typeof value.name === 'string' &&
|
||||||
&& (value.spoiler_image_uri === null || typeof value.spoiler_image_uri === 'string')
|
typeof value.images === 'number' &&
|
||||||
&& (value.fetchedAt === null || typeof value.fetchedAt === 'number');
|
(value.spoiler_image_uri === null || typeof value.spoiler_image_uri === 'string') &&
|
||||||
|
(value.fetchedAt === null || typeof value.fetchedAt === 'number')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -82,8 +84,8 @@ function fetchAndPersistTags(tagIds) {
|
||||||
const remaining = tagIds.slice(41);
|
const remaining = tagIds.slice(41);
|
||||||
|
|
||||||
fetch(`/fetch/tags?ids[]=${ids.join('&ids[]=')}`)
|
fetch(`/fetch/tags?ids[]=${ids.join('&ids[]=')}`)
|
||||||
.then(response => response.json())
|
.then((response) => response.json())
|
||||||
.then(data => data.tags.forEach(tag => persistTag(tag)))
|
.then((data) => data.tags.forEach((tag) => persistTag(tag)))
|
||||||
.then(() => fetchAndPersistTags(remaining));
|
.then(() => fetchAndPersistTags(remaining));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +96,7 @@ function fetchAndPersistTags(tagIds) {
|
||||||
function fetchNewOrStaleTags(tagIds) {
|
function fetchNewOrStaleTags(tagIds) {
|
||||||
const fetchIds = [];
|
const fetchIds = [];
|
||||||
|
|
||||||
tagIds.forEach(t => {
|
tagIds.forEach((t) => {
|
||||||
const stored = store.get(`bor_tags_${t}`);
|
const stored = store.get(`bor_tags_${t}`);
|
||||||
if (!stored || isStale(stored)) {
|
if (!stored || isStale(stored)) {
|
||||||
fetchIds.push(t);
|
fetchIds.push(t);
|
||||||
|
@ -112,17 +114,18 @@ function verifyTagsVersion(latest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeFilters() {
|
function initializeFilters() {
|
||||||
const tags = window.booru.spoileredTagList
|
const tags = window.booru.spoileredTagList.concat(window.booru.hiddenTagList).filter((a, b, c) => c.indexOf(a) === b);
|
||||||
.concat(window.booru.hiddenTagList)
|
|
||||||
.filter((a, b, c) => c.indexOf(a) === b);
|
|
||||||
|
|
||||||
verifyTagsVersion(window.booru.tagsVersion);
|
verifyTagsVersion(window.booru.tagsVersion);
|
||||||
fetchNewOrStaleTags(tags);
|
fetchNewOrStaleTags(tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
function unmarshal(data) {
|
function unmarshal(data) {
|
||||||
try { return JSON.parse(data); }
|
try {
|
||||||
catch { return data; }
|
return JSON.parse(data);
|
||||||
|
} catch {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadBooruData() {
|
function loadBooruData() {
|
||||||
|
|
|
@ -11,40 +11,72 @@ import { addTag } from './tagsinput';
|
||||||
|
|
||||||
// Event types and any qualifying conditions - return true to not run action
|
// Event types and any qualifying conditions - return true to not run action
|
||||||
const types = {
|
const types = {
|
||||||
click(event) { return event.button !== 0; /* Left-click only */ },
|
click(event) {
|
||||||
|
return event.button !== 0; /* Left-click only */
|
||||||
|
},
|
||||||
|
|
||||||
change() { /* No qualifier */ },
|
change() {
|
||||||
|
/* No qualifier */
|
||||||
|
},
|
||||||
|
|
||||||
fetchcomplete() { /* No qualifier */ },
|
fetchcomplete() {
|
||||||
|
/* No qualifier */
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
hide(data) { selectorCb(data.base, data.value, el => el.classList.add('hidden')); },
|
hide(data) {
|
||||||
|
selectorCb(data.base, data.value, (el) => el.classList.add('hidden'));
|
||||||
|
},
|
||||||
|
|
||||||
tabHide(data) { selectorCbChildren(data.base, data.value, el => el.classList.add('hidden')); },
|
tabHide(data) {
|
||||||
|
selectorCbChildren(data.base, data.value, (el) => el.classList.add('hidden'));
|
||||||
|
},
|
||||||
|
|
||||||
show(data) { selectorCb(data.base, data.value, el => el.classList.remove('hidden')); },
|
show(data) {
|
||||||
|
selectorCb(data.base, data.value, (el) => el.classList.remove('hidden'));
|
||||||
|
},
|
||||||
|
|
||||||
toggle(data) { selectorCb(data.base, data.value, el => el.classList.toggle('hidden')); },
|
toggle(data) {
|
||||||
|
selectorCb(data.base, data.value, (el) => el.classList.toggle('hidden'));
|
||||||
|
},
|
||||||
|
|
||||||
submit(data) { selectorCb(data.base, data.value, el => el.submit()); },
|
submit(data) {
|
||||||
|
selectorCb(data.base, data.value, (el) => el.submit());
|
||||||
|
},
|
||||||
|
|
||||||
disable(data) { selectorCb(data.base, data.value, el => el.disabled = true); },
|
disable(data) {
|
||||||
|
selectorCb(data.base, data.value, (el) => {
|
||||||
|
el.disabled = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
copy(data) {
|
copy(data) {
|
||||||
document.querySelector(data.value).select();
|
document.querySelector(data.value).select();
|
||||||
document.execCommand('copy');
|
document.execCommand('copy');
|
||||||
},
|
},
|
||||||
|
|
||||||
inputvalue(data) { document.querySelector(data.value).value = data.el.dataset.setValue; },
|
inputvalue(data) {
|
||||||
|
document.querySelector(data.value).value = data.el.dataset.setValue;
|
||||||
|
},
|
||||||
|
|
||||||
selectvalue(data) { document.querySelector(data.value).value = data.el.querySelector(':checked').dataset.setValue; },
|
selectvalue(data) {
|
||||||
|
document.querySelector(data.value).value = data.el.querySelector(':checked').dataset.setValue;
|
||||||
|
},
|
||||||
|
|
||||||
checkall(data) { $$(`${data.value} input[type=checkbox]`).forEach(c => { c.checked = !c.checked; }); },
|
checkall(data) {
|
||||||
|
$$(`${data.value} input[type=checkbox]`).forEach((c) => {
|
||||||
|
c.checked = !c.checked;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
focus(data) { document.querySelector(data.value).focus(); },
|
focus(data) {
|
||||||
|
document.querySelector(data.value).focus();
|
||||||
|
},
|
||||||
|
|
||||||
preventdefault() { /* The existence of this entry is enough */ },
|
preventdefault() {
|
||||||
|
/* The existence of this entry is enough */
|
||||||
|
},
|
||||||
|
|
||||||
addtag(data) {
|
addtag(data) {
|
||||||
addTag(document.querySelector(data.el.closest('[data-target]').dataset.target), data.el.dataset.tagName);
|
addTag(document.querySelector(data.el.closest('[data-target]').dataset.target), data.el.dataset.tagName);
|
||||||
|
@ -70,16 +102,22 @@ const actions = {
|
||||||
if (loadTab && !newTab.dataset.loaded) {
|
if (loadTab && !newTab.dataset.loaded) {
|
||||||
fetchHtml(loadTab)
|
fetchHtml(loadTab)
|
||||||
.then(handleError)
|
.then(handleError)
|
||||||
.then(response => response.text())
|
.then((response) => response.text())
|
||||||
.then(response => newTab.innerHTML = response)
|
.then((response) => {
|
||||||
.then(() => newTab.dataset.loaded = true)
|
newTab.innerHTML = response;
|
||||||
.catch(() => newTab.textContent = 'Error!');
|
})
|
||||||
|
.then(() => {
|
||||||
|
newTab.dataset.loaded = true;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
newTab.textContent = 'Error!';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
unfilter(data) { showBlock(data.el.closest('.image-show-container')); },
|
unfilter(data) {
|
||||||
|
showBlock(data.el.closest('.image-show-container'));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use this function to apply a callback to elements matching the selectors
|
// Use this function to apply a callback to elements matching the selectors
|
||||||
|
@ -100,7 +138,6 @@ function selectorCbChildren(base = document, selector, cb) {
|
||||||
function matchAttributes(event) {
|
function matchAttributes(event) {
|
||||||
if (!types[event.type](event)) {
|
if (!types[event.type](event)) {
|
||||||
for (const action in actions) {
|
for (const action in actions) {
|
||||||
|
|
||||||
const attr = `data-${event.type}-${action.toLowerCase()}`,
|
const attr = `data-${event.type}-${action.toLowerCase()}`,
|
||||||
el = event.target && event.target.closest(`[${attr}]`),
|
el = event.target && event.target.closest(`[${attr}]`),
|
||||||
value = el && el.getAttribute(attr);
|
value = el && el.getAttribute(attr);
|
||||||
|
@ -109,7 +146,6 @@ function matchAttributes(event) {
|
||||||
// Return true if you don't want to preventDefault
|
// Return true if you don't want to preventDefault
|
||||||
actions[action]({ attr, el, value }) || event.preventDefault();
|
actions[action]({ attr, el, value }) || event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ function copyUserLinksTo(burger: HTMLElement) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$$<HTMLElement>('.js-burger-links').forEach(container => copy(container.children));
|
$$<HTMLElement>('.js-burger-links').forEach((container) => copy(container.children));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupBurgerMenu() {
|
export function setupBurgerMenu() {
|
||||||
|
@ -53,14 +53,13 @@ export function setupBurgerMenu() {
|
||||||
|
|
||||||
copyUserLinksTo(burger);
|
copyUserLinksTo(burger);
|
||||||
|
|
||||||
toggle.addEventListener('click', event => {
|
toggle.addEventListener('click', (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (content.classList.contains('open')) {
|
if (content.classList.contains('open')) {
|
||||||
close(burger, content, body, root);
|
close(burger, content, body, root);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
open(burger, content, body, root);
|
open(burger, content, body, root);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { clearEl, makeEl } from './utils/dom';
|
||||||
function insertCaptcha(_event: Event, target: HTMLInputElement) {
|
function insertCaptcha(_event: Event, target: HTMLInputElement) {
|
||||||
const parentElement = assertNotNull(target.parentElement);
|
const parentElement = assertNotNull(target.parentElement);
|
||||||
|
|
||||||
const script = makeEl('script', {src: 'https://hcaptcha.com/1/api.js', async: true, defer: true});
|
const script = makeEl('script', { src: 'https://hcaptcha.com/1/api.js', async: true, defer: true });
|
||||||
const frame = makeEl('div', {className: 'h-captcha'});
|
const frame = makeEl('div', { className: 'h-captcha' });
|
||||||
|
|
||||||
frame.dataset.sitekey = target.dataset.sitekey;
|
frame.dataset.sitekey = target.dataset.sitekey;
|
||||||
|
|
||||||
|
@ -17,5 +17,5 @@ function insertCaptcha(_event: Event, target: HTMLInputElement) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bindCaptchaLinks() {
|
export function bindCaptchaLinks() {
|
||||||
delegate(document, 'click', {'.js-captcha': leftClick(insertCaptcha)});
|
delegate(document, 'click', { '.js-captcha': leftClick(insertCaptcha) });
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,16 +27,14 @@ function commentPosted(response) {
|
||||||
commentEditForm.reset();
|
commentEditForm.reset();
|
||||||
|
|
||||||
if (requestOk) {
|
if (requestOk) {
|
||||||
response.text().then(text => {
|
response.text().then((text) => {
|
||||||
if (text.includes('<div class="flash flash--warning">')) {
|
if (text.includes('<div class="flash flash--warning">')) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
displayComments(container, text);
|
displayComments(container, text);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
window.scrollTo(0, 0); // Error message is displayed at the top of the page (flash)
|
window.scrollTo(0, 0); // Error message is displayed at the top of the page (flash)
|
||||||
}
|
}
|
||||||
|
@ -58,11 +56,11 @@ function loadParentPost(event) {
|
||||||
|
|
||||||
if (commentMatches) {
|
if (commentMatches) {
|
||||||
// If the regex matched, get the image and comment ID
|
// If the regex matched, get the image and comment ID
|
||||||
const [ , imageId, commentId ] = commentMatches;
|
const [, imageId, commentId] = commentMatches;
|
||||||
|
|
||||||
fetchHtml(`/images/${imageId}/comments/${commentId}`)
|
fetchHtml(`/images/${imageId}/comments/${commentId}`)
|
||||||
.then(handleError)
|
.then(handleError)
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
clearParentPost(clickedLink, fullComment);
|
clearParentPost(clickedLink, fullComment);
|
||||||
insertParentPost(data, clickedLink, fullComment);
|
insertParentPost(data, clickedLink, fullComment);
|
||||||
});
|
});
|
||||||
|
@ -99,7 +97,7 @@ function clearParentPost(clickedLink, fullComment) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove class active_reply_link from all links in the comment
|
// Remove class active_reply_link from all links in the comment
|
||||||
[].slice.call(fullComment.getElementsByClassName('active_reply_link')).forEach(link => {
|
[].slice.call(fullComment.getElementsByClassName('active_reply_link')).forEach((link) => {
|
||||||
link.classList.remove('active_reply_link');
|
link.classList.remove('active_reply_link');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -123,12 +121,15 @@ function loadComments(event) {
|
||||||
const container = document.getElementById('comments'),
|
const container = document.getElementById('comments'),
|
||||||
hasHref = event.target && event.target.getAttribute('href'),
|
hasHref = event.target && event.target.getAttribute('href'),
|
||||||
hasHash = window.location.hash && window.location.hash.match(/#comment_([a-f0-9]+)/),
|
hasHash = window.location.hash && window.location.hash.match(/#comment_([a-f0-9]+)/),
|
||||||
getURL = hasHref || (hasHash ? `${container.dataset.currentUrl}?comment_id=${window.location.hash.substring(9, window.location.hash.length)}`
|
getURL =
|
||||||
|
hasHref ||
|
||||||
|
(hasHash
|
||||||
|
? `${container.dataset.currentUrl}?comment_id=${window.location.hash.substring(9, window.location.hash.length)}`
|
||||||
: container.dataset.currentUrl);
|
: container.dataset.currentUrl);
|
||||||
|
|
||||||
fetchHtml(getURL)
|
fetchHtml(getURL)
|
||||||
.then(handleError)
|
.then(handleError)
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
displayComments(container, data);
|
displayComments(container, data);
|
||||||
|
|
||||||
// Make sure the :target CSS selector applies to the inserted content
|
// Make sure the :target CSS selector applies to the inserted content
|
||||||
|
@ -152,8 +153,7 @@ function setupComments() {
|
||||||
if (!comments.dataset.loaded || !targetOnPage) {
|
if (!comments.dataset.loaded || !targetOnPage) {
|
||||||
// There is no event associated with the initial load, so use false
|
// There is no event associated with the initial load, so use false
|
||||||
loadComments(false);
|
loadComments(false);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
filterNode(comments);
|
filterNode(comments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -165,8 +165,9 @@ function setupComments() {
|
||||||
'#js-refresh-comments': loadComments,
|
'#js-refresh-comments': loadComments,
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('click', event => {
|
document.addEventListener('click', (event) => {
|
||||||
if (event.button === 0) { // Left-click only
|
if (event.button === 0) {
|
||||||
|
// Left-click only
|
||||||
for (const target in targets) {
|
for (const target in targets) {
|
||||||
if (event.target && event.target.closest(target)) {
|
if (event.target && event.target.closest(target)) {
|
||||||
targets[target](event) && event.preventDefault();
|
targets[target](event) && event.preventDefault();
|
||||||
|
@ -175,7 +176,7 @@ function setupComments() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('fetchcomplete', event => {
|
document.addEventListener('fetchcomplete', (event) => {
|
||||||
if (event.target.id === 'js-comment-form') commentPosted(event.detail);
|
if (event.target.id === 'js-comment-form') commentPosted(event.detail);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export function setupDupeReports() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupSwipe(swipe: SVGSVGElement) {
|
function setupSwipe(swipe: SVGSVGElement) {
|
||||||
const [ clip, divider ] = $$<SVGRectElement>('#clip rect, #divider', swipe);
|
const [clip, divider] = $$<SVGRectElement>('#clip rect, #divider', swipe);
|
||||||
const { width } = swipe.viewBox.baseVal;
|
const { width } = swipe.viewBox.baseVal;
|
||||||
|
|
||||||
function moveDivider({ clientX }: MouseEvent) {
|
function moveDivider({ clientX }: MouseEvent) {
|
||||||
|
|
|
@ -11,19 +11,19 @@ const storageKey = 'cached_ses_value';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Keyboard {
|
interface Keyboard {
|
||||||
getLayoutMap: () => Promise<Map<string, string>>
|
getLayoutMap: () => Promise<Map<string, string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserAgentData {
|
interface UserAgentData {
|
||||||
brands: [{brand: string, version: string}],
|
brands: [{ brand: string; version: string }];
|
||||||
mobile: boolean,
|
mobile: boolean;
|
||||||
platform: string,
|
platform: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Navigator {
|
interface Navigator {
|
||||||
deviceMemory: number | undefined,
|
deviceMemory: number | undefined;
|
||||||
keyboard: Keyboard | undefined,
|
keyboard: Keyboard | undefined;
|
||||||
userAgentData: UserAgentData | undefined,
|
userAgentData: UserAgentData | undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,10 +45,10 @@ function cyrb53(str: string, seed: number = 0x16fe7b0a): number {
|
||||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
|
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
||||||
h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||||
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
|
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
||||||
h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||||
|
|
||||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||||
}
|
}
|
||||||
|
@ -102,8 +102,8 @@ function getUserAgentBrands(): string {
|
||||||
// NB: Chromium implements GREASE protocol to prevent ossification of
|
// NB: Chromium implements GREASE protocol to prevent ossification of
|
||||||
// the "Not a brand" string - see https://stackoverflow.com/a/64443187
|
// the "Not a brand" string - see https://stackoverflow.com/a/64443187
|
||||||
brands = data.brands
|
brands = data.brands
|
||||||
.filter(e => !e.brand.match(/.*ot.*rand.*/gi))
|
.filter((e) => !e.brand.match(/.*ot.*rand.*/gi))
|
||||||
.map(e => `${e.brand}${e.version}`)
|
.map((e) => `${e.brand}${e.version}`)
|
||||||
.sort()
|
.sort()
|
||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
@ -161,9 +161,7 @@ async function createFp(): Promise<string> {
|
||||||
new Date().getTimezoneOffset().toString(),
|
new Date().getTimezoneOffset().toString(),
|
||||||
];
|
];
|
||||||
|
|
||||||
return cyrb53(prints.join(''))
|
return cyrb53(prints.join('')).toString(16).padStart(14, '0');
|
||||||
.toString(16)
|
|
||||||
.padStart(14, '0');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { fetchJson } from './utils/requests';
|
||||||
export function setupGalleryEditing() {
|
export function setupGalleryEditing() {
|
||||||
if (!$<HTMLElement>('.rearrange-button')) return;
|
if (!$<HTMLElement>('.rearrange-button')) return;
|
||||||
|
|
||||||
const [ rearrangeEl, saveEl ] = $$<HTMLElement>('.rearrange-button');
|
const [rearrangeEl, saveEl] = $$<HTMLElement>('.rearrange-button');
|
||||||
const sortableEl = assertNotNull($<HTMLDivElement>('#sortable'));
|
const sortableEl = assertNotNull($<HTMLDivElement>('#sortable'));
|
||||||
const containerEl = assertNotNull($<HTMLDivElement>('.js-resizable-media-container'));
|
const containerEl = assertNotNull($<HTMLDivElement>('.js-resizable-media-container'));
|
||||||
|
|
||||||
|
@ -22,7 +22,9 @@ export function setupGalleryEditing() {
|
||||||
|
|
||||||
initDraggables();
|
initDraggables();
|
||||||
|
|
||||||
$$<HTMLDivElement>('.media-box', containerEl).forEach(i => i.draggable = true);
|
$$<HTMLDivElement>('.media-box', containerEl).forEach((i) => {
|
||||||
|
i.draggable = true;
|
||||||
|
});
|
||||||
|
|
||||||
rearrangeEl.addEventListener('click', () => {
|
rearrangeEl.addEventListener('click', () => {
|
||||||
sortableEl.classList.add('editing');
|
sortableEl.classList.add('editing');
|
||||||
|
@ -33,8 +35,9 @@ export function setupGalleryEditing() {
|
||||||
sortableEl.classList.remove('editing');
|
sortableEl.classList.remove('editing');
|
||||||
containerEl.classList.remove('drag-container');
|
containerEl.classList.remove('drag-container');
|
||||||
|
|
||||||
newImages = $$<HTMLDivElement>('.image-container', containerEl)
|
newImages = $$<HTMLDivElement>('.image-container', containerEl).map((i) =>
|
||||||
.map(i => parseInt(assertNotUndefined(i.dataset.imageId), 10));
|
parseInt(assertNotUndefined(i.dataset.imageId), 10),
|
||||||
|
);
|
||||||
|
|
||||||
// If nothing changed, don't bother.
|
// If nothing changed, don't bother.
|
||||||
if (arraysEqual(newImages, oldImages)) return;
|
if (arraysEqual(newImages, oldImages)) return;
|
||||||
|
@ -44,6 +47,8 @@ export function setupGalleryEditing() {
|
||||||
fetchJson('PATCH', reorderPath, {
|
fetchJson('PATCH', reorderPath, {
|
||||||
image_ids: newImages,
|
image_ids: newImages,
|
||||||
// copy the array again so that we have the newly updated set
|
// copy the array again so that we have the newly updated set
|
||||||
}).then(() => oldImages = newImages.slice());
|
}).then(() => {
|
||||||
|
oldImages = newImages.slice();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ function graphSlice(el: SVGSVGElement, width: number, offset: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function resizeGraphs() {
|
function resizeGraphs() {
|
||||||
$$<SVGSVGElement>('#js-graph-svg').forEach(el => {
|
$$<SVGSVGElement>('#js-graph-svg').forEach((el) => {
|
||||||
const parent: HTMLElement | null = el.parentElement;
|
const parent: HTMLElement | null = el.parentElement;
|
||||||
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
|
@ -34,7 +34,7 @@ function resizeGraphs() {
|
||||||
function scaleGraph(target: HTMLElement, min: number, max: number) {
|
function scaleGraph(target: HTMLElement, min: number, max: number) {
|
||||||
const targetSvg = $<SVGSVGElement>('#js-graph-svg', target);
|
const targetSvg = $<SVGSVGElement>('#js-graph-svg', target);
|
||||||
|
|
||||||
if (!targetSvg) { return; }
|
if (!targetSvg) return;
|
||||||
|
|
||||||
const cw = target.clientWidth;
|
const cw = target.clientWidth;
|
||||||
const diff = 100 - (max - min);
|
const diff = 100 - (max - min);
|
||||||
|
@ -47,14 +47,14 @@ function scaleGraph(target: HTMLElement, min: number, max: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupSliders() {
|
function setupSliders() {
|
||||||
$$<HTMLInputElement>('#js-graph-slider').forEach(el => {
|
$$<HTMLInputElement>('#js-graph-slider').forEach((el) => {
|
||||||
const targetId = el.getAttribute('data-target');
|
const targetId = el.getAttribute('data-target');
|
||||||
|
|
||||||
if (!targetId) { return; }
|
if (!targetId) return;
|
||||||
|
|
||||||
const target = $<HTMLElement>(targetId);
|
const target = $<HTMLElement>(targetId);
|
||||||
|
|
||||||
if (!target) { return; }
|
if (!target) return;
|
||||||
|
|
||||||
el.addEventListener('input', () => {
|
el.addEventListener('input', () => {
|
||||||
const min = Number(el.getAttribute('valuemin') || '0');
|
const min = Number(el.getAttribute('valuemin') || '0');
|
||||||
|
|
|
@ -5,7 +5,7 @@ const imageVersions = {
|
||||||
// [width, height]
|
// [width, height]
|
||||||
small: [320, 240],
|
small: [320, 240],
|
||||||
medium: [800, 600],
|
medium: [800, 600],
|
||||||
large: [1280, 1024]
|
large: [1280, 1024],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,7 +91,8 @@ function pickAndResize(elem) {
|
||||||
|
|
||||||
if (imageFormat === 'mp4') {
|
if (imageFormat === 'mp4') {
|
||||||
elem.classList.add('full-height');
|
elem.classList.add('full-height');
|
||||||
elem.insertAdjacentHTML('afterbegin',
|
elem.insertAdjacentHTML(
|
||||||
|
'afterbegin',
|
||||||
`<video controls ${autoplay} loop ${muted} playsinline preload="auto" id="image-display"
|
`<video controls ${autoplay} loop ${muted} playsinline preload="auto" id="image-display"
|
||||||
width="${imageWidth}" height="${imageHeight}">
|
width="${imageWidth}" height="${imageHeight}">
|
||||||
<source src="${uris.webm}" type="video/webm">
|
<source src="${uris.webm}" type="video/webm">
|
||||||
|
@ -100,11 +101,11 @@ function pickAndResize(elem) {
|
||||||
Your browser supports neither MP4/H264 nor
|
Your browser supports neither MP4/H264 nor
|
||||||
WebM/VP8! Please update it to the latest version.
|
WebM/VP8! Please update it to the latest version.
|
||||||
</p>
|
</p>
|
||||||
</video>`
|
</video>`,
|
||||||
);
|
);
|
||||||
}
|
} else if (imageFormat === 'webm') {
|
||||||
else if (imageFormat === 'webm') {
|
elem.insertAdjacentHTML(
|
||||||
elem.insertAdjacentHTML('afterbegin',
|
'afterbegin',
|
||||||
`<video controls ${autoplay} loop ${muted} playsinline id="image-display">
|
`<video controls ${autoplay} loop ${muted} playsinline id="image-display">
|
||||||
<source src="${uri}" type="video/webm">
|
<source src="${uri}" type="video/webm">
|
||||||
<source src="${uri.replace(/webm$/, 'mp4')}" type="video/mp4">
|
<source src="${uri.replace(/webm$/, 'mp4')}" type="video/mp4">
|
||||||
|
@ -112,25 +113,21 @@ function pickAndResize(elem) {
|
||||||
Your browser supports neither MP4/H264 nor
|
Your browser supports neither MP4/H264 nor
|
||||||
WebM/VP8! Please update it to the latest version.
|
WebM/VP8! Please update it to the latest version.
|
||||||
</p>
|
</p>
|
||||||
</video>`
|
</video>`,
|
||||||
);
|
);
|
||||||
const video = elem.querySelector('video');
|
const video = elem.querySelector('video');
|
||||||
if (scaled === 'true') {
|
if (scaled === 'true') {
|
||||||
video.className = 'image-scaled';
|
video.className = 'image-scaled';
|
||||||
}
|
} else if (scaled === 'partscaled') {
|
||||||
else if (scaled === 'partscaled') {
|
|
||||||
video.className = 'image-partscaled';
|
video.className = 'image-partscaled';
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
let image;
|
let image;
|
||||||
if (scaled === 'true') {
|
if (scaled === 'true') {
|
||||||
image = `<picture><img id="image-display" src="${uri}" class="image-scaled"></picture>`;
|
image = `<picture><img id="image-display" src="${uri}" class="image-scaled"></picture>`;
|
||||||
}
|
} else if (scaled === 'partscaled') {
|
||||||
else if (scaled === 'partscaled') {
|
|
||||||
image = `<picture><img id="image-display" src="${uri}" class="image-partscaled"></picture>`;
|
image = `<picture><img id="image-display" src="${uri}" class="image-partscaled"></picture>`;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
image = `<picture><img id="image-display" src="${uri}" width="${imageWidth}" height="${imageHeight}"></picture>`;
|
image = `<picture><img id="image-display" src="${uri}" width="${imageWidth}" height="${imageHeight}"></picture>`;
|
||||||
}
|
}
|
||||||
if (elem.innerHTML === image) return;
|
if (elem.innerHTML === image) return;
|
||||||
|
@ -148,11 +145,9 @@ function bindImageForClick(target) {
|
||||||
target.addEventListener('click', () => {
|
target.addEventListener('click', () => {
|
||||||
if (target.getAttribute('data-scaled') === 'true') {
|
if (target.getAttribute('data-scaled') === 'true') {
|
||||||
target.setAttribute('data-scaled', 'partscaled');
|
target.setAttribute('data-scaled', 'partscaled');
|
||||||
}
|
} else if (target.getAttribute('data-scaled') === 'partscaled') {
|
||||||
else if (target.getAttribute('data-scaled') === 'partscaled') {
|
|
||||||
target.setAttribute('data-scaled', 'false');
|
target.setAttribute('data-scaled', 'false');
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
target.setAttribute('data-scaled', 'true');
|
target.setAttribute('data-scaled', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +156,7 @@ function bindImageForClick(target) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindImageTarget(node = document) {
|
function bindImageTarget(node = document) {
|
||||||
$$('.image-target', node).forEach(target => {
|
$$('.image-target', node).forEach((target) => {
|
||||||
pickAndResize(target);
|
pickAndResize(target);
|
||||||
|
|
||||||
if (target.dataset.mimeType === 'video/webm') {
|
if (target.dataset.mimeType === 'video/webm') {
|
||||||
|
|
|
@ -12,12 +12,7 @@ import { AstMatcher } from './query/types';
|
||||||
type CallbackType = 'tags' | 'complex';
|
type CallbackType = 'tags' | 'complex';
|
||||||
type RunCallback = (img: HTMLDivElement, tags: TagData[], type: CallbackType) => void;
|
type RunCallback = (img: HTMLDivElement, tags: TagData[], type: CallbackType) => void;
|
||||||
|
|
||||||
function run(
|
function run(img: HTMLDivElement, tags: TagData[], complex: AstMatcher, runCallback: RunCallback): boolean {
|
||||||
img: HTMLDivElement,
|
|
||||||
tags: TagData[],
|
|
||||||
complex: AstMatcher,
|
|
||||||
runCallback: RunCallback
|
|
||||||
): boolean {
|
|
||||||
const hit = (() => {
|
const hit = (() => {
|
||||||
// Check tags array first to provide more precise filter explanations
|
// Check tags array first to provide more precise filter explanations
|
||||||
const hitTags = imageHitsTags(img, tags);
|
const hitTags = imageHitsTags(img, tags);
|
||||||
|
@ -56,49 +51,48 @@ function bannerImage(tagsHit: TagData[]) {
|
||||||
// TODO: this approach is not suitable for translations because it depends on
|
// TODO: this approach is not suitable for translations because it depends on
|
||||||
// markup embedded in the page adjacent to this text
|
// markup embedded in the page adjacent to this text
|
||||||
|
|
||||||
/* eslint-disable indent */
|
|
||||||
|
|
||||||
function hideThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
function hideThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
||||||
const bannerText = type === 'tags' ? `[HIDDEN] ${displayTags(tagsHit)}`
|
const bannerText = type === 'tags' ? `[HIDDEN] ${displayTags(tagsHit)}` : '[HIDDEN] <i>(Complex Filter)</i>';
|
||||||
: '[HIDDEN] <i>(Complex Filter)</i>';
|
|
||||||
hideThumb(img, bannerImage(tagsHit), bannerText);
|
hideThumb(img, bannerImage(tagsHit), bannerText);
|
||||||
}
|
}
|
||||||
|
|
||||||
function spoilerThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
function spoilerThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
||||||
const bannerText = type === 'tags' ? displayTags(tagsHit)
|
const bannerText = type === 'tags' ? displayTags(tagsHit) : '<i>(Complex Filter)</i>';
|
||||||
: '<i>(Complex Filter)</i>';
|
|
||||||
spoilerThumb(img, bannerImage(tagsHit), bannerText);
|
spoilerThumb(img, bannerImage(tagsHit), bannerText);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
function hideBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
||||||
const bannerText = type === 'tags' ? `This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is hidden by `
|
const bannerText =
|
||||||
|
type === 'tags'
|
||||||
|
? `This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is hidden by `
|
||||||
: 'This image was hidden by a complex tag expression in ';
|
: 'This image was hidden by a complex tag expression in ';
|
||||||
spoilerBlock(img, bannerImage(tagsHit), bannerText);
|
spoilerBlock(img, bannerImage(tagsHit), bannerText);
|
||||||
}
|
}
|
||||||
|
|
||||||
function spoilerBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
function spoilerBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
||||||
const bannerText = type === 'tags' ? `This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is spoilered by `
|
const bannerText =
|
||||||
|
type === 'tags'
|
||||||
|
? `This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is spoilered by `
|
||||||
: 'This image was spoilered by a complex tag expression in ';
|
: 'This image was spoilered by a complex tag expression in ';
|
||||||
spoilerBlock(img, bannerImage(tagsHit), bannerText);
|
spoilerBlock(img, bannerImage(tagsHit), bannerText);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-enable indent */
|
|
||||||
|
|
||||||
export function filterNode(node: Pick<Document, 'querySelectorAll'>) {
|
export function filterNode(node: Pick<Document, 'querySelectorAll'>) {
|
||||||
const hiddenTags = getHiddenTags(), spoileredTags = getSpoileredTags();
|
const hiddenTags = getHiddenTags(),
|
||||||
|
spoileredTags = getSpoileredTags();
|
||||||
const { hiddenFilter, spoileredFilter } = window.booru;
|
const { hiddenFilter, spoileredFilter } = window.booru;
|
||||||
|
|
||||||
// Image thumb boxes with vote and fave buttons on them
|
// Image thumb boxes with vote and fave buttons on them
|
||||||
$$<HTMLDivElement>('.image-container', node)
|
$$<HTMLDivElement>('.image-container', node)
|
||||||
.filter(img => !run(img, hiddenTags, hiddenFilter, hideThumbTyped))
|
.filter((img) => !run(img, hiddenTags, hiddenFilter, hideThumbTyped))
|
||||||
.filter(img => !run(img, spoileredTags, spoileredFilter, spoilerThumbTyped))
|
.filter((img) => !run(img, spoileredTags, spoileredFilter, spoilerThumbTyped))
|
||||||
.forEach(img => showThumb(img));
|
.forEach((img) => showThumb(img));
|
||||||
|
|
||||||
// Individual image pages and images in posts/comments
|
// Individual image pages and images in posts/comments
|
||||||
$$<HTMLDivElement>('.image-show-container', node)
|
$$<HTMLDivElement>('.image-show-container', node)
|
||||||
.filter(img => !run(img, hiddenTags, hiddenFilter, hideBlockTyped))
|
.filter((img) => !run(img, hiddenTags, hiddenFilter, hideBlockTyped))
|
||||||
.filter(img => !run(img, spoileredTags, spoileredFilter, spoilerBlockTyped))
|
.filter((img) => !run(img, spoileredTags, spoileredFilter, spoilerBlockTyped))
|
||||||
.forEach(img => showBlock(img));
|
.forEach((img) => showBlock(img));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initImagesClientside() {
|
export function initImagesClientside() {
|
||||||
|
|
|
@ -13,7 +13,7 @@ export function inputDuplicatorCreator({
|
||||||
addButtonSelector,
|
addButtonSelector,
|
||||||
fieldSelector,
|
fieldSelector,
|
||||||
maxInputCountSelector,
|
maxInputCountSelector,
|
||||||
removeButtonSelector
|
removeButtonSelector,
|
||||||
}: InputDuplicatorOptions) {
|
}: InputDuplicatorOptions) {
|
||||||
const addButton = $<HTMLButtonElement>(addButtonSelector);
|
const addButton = $<HTMLButtonElement>(addButtonSelector);
|
||||||
if (!addButton) {
|
if (!addButton) {
|
||||||
|
@ -35,14 +35,13 @@ export function inputDuplicatorCreator({
|
||||||
};
|
};
|
||||||
|
|
||||||
delegate(form, 'click', {
|
delegate(form, 'click', {
|
||||||
[removeButtonSelector]: leftClick(fieldRemover)
|
[removeButtonSelector]: leftClick(fieldRemover),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const maxOptionCountElement = assertNotNull($(maxInputCountSelector, form));
|
const maxOptionCountElement = assertNotNull($(maxInputCountSelector, form));
|
||||||
const maxOptionCount = parseInt(maxOptionCountElement.innerHTML, 10);
|
const maxOptionCount = parseInt(maxOptionCountElement.innerHTML, 10);
|
||||||
|
|
||||||
addButton.addEventListener('click', e => {
|
addButton.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const existingFields = $$<HTMLElement>(fieldSelector, form);
|
const existingFields = $$<HTMLElement>(fieldSelector, form);
|
||||||
|
@ -53,7 +52,7 @@ export function inputDuplicatorCreator({
|
||||||
const prevField = existingFields[existingFieldsLength - 1];
|
const prevField = existingFields[existingFieldsLength - 1];
|
||||||
const prevFieldCopy = prevField.cloneNode(true) as HTMLElement;
|
const prevFieldCopy = prevField.cloneNode(true) as HTMLElement;
|
||||||
|
|
||||||
$$<HTMLInputElement>('input', prevFieldCopy).forEach(prevFieldCopyInput => {
|
$$<HTMLInputElement>('input', prevFieldCopy).forEach((prevFieldCopyInput) => {
|
||||||
// Reset new input's value
|
// Reset new input's value
|
||||||
prevFieldCopyInput.value = '';
|
prevFieldCopyInput.value = '';
|
||||||
prevFieldCopyInput.removeAttribute('value');
|
prevFieldCopyInput.removeAttribute('value');
|
||||||
|
|
|
@ -6,18 +6,22 @@ import { fetchJson } from './utils/requests';
|
||||||
import { $ } from './utils/dom';
|
import { $ } from './utils/dom';
|
||||||
|
|
||||||
const endpoints = {
|
const endpoints = {
|
||||||
vote(imageId) { return `/images/${imageId}/vote`; },
|
vote(imageId) {
|
||||||
fave(imageId) { return `/images/${imageId}/fave`; },
|
return `/images/${imageId}/vote`;
|
||||||
hide(imageId) { return `/images/${imageId}/hide`; },
|
},
|
||||||
|
fave(imageId) {
|
||||||
|
return `/images/${imageId}/fave`;
|
||||||
|
},
|
||||||
|
hide(imageId) {
|
||||||
|
return `/images/${imageId}/hide`;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const spoilerDownvoteMsg =
|
const spoilerDownvoteMsg = 'Neigh! - Remove spoilered tags from your filters to downvote from thumbnails';
|
||||||
'Neigh! - Remove spoilered tags from your filters to downvote from thumbnails';
|
|
||||||
|
|
||||||
/* Quick helper function to less verbosely iterate a QSA */
|
/* Quick helper function to less verbosely iterate a QSA */
|
||||||
function onImage(id, selector, cb) {
|
function onImage(id, selector, cb) {
|
||||||
[].forEach.call(
|
[].forEach.call(document.querySelectorAll(`${selector}[data-image-id="${id}"]`), cb);
|
||||||
document.querySelectorAll(`${selector}[data-image-id="${id}"]`), cb);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Since JS modifications to webpages, except form inputs, are not stored
|
/* Since JS modifications to webpages, except form inputs, are not stored
|
||||||
|
@ -35,28 +39,35 @@ function modifyCache(callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cacheStatus(imageId, interactionType, value) {
|
function cacheStatus(imageId, interactionType, value) {
|
||||||
modifyCache(cache => {
|
modifyCache((cache) => {
|
||||||
cache[`${imageId}${interactionType}`] = { imageId, interactionType, value };
|
cache[`${imageId}${interactionType}`] = { imageId, interactionType, value };
|
||||||
return cache;
|
return cache;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function uncacheStatus(imageId, interactionType) {
|
function uncacheStatus(imageId, interactionType) {
|
||||||
modifyCache(cache => {
|
modifyCache((cache) => {
|
||||||
delete cache[`${imageId}${interactionType}`];
|
delete cache[`${imageId}${interactionType}`];
|
||||||
return cache;
|
return cache;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setScore(imageId, data) {
|
function setScore(imageId, data) {
|
||||||
onImage(imageId, '.score',
|
onImage(imageId, '.score', (el) => {
|
||||||
el => el.textContent = data.score);
|
el.textContent = data.score;
|
||||||
onImage(imageId, '.favorites',
|
});
|
||||||
el => el.textContent = data.faves);
|
|
||||||
onImage(imageId, '.upvotes',
|
onImage(imageId, '.favorites', (el) => {
|
||||||
el => el.textContent = data.upvotes);
|
el.textContent = data.faves;
|
||||||
onImage(imageId, '.downvotes',
|
});
|
||||||
el => el.textContent = data.downvotes);
|
|
||||||
|
onImage(imageId, '.upvotes', (el) => {
|
||||||
|
el.textContent = data.upvotes;
|
||||||
|
});
|
||||||
|
|
||||||
|
onImage(imageId, '.downvotes', (el) => {
|
||||||
|
el.textContent = data.downvotes;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* These change the visual appearance of interaction links.
|
/* These change the visual appearance of interaction links.
|
||||||
|
@ -64,58 +75,50 @@ function setScore(imageId, data) {
|
||||||
|
|
||||||
function showUpvoted(imageId) {
|
function showUpvoted(imageId) {
|
||||||
cacheStatus(imageId, 'voted', 'up');
|
cacheStatus(imageId, 'voted', 'up');
|
||||||
onImage(imageId, '.interaction--upvote',
|
onImage(imageId, '.interaction--upvote', (el) => el.classList.add('active'));
|
||||||
el => el.classList.add('active'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDownvoted(imageId) {
|
function showDownvoted(imageId) {
|
||||||
cacheStatus(imageId, 'voted', 'down');
|
cacheStatus(imageId, 'voted', 'down');
|
||||||
onImage(imageId, '.interaction--downvote',
|
onImage(imageId, '.interaction--downvote', (el) => el.classList.add('active'));
|
||||||
el => el.classList.add('active'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showFaved(imageId) {
|
function showFaved(imageId) {
|
||||||
cacheStatus(imageId, 'faved', '');
|
cacheStatus(imageId, 'faved', '');
|
||||||
onImage(imageId, '.interaction--fave',
|
onImage(imageId, '.interaction--fave', (el) => el.classList.add('active'));
|
||||||
el => el.classList.add('active'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showHidden(imageId) {
|
function showHidden(imageId) {
|
||||||
cacheStatus(imageId, 'hidden', '');
|
cacheStatus(imageId, 'hidden', '');
|
||||||
onImage(imageId, '.interaction--hide',
|
onImage(imageId, '.interaction--hide', (el) => el.classList.add('active'));
|
||||||
el => el.classList.add('active'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetVoted(imageId) {
|
function resetVoted(imageId) {
|
||||||
uncacheStatus(imageId, 'voted');
|
uncacheStatus(imageId, 'voted');
|
||||||
|
|
||||||
onImage(imageId, '.interaction--upvote',
|
onImage(imageId, '.interaction--upvote', (el) => el.classList.remove('active'));
|
||||||
el => el.classList.remove('active'));
|
|
||||||
|
|
||||||
onImage(imageId, '.interaction--downvote',
|
onImage(imageId, '.interaction--downvote', (el) => el.classList.remove('active'));
|
||||||
el => el.classList.remove('active'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetFaved(imageId) {
|
function resetFaved(imageId) {
|
||||||
uncacheStatus(imageId, 'faved');
|
uncacheStatus(imageId, 'faved');
|
||||||
onImage(imageId, '.interaction--fave',
|
onImage(imageId, '.interaction--fave', (el) => el.classList.remove('active'));
|
||||||
el => el.classList.remove('active'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetHidden(imageId) {
|
function resetHidden(imageId) {
|
||||||
uncacheStatus(imageId, 'hidden');
|
uncacheStatus(imageId, 'hidden');
|
||||||
onImage(imageId, '.interaction--hide',
|
onImage(imageId, '.interaction--hide', (el) => el.classList.remove('active'));
|
||||||
el => el.classList.remove('active'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function interact(type, imageId, method, data = {}) {
|
function interact(type, imageId, method, data = {}) {
|
||||||
return fetchJson(method, endpoints[type](imageId), data)
|
return fetchJson(method, endpoints[type](imageId), data)
|
||||||
.then(res => res.json())
|
.then((res) => res.json())
|
||||||
.then(res => setScore(imageId, res));
|
.then((res) => setScore(imageId, res));
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayInteractionSet(interactions) {
|
function displayInteractionSet(interactions) {
|
||||||
interactions.forEach(i => {
|
interactions.forEach((i) => {
|
||||||
switch (i.interaction_type) {
|
switch (i.interaction_type) {
|
||||||
case 'faved':
|
case 'faved':
|
||||||
showFaved(i.image_id);
|
showFaved(i.image_id);
|
||||||
|
@ -131,7 +134,6 @@ function displayInteractionSet(interactions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadInteractions() {
|
function loadInteractions() {
|
||||||
|
|
||||||
/* Set up the actual interactions */
|
/* Set up the actual interactions */
|
||||||
displayInteractionSet(window.booru.interactions);
|
displayInteractionSet(window.booru.interactions);
|
||||||
displayInteractionSet(getCache());
|
displayInteractionSet(getCache());
|
||||||
|
@ -141,68 +143,71 @@ function loadInteractions() {
|
||||||
if (!document.getElementById('imagelist-container')) return;
|
if (!document.getElementById('imagelist-container')) return;
|
||||||
|
|
||||||
/* Users will blind downvote without this */
|
/* Users will blind downvote without this */
|
||||||
window.booru.imagesWithDownvotingDisabled.forEach(i => {
|
window.booru.imagesWithDownvotingDisabled.forEach((i) => {
|
||||||
onImage(i, '.interaction--downvote', a => {
|
onImage(i, '.interaction--downvote', (a) => {
|
||||||
|
|
||||||
// TODO Use a 'js-' class to target these instead
|
// TODO Use a 'js-' class to target these instead
|
||||||
const icon = a.querySelector('i') || a.querySelector('.oc-icon-small');
|
const icon = a.querySelector('i') || a.querySelector('.oc-icon-small');
|
||||||
|
|
||||||
icon.setAttribute('title', spoilerDownvoteMsg);
|
icon.setAttribute('title', spoilerDownvoteMsg);
|
||||||
a.classList.add('disabled');
|
a.classList.add('disabled');
|
||||||
a.addEventListener('click', event => {
|
a.addEventListener(
|
||||||
|
'click',
|
||||||
|
(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}, true);
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const targets = {
|
const targets = {
|
||||||
|
|
||||||
/* Active-state targets first */
|
/* Active-state targets first */
|
||||||
'.interaction--upvote.active'(imageId) {
|
'.interaction--upvote.active'(imageId) {
|
||||||
interact('vote', imageId, 'DELETE')
|
interact('vote', imageId, 'DELETE').then(() => resetVoted(imageId));
|
||||||
.then(() => resetVoted(imageId));
|
|
||||||
},
|
},
|
||||||
'.interaction--downvote.active'(imageId) {
|
'.interaction--downvote.active'(imageId) {
|
||||||
interact('vote', imageId, 'DELETE')
|
interact('vote', imageId, 'DELETE').then(() => resetVoted(imageId));
|
||||||
.then(() => resetVoted(imageId));
|
|
||||||
},
|
},
|
||||||
'.interaction--fave.active'(imageId) {
|
'.interaction--fave.active'(imageId) {
|
||||||
interact('fave', imageId, 'DELETE')
|
interact('fave', imageId, 'DELETE').then(() => resetFaved(imageId));
|
||||||
.then(() => resetFaved(imageId));
|
|
||||||
},
|
},
|
||||||
'.interaction--hide.active'(imageId) {
|
'.interaction--hide.active'(imageId) {
|
||||||
interact('hide', imageId, 'DELETE')
|
interact('hide', imageId, 'DELETE').then(() => resetHidden(imageId));
|
||||||
.then(() => resetHidden(imageId));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Inactive targets */
|
/* Inactive targets */
|
||||||
'.interaction--upvote:not(.active)'(imageId) {
|
'.interaction--upvote:not(.active)'(imageId) {
|
||||||
interact('vote', imageId, 'POST', { up: true })
|
interact('vote', imageId, 'POST', { up: true }).then(() => {
|
||||||
.then(() => { resetVoted(imageId); showUpvoted(imageId); });
|
resetVoted(imageId);
|
||||||
|
showUpvoted(imageId);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
'.interaction--downvote:not(.active)'(imageId) {
|
'.interaction--downvote:not(.active)'(imageId) {
|
||||||
interact('vote', imageId, 'POST', { up: false })
|
interact('vote', imageId, 'POST', { up: false }).then(() => {
|
||||||
.then(() => { resetVoted(imageId); showDownvoted(imageId); });
|
resetVoted(imageId);
|
||||||
|
showDownvoted(imageId);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
'.interaction--fave:not(.active)'(imageId) {
|
'.interaction--fave:not(.active)'(imageId) {
|
||||||
interact('fave', imageId, 'POST')
|
interact('fave', imageId, 'POST').then(() => {
|
||||||
.then(() => { resetVoted(imageId); showFaved(imageId); showUpvoted(imageId); });
|
resetVoted(imageId);
|
||||||
|
showFaved(imageId);
|
||||||
|
showUpvoted(imageId);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
'.interaction--hide:not(.active)'(imageId) {
|
'.interaction--hide:not(.active)'(imageId) {
|
||||||
interact('hide', imageId, 'POST')
|
interact('hide', imageId, 'POST').then(() => {
|
||||||
.then(() => { showHidden(imageId); });
|
showHidden(imageId);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function bindInteractions() {
|
function bindInteractions() {
|
||||||
document.addEventListener('click', event => {
|
document.addEventListener('click', (event) => {
|
||||||
|
if (event.button === 0) {
|
||||||
if (event.button === 0) { // Is it a left-click?
|
// Is it a left-click?
|
||||||
for (const target in targets) {
|
for (const target in targets) {
|
||||||
/* Event delegation doesn't quite grab what we want here. */
|
/* Event delegation doesn't quite grab what we want here. */
|
||||||
const link = event.target && event.target.closest(target);
|
const link = event.target && event.target.closest(target);
|
||||||
|
@ -213,21 +218,20 @@ function bindInteractions() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loggedOutInteractions() {
|
function loggedOutInteractions() {
|
||||||
[].forEach.call(document.querySelectorAll('.interaction--fave,.interaction--upvote,.interaction--downvote'),
|
[].forEach.call(document.querySelectorAll('.interaction--fave,.interaction--upvote,.interaction--downvote'), (a) =>
|
||||||
a => a.setAttribute('href', '/sessions/new'));
|
a.setAttribute('href', '/sessions/new'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupInteractions() {
|
function setupInteractions() {
|
||||||
if (window.booru.userIsSignedIn) {
|
if (window.booru.userIsSignedIn) {
|
||||||
bindInteractions();
|
bindInteractions();
|
||||||
loadInteractions();
|
loadInteractions();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
loggedOutInteractions();
|
loggedOutInteractions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,19 +7,19 @@ import { $, $$ } from './utils/dom';
|
||||||
const markdownSyntax = {
|
const markdownSyntax = {
|
||||||
bold: {
|
bold: {
|
||||||
action: wrapSelection,
|
action: wrapSelection,
|
||||||
options: { prefix: '**', shortcutKey: 'b' }
|
options: { prefix: '**', shortcutKey: 'b' },
|
||||||
},
|
},
|
||||||
italics: {
|
italics: {
|
||||||
action: wrapSelection,
|
action: wrapSelection,
|
||||||
options: { prefix: '*', shortcutKey: 'i' }
|
options: { prefix: '*', shortcutKey: 'i' },
|
||||||
},
|
},
|
||||||
under: {
|
under: {
|
||||||
action: wrapSelection,
|
action: wrapSelection,
|
||||||
options: { prefix: '__', shortcutKey: 'u' }
|
options: { prefix: '__', shortcutKey: 'u' },
|
||||||
},
|
},
|
||||||
spoiler: {
|
spoiler: {
|
||||||
action: wrapSelection,
|
action: wrapSelection,
|
||||||
options: { prefix: '||', shortcutKey: 's' }
|
options: { prefix: '||', shortcutKey: 's' },
|
||||||
},
|
},
|
||||||
code: {
|
code: {
|
||||||
action: wrapSelectionOrLines,
|
action: wrapSelectionOrLines,
|
||||||
|
@ -29,37 +29,37 @@ const markdownSyntax = {
|
||||||
prefixMultiline: '```\n',
|
prefixMultiline: '```\n',
|
||||||
suffixMultiline: '\n```',
|
suffixMultiline: '\n```',
|
||||||
singleWrap: true,
|
singleWrap: true,
|
||||||
shortcutKey: 'e'
|
shortcutKey: 'e',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
strike: {
|
strike: {
|
||||||
action: wrapSelection,
|
action: wrapSelection,
|
||||||
options: { prefix: '~~' }
|
options: { prefix: '~~' },
|
||||||
},
|
},
|
||||||
superscript: {
|
superscript: {
|
||||||
action: wrapSelection,
|
action: wrapSelection,
|
||||||
options: { prefix: '^' }
|
options: { prefix: '^' },
|
||||||
},
|
},
|
||||||
subscript: {
|
subscript: {
|
||||||
action: wrapSelection,
|
action: wrapSelection,
|
||||||
options: { prefix: '%' }
|
options: { prefix: '%' },
|
||||||
},
|
},
|
||||||
quote: {
|
quote: {
|
||||||
action: wrapLines,
|
action: wrapLines,
|
||||||
options: { prefix: '> ' }
|
options: { prefix: '> ' },
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
action: insertLink,
|
action: insertLink,
|
||||||
options: { shortcutKey: 'l' }
|
options: { shortcutKey: 'l' },
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
action: insertLink,
|
action: insertLink,
|
||||||
options: { image: true, shortcutKey: 'k' }
|
options: { image: true, shortcutKey: 'k' },
|
||||||
},
|
},
|
||||||
escape: {
|
escape: {
|
||||||
action: escapeSelection,
|
action: escapeSelection,
|
||||||
options: { escapeChar: '\\' }
|
options: { escapeChar: '\\' },
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSelections(textarea, linesOnly = false) {
|
function getSelections(textarea, linesOnly = false) {
|
||||||
|
@ -78,8 +78,7 @@ function getSelections(textarea, linesOnly = false) {
|
||||||
const { lastIndex } = explorer;
|
const { lastIndex } = explorer;
|
||||||
if (lastIndex <= selectionStart) {
|
if (lastIndex <= selectionStart) {
|
||||||
startNewlineIndex = lastIndex;
|
startNewlineIndex = lastIndex;
|
||||||
}
|
} else if (lastIndex > selectionEnd) {
|
||||||
else if (lastIndex > selectionEnd) {
|
|
||||||
endNewlineIndex = lastIndex - 1;
|
endNewlineIndex = lastIndex - 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -96,8 +95,7 @@ function getSelections(textarea, linesOnly = false) {
|
||||||
}
|
}
|
||||||
selectionEnd = endNewlineIndex;
|
selectionEnd = endNewlineIndex;
|
||||||
selection = textarea.value.substring(selectionStart, selectionEnd);
|
selection = textarea.value.substring(selectionStart, selectionEnd);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// Deselect trailing space and line break
|
// Deselect trailing space and line break
|
||||||
for (caret = selection.length - 1; caret > 0; caret--) {
|
for (caret = selection.length - 1; caret > 0; caret--) {
|
||||||
if (selection[caret] !== ' ' && selection[caret] !== '\n') break;
|
if (selection[caret] !== ' ' && selection[caret] !== '\n') break;
|
||||||
|
@ -117,7 +115,7 @@ function getSelections(textarea, linesOnly = false) {
|
||||||
processLinesOnly,
|
processLinesOnly,
|
||||||
selectedText: selection,
|
selectedText: selection,
|
||||||
beforeSelection: textarea.value.substring(0, selectionStart) + leadingSpace,
|
beforeSelection: textarea.value.substring(0, selectionStart) + leadingSpace,
|
||||||
afterSelection: trailingSpace + textarea.value.substring(selectionEnd)
|
afterSelection: trailingSpace + textarea.value.substring(selectionEnd),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,7 +128,8 @@ function transformSelection(textarea, transformer, eachLine) {
|
||||||
|
|
||||||
textarea.value = beforeSelection + newText + afterSelection;
|
textarea.value = beforeSelection + newText + afterSelection;
|
||||||
|
|
||||||
const newSelectionStart = caretOffset >= 1
|
const newSelectionStart =
|
||||||
|
caretOffset >= 1
|
||||||
? beforeSelection.length + caretOffset
|
? beforeSelection.length + caretOffset
|
||||||
: textarea.value.length - afterSelection.length - caretOffset;
|
: textarea.value.length - afterSelection.length - caretOffset;
|
||||||
|
|
||||||
|
@ -157,13 +156,13 @@ function insertLink(textarea, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapSelection(textarea, options) {
|
function wrapSelection(textarea, options) {
|
||||||
transformSelection(textarea, selectedText => {
|
transformSelection(textarea, (selectedText) => {
|
||||||
const { text = selectedText, prefix = '', suffix = options.prefix } = options,
|
const { text = selectedText, prefix = '', suffix = options.prefix } = options,
|
||||||
emptyText = text === '';
|
emptyText = text === '';
|
||||||
let newText = text;
|
let newText = text;
|
||||||
|
|
||||||
if (!emptyText) {
|
if (!emptyText) {
|
||||||
newText = text.replace(/(\n{2,})/g, match => {
|
newText = text.replace(/(\n{2,})/g, (match) => {
|
||||||
return suffix + match + prefix;
|
return suffix + match + prefix;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -172,26 +171,33 @@ function wrapSelection(textarea, options) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newText,
|
newText,
|
||||||
caretOffset: emptyText ? prefix.length : newText.length
|
caretOffset: emptyText ? prefix.length : newText.length,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapLines(textarea, options, eachLine = true) {
|
function wrapLines(textarea, options, eachLine = true) {
|
||||||
transformSelection(textarea, (selectedText, processLinesOnly) => {
|
transformSelection(
|
||||||
|
textarea,
|
||||||
|
(selectedText, processLinesOnly) => {
|
||||||
const { text = selectedText, singleWrap = false } = options,
|
const { text = selectedText, singleWrap = false } = options,
|
||||||
prefix = (processLinesOnly && options.prefixMultiline) || options.prefix || '',
|
prefix = (processLinesOnly && options.prefixMultiline) || options.prefix || '',
|
||||||
suffix = (processLinesOnly && options.suffixMultiline) || options.suffix || '',
|
suffix = (processLinesOnly && options.suffixMultiline) || options.suffix || '',
|
||||||
emptyText = text === '';
|
emptyText = text === '';
|
||||||
let newText = singleWrap
|
let newText = singleWrap
|
||||||
? prefix + text.trim() + suffix
|
? prefix + text.trim() + suffix
|
||||||
: text.split(/\n/g).map(line => prefix + line.trim() + suffix).join('\n');
|
: text
|
||||||
|
.split(/\n/g)
|
||||||
|
.map((line) => prefix + line.trim() + suffix)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
// Force a space at the end of lines with only blockquote markers
|
// Force a space at the end of lines with only blockquote markers
|
||||||
newText = newText.replace(/^((?:>\s+)*)>$/gm, '$1> ');
|
newText = newText.replace(/^((?:>\s+)*)>$/gm, '$1> ');
|
||||||
|
|
||||||
return { newText, caretOffset: emptyText ? prefix.length : newText.length };
|
return { newText, caretOffset: emptyText ? prefix.length : newText.length };
|
||||||
}, eachLine);
|
},
|
||||||
|
eachLine,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapSelectionOrLines(textarea, options) {
|
function wrapSelectionOrLines(textarea, options) {
|
||||||
|
@ -199,7 +205,7 @@ function wrapSelectionOrLines(textarea, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeSelection(textarea, options) {
|
function escapeSelection(textarea, options) {
|
||||||
transformSelection(textarea, selectedText => {
|
transformSelection(textarea, (selectedText) => {
|
||||||
const { text = selectedText } = options,
|
const { text = selectedText } = options,
|
||||||
emptyText = text === '';
|
emptyText = text === '';
|
||||||
|
|
||||||
|
@ -209,7 +215,7 @@ function escapeSelection(textarea, options) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newText,
|
newText,
|
||||||
caretOffset: newText.length
|
caretOffset: newText.length,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -229,7 +235,14 @@ function clickHandler(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortcutHandler(event) {
|
function shortcutHandler(event) {
|
||||||
if (!event.ctrlKey || (window.navigator.platform === 'MacIntel' && !event.metaKey) || event.shiftKey || event.altKey) return;
|
if (
|
||||||
|
!event.ctrlKey ||
|
||||||
|
(window.navigator.platform === 'MacIntel' && !event.metaKey) ||
|
||||||
|
event.shiftKey ||
|
||||||
|
event.altKey
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const textarea = event.target,
|
const textarea = event.target,
|
||||||
key = event.key.toLowerCase();
|
key = event.key.toLowerCase();
|
||||||
|
|
||||||
|
@ -242,10 +255,10 @@ function shortcutHandler(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupToolbar() {
|
function setupToolbar() {
|
||||||
$$('.communication__toolbar').forEach(toolbar => {
|
$$('.communication__toolbar').forEach((toolbar) => {
|
||||||
toolbar.addEventListener('click', clickHandler);
|
toolbar.addEventListener('click', clickHandler);
|
||||||
});
|
});
|
||||||
$$('.js-toolbar-input').forEach(textarea => {
|
$$('.js-toolbar-input').forEach((textarea) => {
|
||||||
textarea.addEventListener('keydown', shortcutHandler);
|
textarea.addEventListener('keydown', shortcutHandler);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,10 @@ import '../types/ujs';
|
||||||
|
|
||||||
let touchMoved = false;
|
let touchMoved = false;
|
||||||
|
|
||||||
function formResult({target, detail}: FetchcompleteEvent) {
|
function formResult({ target, detail }: FetchcompleteEvent) {
|
||||||
const elements: Record<string, string> = {
|
const elements: Record<string, string> = {
|
||||||
'#description-form': '.image-description',
|
'#description-form': '.image-description',
|
||||||
'#uploader-form': '.image-uploader'
|
'#uploader-form': '.image-uploader',
|
||||||
};
|
};
|
||||||
|
|
||||||
function showResult(formEl: HTMLFormElement, resultEl: HTMLElement, response: string) {
|
function showResult(formEl: HTMLFormElement, resultEl: HTMLElement, response: string) {
|
||||||
|
@ -20,17 +20,17 @@ function formResult({target, detail}: FetchcompleteEvent) {
|
||||||
hideEl(formEl);
|
hideEl(formEl);
|
||||||
showEl(resultEl);
|
showEl(resultEl);
|
||||||
|
|
||||||
$$<HTMLInputElement | HTMLButtonElement>('input[type="submit"],button', formEl).forEach(button => {
|
$$<HTMLInputElement | HTMLButtonElement>('input[type="submit"],button', formEl).forEach((button) => {
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [ formSelector, resultSelector ] of Object.entries(elements)) {
|
for (const [formSelector, resultSelector] of Object.entries(elements)) {
|
||||||
if (target.matches(formSelector)) {
|
if (target.matches(formSelector)) {
|
||||||
const form = assertType(target, HTMLFormElement);
|
const form = assertType(target, HTMLFormElement);
|
||||||
const result = assertNotNull($<HTMLElement>(resultSelector));
|
const result = assertNotNull($<HTMLElement>(resultSelector));
|
||||||
|
|
||||||
detail.text().then(text => showResult(form, result, text));
|
detail.text().then((text) => showResult(form, result, text));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,11 +85,13 @@ export function setupEvents() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (store.get('hide_score')) {
|
if (store.get('hide_score')) {
|
||||||
$$<HTMLElement>('.upvotes,.score,.downvotes').forEach(s => hideEl(s));
|
$$<HTMLElement>('.upvotes,.score,.downvotes').forEach((s) => hideEl(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('fetchcomplete', formResult);
|
document.addEventListener('fetchcomplete', formResult);
|
||||||
document.addEventListener('click', revealSpoiler);
|
document.addEventListener('click', revealSpoiler);
|
||||||
document.addEventListener('touchend', revealSpoiler);
|
document.addEventListener('touchend', revealSpoiler);
|
||||||
document.addEventListener('touchmove', () => touchMoved = true);
|
document.addEventListener('touchmove', () => {
|
||||||
|
touchMoved = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,13 +13,13 @@ const NOTIFICATION_INTERVAL = 600000,
|
||||||
|
|
||||||
function bindSubscriptionLinks() {
|
function bindSubscriptionLinks() {
|
||||||
delegate(document, 'fetchcomplete', {
|
delegate(document, 'fetchcomplete', {
|
||||||
'.js-subscription-link': event => {
|
'.js-subscription-link': (event) => {
|
||||||
const target = assertNotNull(event.target.closest('.js-subscription-target'));
|
const target = assertNotNull(event.target.closest('.js-subscription-target'));
|
||||||
|
|
||||||
event.detail.text().then(text => {
|
event.detail.text().then((text) => {
|
||||||
target.outerHTML = text;
|
target.outerHTML = text;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ function getNewNotifications() {
|
||||||
|
|
||||||
fetchJson('GET', '/notifications/unread')
|
fetchJson('GET', '/notifications/unread')
|
||||||
.then(handleError)
|
.then(handleError)
|
||||||
.then(response => response.json())
|
.then((response) => response.json())
|
||||||
.then(({ notifications }) => {
|
.then(({ notifications }) => {
|
||||||
updateNotificationTicker(notifications);
|
updateNotificationTicker(notifications);
|
||||||
storeNotificationCount(notifications);
|
storeNotificationCount(notifications);
|
||||||
|
|
|
@ -18,8 +18,7 @@ export function warnAboutPMs() {
|
||||||
|
|
||||||
if (value.match(imageEmbedRegex)) {
|
if (value.match(imageEmbedRegex)) {
|
||||||
showEl(warning);
|
showEl(warning);
|
||||||
}
|
} else if (!warning.classList.contains('hidden')) {
|
||||||
else if (!warning.classList.contains('hidden')) {
|
|
||||||
hideEl(warning);
|
hideEl(warning);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,7 +47,7 @@ function getPreview(body, anonymous, previewLoading, previewIdle, previewContent
|
||||||
|
|
||||||
fetchJson('POST', path, { body, anonymous })
|
fetchJson('POST', path, { body, anonymous })
|
||||||
.then(handleError)
|
.then(handleError)
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
previewContent.innerHTML = data;
|
previewContent.innerHTML = data;
|
||||||
filterNode(previewContent);
|
filterNode(previewContent);
|
||||||
bindImageTarget(previewContent);
|
bindImageTarget(previewContent);
|
||||||
|
@ -110,13 +110,14 @@ function setupPreviews() {
|
||||||
// Fire handler for automatic resizing if textarea contains text on page load (e.g. editing)
|
// Fire handler for automatic resizing if textarea contains text on page load (e.g. editing)
|
||||||
if (textarea.value) textarea.dispatchEvent(new Event('change'));
|
if (textarea.value) textarea.dispatchEvent(new Event('change'));
|
||||||
|
|
||||||
previewAnon && previewAnon.addEventListener('click', () => {
|
previewAnon &&
|
||||||
|
previewAnon.addEventListener('click', () => {
|
||||||
if (previewContent.classList.contains('hidden')) return;
|
if (previewContent.classList.contains('hidden')) return;
|
||||||
|
|
||||||
updatePreview();
|
updatePreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('click', event => {
|
document.addEventListener('click', (event) => {
|
||||||
if (event.target && event.target.closest('.post-reply')) {
|
if (event.target && event.target.closest('.post-reply')) {
|
||||||
const link = event.target.closest('.post-reply');
|
const link = event.target.closest('.post-reply');
|
||||||
commentReply(link.dataset.author, link.getAttribute('href'), textarea, link.dataset.post);
|
commentReply(link.dataset.author, link.getAttribute('href'), textarea, link.dataset.post);
|
||||||
|
|
|
@ -97,7 +97,9 @@ describe('Date parsing', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not match malformed absolute date expressions', () => {
|
it('should not match malformed absolute date expressions', () => {
|
||||||
expect(() => makeDateMatcher('2024-06-21T06:21:30+01:3020', 'eq')).toThrow('Cannot parse date string: 2024-06-21T06:21:30+01:3020');
|
expect(() => makeDateMatcher('2024-06-21T06:21:30+01:3020', 'eq')).toThrow(
|
||||||
|
'Cannot parse date string: 2024-06-21T06:21:30+01:3020',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not match malformed relative date expressions', () => {
|
it('should not match malformed relative date expressions', () => {
|
||||||
|
|
|
@ -4,10 +4,10 @@ describe('User field parsing', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
window.booru.interactions = [
|
window.booru.interactions = [
|
||||||
{image_id: 0, user_id: 0, interaction_type: 'faved', value: null},
|
{ image_id: 0, user_id: 0, interaction_type: 'faved', value: null },
|
||||||
{image_id: 0, user_id: 0, interaction_type: 'voted', value: 'up'},
|
{ image_id: 0, user_id: 0, interaction_type: 'voted', value: 'up' },
|
||||||
{image_id: 1, user_id: 0, interaction_type: 'voted', value: 'down'},
|
{ image_id: 1, user_id: 0, interaction_type: 'voted', value: 'down' },
|
||||||
{image_id: 2, user_id: 0, interaction_type: 'hidden', value: null},
|
{ image_id: 2, user_id: 0, interaction_type: 'hidden', value: null },
|
||||||
];
|
];
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { AstMatcher } from './types';
|
import { AstMatcher } from './types';
|
||||||
|
|
||||||
export function matchAny(...matchers: AstMatcher[]): AstMatcher {
|
export function matchAny(...matchers: AstMatcher[]): AstMatcher {
|
||||||
return (e: HTMLElement) => matchers.some(matcher => matcher(e));
|
return (e: HTMLElement) => matchers.some((matcher) => matcher(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function matchAll(...matchers: AstMatcher[]): AstMatcher {
|
export function matchAll(...matchers: AstMatcher[]): AstMatcher {
|
||||||
return (e: HTMLElement) => matchers.every(matcher => matcher(e));
|
return (e: HTMLElement) => matchers.every((matcher) => matcher(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function matchNot(matcher: AstMatcher): AstMatcher {
|
export function matchNot(matcher: AstMatcher): AstMatcher {
|
||||||
|
|
|
@ -17,16 +17,16 @@ function makeMatcher(bottomDate: PosixTimeMs, topDate: PosixTimeMs, qual: RangeE
|
||||||
// done compared to numeric ranges.
|
// done compared to numeric ranges.
|
||||||
switch (qual) {
|
switch (qual) {
|
||||||
case 'lte':
|
case 'lte':
|
||||||
return v => new Date(v).getTime() < topDate;
|
return (v) => new Date(v).getTime() < topDate;
|
||||||
case 'gte':
|
case 'gte':
|
||||||
return v => new Date(v).getTime() >= bottomDate;
|
return (v) => new Date(v).getTime() >= bottomDate;
|
||||||
case 'lt':
|
case 'lt':
|
||||||
return v => new Date(v).getTime() < bottomDate;
|
return (v) => new Date(v).getTime() < bottomDate;
|
||||||
case 'gt':
|
case 'gt':
|
||||||
return v => new Date(v).getTime() >= topDate;
|
return (v) => new Date(v).getTime() >= topDate;
|
||||||
case 'eq':
|
case 'eq':
|
||||||
default:
|
default:
|
||||||
return v => {
|
return (v) => {
|
||||||
const t = new Date(v).getTime();
|
const t = new Date(v).getTime();
|
||||||
return t >= bottomDate && t < topDate;
|
return t >= bottomDate && t < topDate;
|
||||||
};
|
};
|
||||||
|
@ -44,7 +44,7 @@ function makeRelativeDateMatcher(dateVal: string, qual: RangeEqualQualifier): Fi
|
||||||
day: 86400000,
|
day: 86400000,
|
||||||
week: 604800000,
|
week: 604800000,
|
||||||
month: 2592000000,
|
month: 2592000000,
|
||||||
year: 31536000000
|
year: 31536000000,
|
||||||
};
|
};
|
||||||
|
|
||||||
const amount = parseInt(match[1], 10);
|
const amount = parseInt(match[1], 10);
|
||||||
|
@ -58,14 +58,7 @@ function makeRelativeDateMatcher(dateVal: string, qual: RangeEqualQualifier): Fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeAbsoluteDateMatcher(dateVal: string, qual: RangeEqualQualifier): FieldMatcher {
|
function makeAbsoluteDateMatcher(dateVal: string, qual: RangeEqualQualifier): FieldMatcher {
|
||||||
const parseRes: RegExp[] = [
|
const parseRes: RegExp[] = [/^(\d{4})/, /^-(\d{2})/, /^-(\d{2})/, /^(?:\s+|T|t)(\d{2})/, /^:(\d{2})/, /^:(\d{2})/];
|
||||||
/^(\d{4})/,
|
|
||||||
/^-(\d{2})/,
|
|
||||||
/^-(\d{2})/,
|
|
||||||
/^(?:\s+|T|t)(\d{2})/,
|
|
||||||
/^:(\d{2})/,
|
|
||||||
/^:(\d{2})/
|
|
||||||
];
|
|
||||||
const timeZoneOffset: TimeZoneOffset = [0, 0];
|
const timeZoneOffset: TimeZoneOffset = [0, 0];
|
||||||
const timeData: AbsoluteDate = [0, 0, 1, 0, 0, 0];
|
const timeData: AbsoluteDate = [0, 0, 1, 0, 0, 0];
|
||||||
|
|
||||||
|
@ -81,8 +74,7 @@ function makeAbsoluteDateMatcher(dateVal: string, qual: RangeEqualQualifier): Fi
|
||||||
timeZoneOffset[1] *= -1;
|
timeZoneOffset[1] *= -1;
|
||||||
}
|
}
|
||||||
localDateVal = localDateVal.substring(0, localDateVal.length - 6);
|
localDateVal = localDateVal.substring(0, localDateVal.length - 6);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
localDateVal = localDateVal.replace(/[Zz]$/, '');
|
localDateVal = localDateVal.replace(/[Zz]$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,16 +89,14 @@ function makeAbsoluteDateMatcher(dateVal: string, qual: RangeEqualQualifier): Fi
|
||||||
if (matchIndex === 1) {
|
if (matchIndex === 1) {
|
||||||
// Months are offset by 1.
|
// Months are offset by 1.
|
||||||
timeData[matchIndex] = parseInt(componentMatch[1], 10) - 1;
|
timeData[matchIndex] = parseInt(componentMatch[1], 10) - 1;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// All other components are not offset.
|
// All other components are not offset.
|
||||||
timeData[matchIndex] = parseInt(componentMatch[1], 10);
|
timeData[matchIndex] = parseInt(componentMatch[1], 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Truncate string.
|
// Truncate string.
|
||||||
localDateVal = localDateVal.substring(componentMatch[0].length);
|
localDateVal = localDateVal.substring(componentMatch[0].length);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
throw new ParseError(`Cannot parse date string: ${origDateVal}`);
|
throw new ParseError(`Cannot parse date string: ${origDateVal}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,23 @@ import { FieldName } from './types';
|
||||||
|
|
||||||
type AttributeName = string;
|
type AttributeName = string;
|
||||||
|
|
||||||
export const numberFields: FieldName[] =
|
export const numberFields: FieldName[] = [
|
||||||
['id', 'width', 'height', 'aspect_ratio',
|
'id',
|
||||||
'comment_count', 'score', 'upvotes', 'downvotes',
|
'width',
|
||||||
'faves', 'tag_count', 'score'];
|
'height',
|
||||||
|
'aspect_ratio',
|
||||||
|
'comment_count',
|
||||||
|
'score',
|
||||||
|
'upvotes',
|
||||||
|
'downvotes',
|
||||||
|
'faves',
|
||||||
|
'tag_count',
|
||||||
|
'score',
|
||||||
|
];
|
||||||
|
|
||||||
export const dateFields: FieldName[] = ['created_at'];
|
export const dateFields: FieldName[] = ['created_at'];
|
||||||
|
|
||||||
export const literalFields =
|
export const literalFields = ['tags', 'orig_sha512_hash', 'sha512_hash', 'uploader', 'source_url', 'description'];
|
||||||
['tags', 'orig_sha512_hash', 'sha512_hash',
|
|
||||||
'uploader', 'source_url', 'description'];
|
|
||||||
|
|
||||||
export const termSpaceToImageField: Record<FieldName, AttributeName> = {
|
export const termSpaceToImageField: Record<FieldName, AttributeName> = {
|
||||||
tags: 'data-image-tag-aliases',
|
tags: 'data-image-tag-aliases',
|
||||||
|
@ -32,7 +39,7 @@ export const termSpaceToImageField: Record<FieldName, AttributeName> = {
|
||||||
faves: 'data-faves',
|
faves: 'data-faves',
|
||||||
sha512_hash: 'data-sha512',
|
sha512_hash: 'data-sha512',
|
||||||
orig_sha512_hash: 'data-orig-sha512',
|
orig_sha512_hash: 'data-orig-sha512',
|
||||||
created_at: 'data-created-at'
|
created_at: 'data-created-at',
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ const tokenList: Token[] = [
|
||||||
['not_op', /^\s*[!-]\s*/],
|
['not_op', /^\s*[!-]\s*/],
|
||||||
['space', /^\s+/],
|
['space', /^\s+/],
|
||||||
['word', /^(?:\\[\s,()^~]|[^\s,()^~])+/],
|
['word', /^(?:\\[\s,()^~]|[^\s,()^~])+/],
|
||||||
['word', /^(?:\\[\s,()]|[^\s,()])+/]
|
['word', /^(?:\\[\s,()]|[^\s,()])+/],
|
||||||
];
|
];
|
||||||
|
|
||||||
export type ParseTerm = (term: string, fuzz: number, boost: number) => AstMatcher;
|
export type ParseTerm = (term: string, fuzz: number, boost: number) => AstMatcher;
|
||||||
|
@ -26,9 +26,9 @@ export type Range = [number, number];
|
||||||
export type TermContext = [Range, string];
|
export type TermContext = [Range, string];
|
||||||
|
|
||||||
export interface LexResult {
|
export interface LexResult {
|
||||||
tokenList: TokenList,
|
tokenList: TokenList;
|
||||||
termContexts: TermContext[],
|
termContexts: TermContext[];
|
||||||
error: ParseError | null
|
error: ParseError | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexResult {
|
export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexResult {
|
||||||
|
@ -49,7 +49,7 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR
|
||||||
const ret: LexResult = {
|
const ret: LexResult = {
|
||||||
tokenList: [],
|
tokenList: [],
|
||||||
termContexts: [],
|
termContexts: [],
|
||||||
error: null
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const beginTerm = (token: string) => {
|
const beginTerm = (token: string) => {
|
||||||
|
@ -86,7 +86,10 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR
|
||||||
|
|
||||||
const token = match[0];
|
const token = match[0];
|
||||||
|
|
||||||
if (searchTerm !== null && (['and_op', 'or_op'].indexOf(tokenName) !== -1 || tokenName === 'rparen' && lparenCtr === 0)) {
|
if (
|
||||||
|
searchTerm !== null &&
|
||||||
|
(['and_op', 'or_op'].indexOf(tokenName) !== -1 || (tokenName === 'rparen' && lparenCtr === 0))
|
||||||
|
) {
|
||||||
endTerm();
|
endTerm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,8 +110,7 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
// We're already inside a search term, so it does not apply, obv.
|
// We're already inside a search term, so it does not apply, obv.
|
||||||
searchTerm += token;
|
searchTerm += token;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
negate = !negate;
|
negate = !negate;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -118,8 +120,7 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR
|
||||||
// instead, consider it as part of the search term, as a user convenience.
|
// instead, consider it as part of the search term, as a user convenience.
|
||||||
searchTerm += token;
|
searchTerm += token;
|
||||||
lparenCtr += 1;
|
lparenCtr += 1;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
opQueue.unshift('lparen');
|
opQueue.unshift('lparen');
|
||||||
groupNegate.push(negate);
|
groupNegate.push(negate);
|
||||||
negate = false;
|
negate = false;
|
||||||
|
@ -129,8 +130,7 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR
|
||||||
if (lparenCtr > 0) {
|
if (lparenCtr > 0) {
|
||||||
searchTerm = assertNotNull(searchTerm) + token;
|
searchTerm = assertNotNull(searchTerm) + token;
|
||||||
lparenCtr -= 1;
|
lparenCtr -= 1;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
while (opQueue.length > 0) {
|
while (opQueue.length > 0) {
|
||||||
const op = assertNotUndefined(opQueue.shift());
|
const op = assertNotUndefined(opQueue.shift());
|
||||||
if (op === 'lparen') {
|
if (op === 'lparen') {
|
||||||
|
@ -149,8 +149,7 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR
|
||||||
// to a temporary string in case this is actually inside the term.
|
// to a temporary string in case this is actually inside the term.
|
||||||
fuzz = parseFloat(token.substring(1));
|
fuzz = parseFloat(token.substring(1));
|
||||||
boostFuzzStr += token;
|
boostFuzzStr += token;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
beginTerm(token);
|
beginTerm(token);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -158,16 +157,14 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
boost = parseFloat(token.substring(1));
|
boost = parseFloat(token.substring(1));
|
||||||
boostFuzzStr += token;
|
boostFuzzStr += token;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
beginTerm(token);
|
beginTerm(token);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'quoted_lit':
|
case 'quoted_lit':
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
searchTerm += token;
|
searchTerm += token;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
beginTerm(token);
|
beginTerm(token);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -180,8 +177,7 @@ export function generateLexResult(searchStr: string, parseTerm: ParseTerm): LexR
|
||||||
boostFuzzStr = '';
|
boostFuzzStr = '';
|
||||||
}
|
}
|
||||||
searchTerm += token;
|
searchTerm += token;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
beginTerm(token);
|
beginTerm(token);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -23,11 +23,13 @@ function makeWildcardMatcher(term: string): FieldMatcher {
|
||||||
// A custom NFA with caching may be more sophisticated but not
|
// A custom NFA with caching may be more sophisticated but not
|
||||||
// likely to be faster.
|
// likely to be faster.
|
||||||
const wildcard = new RegExp(
|
const wildcard = new RegExp(
|
||||||
`^${term.replace(/([.+^$[\]\\(){}|-])/g, '\\$1')
|
`^${term
|
||||||
|
.replace(/([.+^$[\]\\(){}|-])/g, '\\$1')
|
||||||
.replace(/([^\\]|[^\\](?:\\\\)+)\*/g, '$1.*')
|
.replace(/([^\\]|[^\\](?:\\\\)+)\*/g, '$1.*')
|
||||||
.replace(/^(?:\\\\)*\*/g, '.*')
|
.replace(/^(?:\\\\)*\*/g, '.*')
|
||||||
.replace(/([^\\]|[^\\](?:\\\\)+)\?/g, '$1.?')
|
.replace(/([^\\]|[^\\](?:\\\\)+)\?/g, '$1.?')
|
||||||
.replace(/^(?:\\\\)*\?/g, '.?')}$`, 'i'
|
.replace(/^(?:\\\\)*\?/g, '.?')}$`,
|
||||||
|
'i',
|
||||||
);
|
);
|
||||||
|
|
||||||
return (v, name) => {
|
return (v, name) => {
|
||||||
|
@ -69,10 +71,9 @@ function fuzzyMatch(term: string, targetStr: string, fuzz: number): boolean {
|
||||||
// Insertion.
|
// Insertion.
|
||||||
v2[j] + 1,
|
v2[j] + 1,
|
||||||
// Substitution or No Change.
|
// Substitution or No Change.
|
||||||
v1[j] + cost
|
v1[j] + cost,
|
||||||
);
|
);
|
||||||
if (i > 1 && j > 1 && term[i] === targetStrLower[j - 1] &&
|
if (i > 1 && j > 1 && term[i] === targetStrLower[j - 1] && targetStrLower[i - 1] === targetStrLower[j]) {
|
||||||
targetStrLower[i - 1] === targetStrLower[j]) {
|
|
||||||
v2[j + 1] = Math.min(v2[j], v0[j - 1] + cost);
|
v2[j + 1] = Math.min(v2[j], v0[j - 1] + cost);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,10 @@ import { makeUserMatcher } from './user';
|
||||||
import { FieldMatcher, RangeEqualQualifier } from './types';
|
import { FieldMatcher, RangeEqualQualifier } from './types';
|
||||||
|
|
||||||
export interface MatcherFactory {
|
export interface MatcherFactory {
|
||||||
makeDateMatcher: (dateVal: string, qual: RangeEqualQualifier) => FieldMatcher,
|
makeDateMatcher: (dateVal: string, qual: RangeEqualQualifier) => FieldMatcher;
|
||||||
makeLiteralMatcher: (term: string, fuzz: number, wildcardable: boolean) => FieldMatcher,
|
makeLiteralMatcher: (term: string, fuzz: number, wildcardable: boolean) => FieldMatcher;
|
||||||
makeNumberMatcher: (term: number, fuzz: number, qual: RangeEqualQualifier) => FieldMatcher,
|
makeNumberMatcher: (term: number, fuzz: number, qual: RangeEqualQualifier) => FieldMatcher;
|
||||||
makeUserMatcher: (term: string) => FieldMatcher
|
makeUserMatcher: (term: string) => FieldMatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultMatcher: MatcherFactory = {
|
export const defaultMatcher: MatcherFactory = {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { FieldMatcher, RangeEqualQualifier } from './types';
|
||||||
|
|
||||||
export function makeNumberMatcher(term: number, fuzz: number, qual: RangeEqualQualifier): FieldMatcher {
|
export function makeNumberMatcher(term: number, fuzz: number, qual: RangeEqualQualifier): FieldMatcher {
|
||||||
// Range matching.
|
// Range matching.
|
||||||
return v => {
|
return (v) => {
|
||||||
const attrVal = parseFloat(v);
|
const attrVal = parseFloat(v);
|
||||||
|
|
||||||
if (isNaN(attrVal)) {
|
if (isNaN(attrVal)) {
|
||||||
|
|
|
@ -23,19 +23,16 @@ export function parseTokens(lexicalArray: TokenList): AstMatcher {
|
||||||
|
|
||||||
if (token === 'and_op') {
|
if (token === 'and_op') {
|
||||||
intermediate = matchAll(op1, op2);
|
intermediate = matchAll(op1, op2);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
intermediate = matchAny(op1, op2);
|
intermediate = matchAny(op1, op2);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
intermediate = token;
|
intermediate = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lexicalArray[i + 1] === 'not_op') {
|
if (lexicalArray[i + 1] === 'not_op') {
|
||||||
operandStack.push(matchNot(intermediate));
|
operandStack.push(matchNot(intermediate));
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
operandStack.push(intermediate);
|
operandStack.push(intermediate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,11 +67,9 @@ function makeTermMatcher(term: string, fuzz: number, factory: MatcherFactory): [
|
||||||
}
|
}
|
||||||
|
|
||||||
return [fieldName, factory.makeNumberMatcher(parseFloat(termCandidate), fuzz, rangeType)];
|
return [fieldName, factory.makeNumberMatcher(parseFloat(termCandidate), fuzz, rangeType)];
|
||||||
}
|
} else if (literalFields.indexOf(candidateTermSpace) !== -1) {
|
||||||
else if (literalFields.indexOf(candidateTermSpace) !== -1) {
|
|
||||||
return [candidateTermSpace, factory.makeLiteralMatcher(termCandidate, fuzz, wildcardable)];
|
return [candidateTermSpace, factory.makeLiteralMatcher(termCandidate, fuzz, wildcardable)];
|
||||||
}
|
} else if (candidateTermSpace === 'my') {
|
||||||
else if (candidateTermSpace === 'my') {
|
|
||||||
return [candidateTermSpace, factory.makeUserMatcher(termCandidate)];
|
return [candidateTermSpace, factory.makeUserMatcher(termCandidate)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
import { Interaction, InteractionType, InteractionValue } from '../../types/booru-object';
|
import { Interaction, InteractionType, InteractionValue } from '../../types/booru-object';
|
||||||
import { FieldMatcher } from './types';
|
import { FieldMatcher } from './types';
|
||||||
|
|
||||||
function interactionMatch(imageId: number, type: InteractionType, value: InteractionValue, interactions: Interaction[]): boolean {
|
function interactionMatch(
|
||||||
return interactions.some(v => v.image_id === imageId && v.interaction_type === type && (value === null || v.value === value));
|
imageId: number,
|
||||||
|
type: InteractionType,
|
||||||
|
value: InteractionValue,
|
||||||
|
interactions: Interaction[],
|
||||||
|
): boolean {
|
||||||
|
return interactions.some(
|
||||||
|
(v) => v.image_id === imageId && v.interaction_type === type && (value === null || v.value === value),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeUserMatcher(term: string): FieldMatcher {
|
export function makeUserMatcher(term: string): FieldMatcher {
|
||||||
|
|
|
@ -9,56 +9,54 @@ import { fetchJson, handleError } from './utils/requests';
|
||||||
const imageQueueStorage = 'quickTagQueue';
|
const imageQueueStorage = 'quickTagQueue';
|
||||||
const currentTagStorage = 'quickTagName';
|
const currentTagStorage = 'quickTagName';
|
||||||
|
|
||||||
function currentQueue() { return store.get(imageQueueStorage) || []; }
|
function currentQueue() {
|
||||||
|
return store.get(imageQueueStorage) || [];
|
||||||
|
}
|
||||||
|
|
||||||
function currentTags() { return store.get(currentTagStorage) || ''; }
|
function currentTags() {
|
||||||
|
return store.get(currentTagStorage) || '';
|
||||||
|
}
|
||||||
|
|
||||||
function getTagButton() { return $('.js-quick-tag'); }
|
function getTagButton() {
|
||||||
|
return $('.js-quick-tag');
|
||||||
|
}
|
||||||
|
|
||||||
function setTagButton(text) { $('.js-quick-tag--submit span').textContent = text; }
|
function setTagButton(text) {
|
||||||
|
$('.js-quick-tag--submit span').textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
function toggleActiveState() {
|
function toggleActiveState() {
|
||||||
|
toggleEl($('.js-quick-tag'), $('.js-quick-tag--abort'), $('.js-quick-tag--all'), $('.js-quick-tag--submit'));
|
||||||
toggleEl($('.js-quick-tag'),
|
|
||||||
$('.js-quick-tag--abort'),
|
|
||||||
$('.js-quick-tag--all'),
|
|
||||||
$('.js-quick-tag--submit'));
|
|
||||||
|
|
||||||
setTagButton(`Submit (${currentTags()})`);
|
setTagButton(`Submit (${currentTags()})`);
|
||||||
|
|
||||||
$$('.media-box__header').forEach(el => el.classList.toggle('media-box__header--unselected'));
|
$$('.media-box__header').forEach((el) => el.classList.toggle('media-box__header--unselected'));
|
||||||
$$('.media-box__header').forEach(el => el.classList.remove('media-box__header--selected'));
|
$$('.media-box__header').forEach((el) => el.classList.remove('media-box__header--selected'));
|
||||||
currentQueue().forEach(id => $$(`.media-box__header[data-image-id="${id}"]`).forEach(el => el.classList.add('media-box__header--selected')));
|
currentQueue().forEach((id) =>
|
||||||
|
$$(`.media-box__header[data-image-id="${id}"]`).forEach((el) => el.classList.add('media-box__header--selected')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function activate() {
|
function activate() {
|
||||||
|
|
||||||
store.set(currentTagStorage, window.prompt('A comma-delimited list of tags you want to add:'));
|
store.set(currentTagStorage, window.prompt('A comma-delimited list of tags you want to add:'));
|
||||||
|
|
||||||
if (currentTags()) toggleActiveState();
|
if (currentTags()) toggleActiveState();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
|
|
||||||
store.remove(currentTagStorage);
|
store.remove(currentTagStorage);
|
||||||
store.remove(imageQueueStorage);
|
store.remove(imageQueueStorage);
|
||||||
|
|
||||||
toggleActiveState();
|
toggleActiveState();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function promptReset() {
|
function promptReset() {
|
||||||
|
|
||||||
if (window.confirm('Are you sure you want to abort batch tagging?')) {
|
if (window.confirm('Are you sure you want to abort batch tagging?')) {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
|
|
||||||
setTagButton(`Wait... (${currentTags()})`);
|
setTagButton(`Wait... (${currentTags()})`);
|
||||||
|
|
||||||
fetchJson('PUT', '/admin/batch/tags', {
|
fetchJson('PUT', '/admin/batch/tags', {
|
||||||
|
@ -66,32 +64,28 @@ function submit() {
|
||||||
image_ids: currentQueue(),
|
image_ids: currentQueue(),
|
||||||
})
|
})
|
||||||
.then(handleError)
|
.then(handleError)
|
||||||
.then(r => r.json())
|
.then((r) => r.json())
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
|
|
||||||
if (data.failed.length) window.alert(`Failed to add tags to the images with these IDs: ${data.failed}`);
|
if (data.failed.length) window.alert(`Failed to add tags to the images with these IDs: ${data.failed}`);
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function modifyImageQueue(mediaBox) {
|
function modifyImageQueue(mediaBox) {
|
||||||
|
|
||||||
if (currentTags()) {
|
if (currentTags()) {
|
||||||
const imageId = mediaBox.dataset.imageId,
|
const imageId = mediaBox.dataset.imageId,
|
||||||
queue = currentQueue(),
|
queue = currentQueue(),
|
||||||
isSelected = queue.includes(imageId);
|
isSelected = queue.includes(imageId);
|
||||||
|
|
||||||
isSelected ? queue.splice(queue.indexOf(imageId), 1)
|
isSelected ? queue.splice(queue.indexOf(imageId), 1) : queue.push(imageId);
|
||||||
: queue.push(imageId);
|
|
||||||
|
|
||||||
$$(`.media-box__header[data-image-id="${imageId}"]`).forEach(el => el.classList.toggle('media-box__header--selected'));
|
$$(`.media-box__header[data-image-id="${imageId}"]`).forEach((el) =>
|
||||||
|
el.classList.toggle('media-box__header--selected'),
|
||||||
|
);
|
||||||
|
|
||||||
store.set(imageQueueStorage, queue);
|
store.set(imageQueueStorage, queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAllImages() {
|
function toggleAllImages() {
|
||||||
|
@ -99,7 +93,6 @@ function toggleAllImages() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function clickHandler(event) {
|
function clickHandler(event) {
|
||||||
|
|
||||||
const targets = {
|
const targets = {
|
||||||
'.js-quick-tag': activate,
|
'.js-quick-tag': activate,
|
||||||
'.js-quick-tag--abort': promptReset,
|
'.js-quick-tag--abort': promptReset,
|
||||||
|
@ -114,14 +107,11 @@ function clickHandler(event) {
|
||||||
currentTags() && event.preventDefault();
|
currentTags() && event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupQuickTag() {
|
function setupQuickTag() {
|
||||||
|
|
||||||
if (getTagButton() && currentTags()) toggleActiveState();
|
if (getTagButton() && currentTags()) toggleActiveState();
|
||||||
if (getTagButton()) onLeftClick(clickHandler);
|
if (getTagButton()) onLeftClick(clickHandler);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { setupQuickTag };
|
export { setupQuickTag };
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { $, $$ } from './utils/dom';
|
||||||
import { addTag } from './tagsinput';
|
import { addTag } from './tagsinput';
|
||||||
|
|
||||||
function showHelp(subject: string, type: string | null) {
|
function showHelp(subject: string, type: string | null) {
|
||||||
$$<HTMLElement>('[data-search-help]').forEach(helpBox => {
|
$$<HTMLElement>('[data-search-help]').forEach((helpBox) => {
|
||||||
if (helpBox.getAttribute('data-search-help') === type) {
|
if (helpBox.getAttribute('data-search-help') === type) {
|
||||||
const searchSubject = $<HTMLElement>('.js-search-help-subject', helpBox);
|
const searchSubject = $<HTMLElement>('.js-search-help-subject', helpBox);
|
||||||
|
|
||||||
|
@ -11,8 +11,7 @@ function showHelp(subject: string, type: string | null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
helpBox.classList.remove('hidden');
|
helpBox.classList.remove('hidden');
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
helpBox.classList.add('hidden');
|
helpBox.classList.add('hidden');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -21,7 +20,8 @@ function showHelp(subject: string, type: string | null) {
|
||||||
function prependToLast(field: HTMLInputElement, value: string) {
|
function prependToLast(field: HTMLInputElement, value: string) {
|
||||||
const separatorIndex = field.value.lastIndexOf(',');
|
const separatorIndex = field.value.lastIndexOf(',');
|
||||||
const advanceBy = field.value[separatorIndex + 1] === ' ' ? 2 : 1;
|
const advanceBy = field.value[separatorIndex + 1] === ' ' ? 2 : 1;
|
||||||
field.value = field.value.slice(0, separatorIndex + advanceBy) + value + field.value.slice(separatorIndex + advanceBy);
|
field.value =
|
||||||
|
field.value.slice(0, separatorIndex + advanceBy) + value + field.value.slice(separatorIndex + advanceBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectLast(field: HTMLInputElement, characterCount: number) {
|
function selectLast(field: HTMLInputElement, characterCount: number) {
|
||||||
|
@ -32,14 +32,18 @@ function selectLast(field: HTMLInputElement, characterCount: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeFormHelper(e: PointerEvent) {
|
function executeFormHelper(e: PointerEvent) {
|
||||||
if (!e.target) { return; }
|
if (!e.target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const searchField = $<HTMLInputElement>('.js-search-field');
|
const searchField = $<HTMLInputElement>('.js-search-field');
|
||||||
const attr = (name: string) => e.target && (e.target as HTMLElement).getAttribute(name);
|
const attr = (name: string) => e.target && (e.target as HTMLElement).getAttribute(name);
|
||||||
|
|
||||||
if (attr('data-search-add')) addTag(searchField, attr('data-search-add'));
|
if (attr('data-search-add')) addTag(searchField, attr('data-search-add'));
|
||||||
if (attr('data-search-show-help')) showHelp((e.target as Node).textContent || '', attr('data-search-show-help'));
|
if (attr('data-search-show-help')) showHelp((e.target as Node).textContent || '', attr('data-search-show-help'));
|
||||||
if (attr('data-search-select-last') && searchField) selectLast(searchField, parseInt(attr('data-search-select-last') || '', 10));
|
if (attr('data-search-select-last') && searchField) {
|
||||||
|
selectLast(searchField, parseInt(attr('data-search-select-last') || '', 10));
|
||||||
|
}
|
||||||
if (attr('data-search-prepend') && searchField) prependToLast(searchField, attr('data-search-prepend') || '');
|
if (attr('data-search-prepend') && searchField) prependToLast(searchField, attr('data-search-prepend') || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,21 +7,18 @@ import { $, $$ } from './utils/dom';
|
||||||
import store from './utils/store';
|
import store from './utils/store';
|
||||||
|
|
||||||
export function setupSettings() {
|
export function setupSettings() {
|
||||||
|
|
||||||
if (!$('#js-setting-table')) return;
|
if (!$('#js-setting-table')) return;
|
||||||
|
|
||||||
const localCheckboxes = $$<HTMLInputElement>('[data-tab="local"] input[type="checkbox"]');
|
const localCheckboxes = $$<HTMLInputElement>('[data-tab="local"] input[type="checkbox"]');
|
||||||
const themeSelect = assertNotNull($<HTMLSelectElement>('#user_theme_name'));
|
const themeSelect = assertNotNull($<HTMLSelectElement>('#user_theme_name'));
|
||||||
const themeColorSelect = assertNotNull($<HTMLSelectElement>('#user_theme_color'));
|
const themeColorSelect = assertNotNull($<HTMLSelectElement>('#user_theme_color'));
|
||||||
const themePaths: Record<string, string> = JSON.parse(
|
const themePaths: Record<string, string> = JSON.parse(
|
||||||
assertNotUndefined(
|
assertNotUndefined(assertNotNull($<HTMLDivElement>('#js-theme-paths')).dataset.themePaths),
|
||||||
assertNotNull($<HTMLDivElement>('#js-theme-paths')).dataset.themePaths
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const styleSheet = assertNotNull($<HTMLLinkElement>('#js-theme-stylesheet'));
|
const styleSheet = assertNotNull($<HTMLLinkElement>('#js-theme-stylesheet'));
|
||||||
|
|
||||||
// Local settings
|
// Local settings
|
||||||
localCheckboxes.forEach(checkbox => {
|
localCheckboxes.forEach((checkbox) => {
|
||||||
checkbox.addEventListener('change', () => {
|
checkbox.addEventListener('change', () => {
|
||||||
store.set(checkbox.id.replace('user_', ''), checkbox.checked);
|
store.set(checkbox.id.replace('user_', ''), checkbox.checked);
|
||||||
});
|
});
|
||||||
|
|
|
@ -37,28 +37,48 @@ function click(selector: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOK(event: KeyboardEvent): boolean {
|
function isOK(event: KeyboardEvent): boolean {
|
||||||
return !event.altKey && !event.ctrlKey && !event.metaKey &&
|
return (
|
||||||
|
!event.altKey &&
|
||||||
|
!event.ctrlKey &&
|
||||||
|
!event.metaKey &&
|
||||||
document.activeElement !== null &&
|
document.activeElement !== null &&
|
||||||
document.activeElement.tagName !== 'INPUT' &&
|
document.activeElement.tagName !== 'INPUT' &&
|
||||||
document.activeElement.tagName !== 'TEXTAREA';
|
document.activeElement.tagName !== 'TEXTAREA'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyCodes: ShortcutKeyMap = {
|
const keyCodes: ShortcutKeyMap = {
|
||||||
'j'() { click('.js-prev'); }, // J - go to previous image
|
j() {
|
||||||
'i'() { click('.js-up'); }, // I - go to index page
|
click('.js-prev');
|
||||||
'k'() { click('.js-next'); }, // K - go to next image
|
}, // J - go to previous image
|
||||||
'r'() { click('.js-rand'); }, // R - go to random image
|
i() {
|
||||||
's'() { click('.js-source-link'); }, // S - go to image source
|
click('.js-up');
|
||||||
'l'() { click('.js-tag-sauce-toggle'); }, // L - edit tags
|
}, // I - go to index page
|
||||||
'o'() { openFullView(); }, // O - open original
|
k() {
|
||||||
'v'() { openFullViewNewTab(); }, // V - open original in a new tab
|
click('.js-next');
|
||||||
'f'() { // F - favourite image
|
}, // K - go to next image
|
||||||
click(getHover() ? `a.interaction--fave[data-image-id="${getHover()}"]`
|
r() {
|
||||||
: '.block__header a.interaction--fave');
|
click('.js-rand');
|
||||||
|
}, // R - go to random image
|
||||||
|
s() {
|
||||||
|
click('.js-source-link');
|
||||||
|
}, // S - go to image source
|
||||||
|
l() {
|
||||||
|
click('.js-tag-sauce-toggle');
|
||||||
|
}, // L - edit tags
|
||||||
|
o() {
|
||||||
|
openFullView();
|
||||||
|
}, // O - open original
|
||||||
|
v() {
|
||||||
|
openFullViewNewTab();
|
||||||
|
}, // V - open original in a new tab
|
||||||
|
f() {
|
||||||
|
// F - favourite image
|
||||||
|
click(getHover() ? `a.interaction--fave[data-image-id="${getHover()}"]` : '.block__header a.interaction--fave');
|
||||||
},
|
},
|
||||||
'u'() { // U - upvote image
|
u() {
|
||||||
click(getHover() ? `a.interaction--upvote[data-image-id="${getHover()}"]`
|
// U - upvote image
|
||||||
: '.block__header a.interaction--upvote');
|
click(getHover() ? `a.interaction--upvote[data-image-id="${getHover()}"]` : '.block__header a.interaction--upvote');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -34,8 +34,7 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty:
|
||||||
function clampValue(value: number): number {
|
function clampValue(value: number): number {
|
||||||
if (cachedValue >= cachedLimit && value < cachedLimit) {
|
if (cachedValue >= cachedLimit && value < cachedLimit) {
|
||||||
return cachedLimit;
|
return cachedLimit;
|
||||||
}
|
} else if (cachedValue < cachedLimit && value >= cachedLimit) {
|
||||||
else if (cachedValue < cachedLimit && value >= cachedLimit) {
|
|
||||||
return cachedLimit - 1; // Offset by 1 to ensure stored value is less than limit.
|
return cachedLimit - 1; // Offset by 1 to ensure stored value is less than limit.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +54,9 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty:
|
||||||
// Initializes cached variables. Should be used
|
// Initializes cached variables. Should be used
|
||||||
// when the pointer event begins.
|
// when the pointer event begins.
|
||||||
function initVars() {
|
function initVars() {
|
||||||
if (!parent) { return; }
|
if (!parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rect = parent.getBoundingClientRect();
|
const rect = parent.getBoundingClientRect();
|
||||||
|
|
||||||
|
@ -70,7 +71,9 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty:
|
||||||
|
|
||||||
// Called during pointer movement.
|
// Called during pointer movement.
|
||||||
function dragMove(e: PointerEvent) {
|
function dragMove(e: PointerEvent) {
|
||||||
if (!dragging) { return; }
|
if (!dragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -79,13 +82,7 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty:
|
||||||
// `lerp` cleverly clamps the value between min and max,
|
// `lerp` cleverly clamps the value between min and max,
|
||||||
// so no need for any explicit checks for that here, only
|
// so no need for any explicit checks for that here, only
|
||||||
// the crossover check is required.
|
// the crossover check is required.
|
||||||
curValue = clampValue(
|
curValue = clampValue(lerp((desiredPos - minPos) / (maxPos - minPos), cachedMin, cachedMax));
|
||||||
lerp(
|
|
||||||
(desiredPos - minPos) / (maxPos - minPos),
|
|
||||||
cachedMin,
|
|
||||||
cachedMax
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Same here, lerp clamps the value so it doesn't get out
|
// Same here, lerp clamps the value so it doesn't get out
|
||||||
// of the slider boundary.
|
// of the slider boundary.
|
||||||
|
@ -99,7 +96,9 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty:
|
||||||
|
|
||||||
// Called when the pointer is let go of.
|
// Called when the pointer is let go of.
|
||||||
function dragEnd(e: PointerEvent) {
|
function dragEnd(e: PointerEvent) {
|
||||||
if (!dragging) { return; }
|
if (!dragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -111,7 +110,9 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty:
|
||||||
|
|
||||||
// Called when the slider head is clicked or tapped.
|
// Called when the slider head is clicked or tapped.
|
||||||
function dragBegin(e: PointerEvent) {
|
function dragBegin(e: PointerEvent) {
|
||||||
if (!parent) { return; }
|
if (!parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
initVars();
|
initVars();
|
||||||
|
@ -168,7 +169,7 @@ function setupSlider(el: HTMLInputElement) {
|
||||||
|
|
||||||
// Sets up all sliders currently on the page.
|
// Sets up all sliders currently on the page.
|
||||||
function setupSliders() {
|
function setupSliders() {
|
||||||
$$<HTMLInputElement>('input[type="dualrange"]').forEach(el => {
|
$$<HTMLInputElement>('input[type="dualrange"]').forEach((el) => {
|
||||||
setupSlider(el);
|
setupSlider(el);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ export function imageSourcesCreator() {
|
||||||
if (target.matches('#source-form')) {
|
if (target.matches('#source-form')) {
|
||||||
const sourceSauce = assertNotNull($<HTMLElement>('.js-sourcesauce'));
|
const sourceSauce = assertNotNull($<HTMLElement>('.js-sourcesauce'));
|
||||||
|
|
||||||
detail.text().then(text => {
|
detail.text().then((text) => {
|
||||||
sourceSauce.outerHTML = text;
|
sourceSauce.outerHTML = text;
|
||||||
setupInputs();
|
setupInputs();
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,6 @@ import { $$, hideEl } from './utils/dom';
|
||||||
|
|
||||||
export function hideStaffTools() {
|
export function hideStaffTools() {
|
||||||
if (window.booru.hideStaffTools === 'true') {
|
if (window.booru.hideStaffTools === 'true') {
|
||||||
$$<HTMLElement>('.js-staff-action').forEach(el => hideEl(el));
|
$$<HTMLElement>('.js-staff-action').forEach((el) => hideEl(el));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,19 +19,46 @@ function removeTag(tagId: number, list: number[]) {
|
||||||
|
|
||||||
function createTagDropdown(tag: HTMLSpanElement) {
|
function createTagDropdown(tag: HTMLSpanElement) {
|
||||||
const { userIsSignedIn, userCanEditFilter, watchedTagList, spoileredTagList, hiddenTagList } = window.booru;
|
const { userIsSignedIn, userCanEditFilter, watchedTagList, spoileredTagList, hiddenTagList } = window.booru;
|
||||||
const [ unwatch, watch, unspoiler, spoiler, unhide, hide, signIn, filter ] = $$<HTMLElement>('.tag__dropdown__link', tag);
|
const [unwatch, watch, unspoiler, spoiler, unhide, hide, signIn, filter] = $$<HTMLElement>(
|
||||||
const [ unwatched, watched, spoilered, hidden ] = $$<HTMLSpanElement>('.tag__state', tag);
|
'.tag__dropdown__link',
|
||||||
|
tag,
|
||||||
|
);
|
||||||
|
const [unwatched, watched, spoilered, hidden] = $$<HTMLSpanElement>('.tag__state', tag);
|
||||||
const tagId = parseInt(assertNotUndefined(tag.dataset.tagId), 10);
|
const tagId = parseInt(assertNotUndefined(tag.dataset.tagId), 10);
|
||||||
|
|
||||||
const actions: TagDropdownActionList = {
|
const actions: TagDropdownActionList = {
|
||||||
unwatch() { hideEl(unwatch, watched); showEl(watch, unwatched); removeTag(tagId, watchedTagList); },
|
unwatch() {
|
||||||
watch() { hideEl(watch, unwatched); showEl(unwatch, watched); addTag(tagId, watchedTagList); },
|
hideEl(unwatch, watched);
|
||||||
|
showEl(watch, unwatched);
|
||||||
|
removeTag(tagId, watchedTagList);
|
||||||
|
},
|
||||||
|
watch() {
|
||||||
|
hideEl(watch, unwatched);
|
||||||
|
showEl(unwatch, watched);
|
||||||
|
addTag(tagId, watchedTagList);
|
||||||
|
},
|
||||||
|
|
||||||
unspoiler() { hideEl(unspoiler, spoilered); showEl(spoiler); removeTag(tagId, spoileredTagList); },
|
unspoiler() {
|
||||||
spoiler() { hideEl(spoiler); showEl(unspoiler, spoilered); addTag(tagId, spoileredTagList); },
|
hideEl(unspoiler, spoilered);
|
||||||
|
showEl(spoiler);
|
||||||
|
removeTag(tagId, spoileredTagList);
|
||||||
|
},
|
||||||
|
spoiler() {
|
||||||
|
hideEl(spoiler);
|
||||||
|
showEl(unspoiler, spoilered);
|
||||||
|
addTag(tagId, spoileredTagList);
|
||||||
|
},
|
||||||
|
|
||||||
unhide() { hideEl(unhide, hidden); showEl(hide); removeTag(tagId, hiddenTagList); },
|
unhide() {
|
||||||
hide() { hideEl(hide); showEl(unhide, hidden); addTag(tagId, hiddenTagList); },
|
hideEl(unhide, hidden);
|
||||||
|
showEl(hide);
|
||||||
|
removeTag(tagId, hiddenTagList);
|
||||||
|
},
|
||||||
|
hide() {
|
||||||
|
hideEl(hide);
|
||||||
|
showEl(unhide, hidden);
|
||||||
|
addTag(tagId, hiddenTagList);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const tagIsWatched = watchedTagList.includes(tagId);
|
const tagIsWatched = watchedTagList.includes(tagId);
|
||||||
|
@ -53,10 +80,9 @@ function createTagDropdown(tag: HTMLSpanElement) {
|
||||||
if (userCanEditFilter) showEl(spoilerLink);
|
if (userCanEditFilter) showEl(spoilerLink);
|
||||||
if (userCanEditFilter) showEl(hiddenLink);
|
if (userCanEditFilter) showEl(hiddenLink);
|
||||||
if (!userIsSignedIn) showEl(signIn);
|
if (!userIsSignedIn) showEl(signIn);
|
||||||
if (userIsSignedIn &&
|
if (userIsSignedIn && !userCanEditFilter) showEl(filter);
|
||||||
!userCanEditFilter) showEl(filter);
|
|
||||||
|
|
||||||
tag.addEventListener('fetchcomplete', event => {
|
tag.addEventListener('fetchcomplete', (event) => {
|
||||||
const act = assertNotUndefined(event.target.dataset.tagAction);
|
const act = assertNotUndefined(event.target.dataset.tagAction);
|
||||||
actions[act]();
|
actions[act]();
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import { $, $$, clearEl, removeEl, showEl, hideEl, escapeCss, escapeHtml } from './utils/dom';
|
import { $, $$, clearEl, removeEl, showEl, hideEl, escapeCss, escapeHtml } from './utils/dom';
|
||||||
|
|
||||||
function setupTagsInput(tagBlock) {
|
function setupTagsInput(tagBlock) {
|
||||||
const [ textarea, container ] = $$('.js-taginput', tagBlock);
|
const [textarea, container] = $$('.js-taginput', tagBlock);
|
||||||
const setup = $('.js-tag-block ~ button', tagBlock.parentNode);
|
const setup = $('.js-tag-block ~ button', tagBlock.parentNode);
|
||||||
const inputField = $('input', container);
|
const inputField = $('input', container);
|
||||||
|
|
||||||
|
@ -42,7 +42,6 @@ function setupTagsInput(tagBlock) {
|
||||||
importTags();
|
importTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleAutocomplete(event) {
|
function handleAutocomplete(event) {
|
||||||
insertTag(event.detail.value);
|
insertTag(event.detail.value);
|
||||||
inputField.focus();
|
inputField.focus();
|
||||||
|
@ -82,10 +81,9 @@ function setupTagsInput(tagBlock) {
|
||||||
// enter or comma
|
// enter or comma
|
||||||
if (keyCode === 13 || (keyCode === 188 && !shiftKey)) {
|
if (keyCode === 13 || (keyCode === 188 && !shiftKey)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
inputField.value.split(',').forEach(t => insertTag(t));
|
inputField.value.split(',').forEach((t) => insertTag(t));
|
||||||
inputField.value = '';
|
inputField.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCtrlEnter(event) {
|
function handleCtrlEnter(event) {
|
||||||
|
@ -131,19 +129,21 @@ function setupTagsInput(tagBlock) {
|
||||||
container.appendChild(inputField);
|
container.appendChild(inputField);
|
||||||
|
|
||||||
tags = [];
|
tags = [];
|
||||||
textarea.value.split(',').forEach(t => insertTag(t));
|
textarea.value.split(',').forEach((t) => insertTag(t));
|
||||||
textarea.value = tags.join(', ');
|
textarea.value = tags.join(', ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fancyEditorRequested(tagBlock) {
|
function fancyEditorRequested(tagBlock) {
|
||||||
// Check whether the user made the fancy editor the default for each type of tag block.
|
// Check whether the user made the fancy editor the default for each type of tag block.
|
||||||
return window.booru.fancyTagUpload && tagBlock.classList.contains('fancy-tag-upload') ||
|
return (
|
||||||
window.booru.fancyTagEdit && tagBlock.classList.contains('fancy-tag-edit');
|
(window.booru.fancyTagUpload && tagBlock.classList.contains('fancy-tag-upload')) ||
|
||||||
|
(window.booru.fancyTagEdit && tagBlock.classList.contains('fancy-tag-edit'))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupTagListener() {
|
function setupTagListener() {
|
||||||
document.addEventListener('addtag', event => {
|
document.addEventListener('addtag', (event) => {
|
||||||
if (event.target.value) event.target.value += ', ';
|
if (event.target.value) event.target.value += ', ';
|
||||||
event.target.value += event.detail.name;
|
event.target.value += event.detail.name;
|
||||||
});
|
});
|
||||||
|
|
|
@ -30,7 +30,7 @@ function tagInputButtons(event: MouseEvent) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [ name, action ] of Object.entries(actions)) {
|
for (const [name, action] of Object.entries(actions)) {
|
||||||
if (target && target.matches(`#tagsinput-${name}`)) {
|
if (target && target.matches(`#tagsinput-${name}`)) {
|
||||||
action(assertNotNull($<HTMLTextAreaElement>('#image_tag_input')));
|
action(assertNotNull($<HTMLTextAreaElement>('#image_tag_input')));
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ function tagInputButtons(event: MouseEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupTags() {
|
function setupTags() {
|
||||||
$$<HTMLDivElement>('.js-tag-block').forEach(el => {
|
$$<HTMLDivElement>('.js-tag-block').forEach((el) => {
|
||||||
setupTagsInput(el);
|
setupTagsInput(el);
|
||||||
el.classList.remove('js-tag-block');
|
el.classList.remove('js-tag-block');
|
||||||
});
|
});
|
||||||
|
@ -48,7 +48,7 @@ function updateTagSauce({ target, detail }: FetchcompleteEvent) {
|
||||||
if (target.matches('#tags-form')) {
|
if (target.matches('#tags-form')) {
|
||||||
const tagSauce = assertNotNull($<HTMLDivElement>('.js-tagsauce'));
|
const tagSauce = assertNotNull($<HTMLDivElement>('.js-tagsauce'));
|
||||||
|
|
||||||
detail.text().then(text => {
|
detail.text().then((text) => {
|
||||||
tagSauce.outerHTML = text;
|
tagSauce.outerHTML = text;
|
||||||
setupTags();
|
setupTags();
|
||||||
initTagDropdown();
|
initTagDropdown();
|
||||||
|
|
|
@ -43,16 +43,16 @@ function setTimeAgo(el: HTMLTimeElement) {
|
||||||
years = days / 365;
|
years = days / 365;
|
||||||
|
|
||||||
const words =
|
const words =
|
||||||
seconds < 45 && substitute('seconds', seconds) ||
|
(seconds < 45 && substitute('seconds', seconds)) ||
|
||||||
seconds < 90 && substitute('minute', 1) ||
|
(seconds < 90 && substitute('minute', 1)) ||
|
||||||
minutes < 45 && substitute('minutes', minutes) ||
|
(minutes < 45 && substitute('minutes', minutes)) ||
|
||||||
minutes < 90 && substitute('hour', 1) ||
|
(minutes < 90 && substitute('hour', 1)) ||
|
||||||
hours < 24 && substitute('hours', hours) ||
|
(hours < 24 && substitute('hours', hours)) ||
|
||||||
hours < 42 && substitute('day', 1) ||
|
(hours < 42 && substitute('day', 1)) ||
|
||||||
days < 30 && substitute('days', days) ||
|
(days < 30 && substitute('days', days)) ||
|
||||||
days < 45 && substitute('month', 1) ||
|
(days < 45 && substitute('month', 1)) ||
|
||||||
days < 365 && substitute('months', months) ||
|
(days < 365 && substitute('months', months)) ||
|
||||||
years < 1.5 && substitute('year', 1) ||
|
(years < 1.5 && substitute('year', 1)) ||
|
||||||
substitute('years', years);
|
substitute('years', years);
|
||||||
|
|
||||||
if (!el.getAttribute('title')) {
|
if (!el.getAttribute('title')) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { fire, delegate, leftClick } from './utils/events';
|
||||||
|
|
||||||
const headers = () => ({
|
const headers = () => ({
|
||||||
'x-csrf-token': window.booru.csrfToken,
|
'x-csrf-token': window.booru.csrfToken,
|
||||||
'x-requested-with': 'XMLHttpRequest'
|
'x-requested-with': 'XMLHttpRequest',
|
||||||
});
|
});
|
||||||
|
|
||||||
function confirm(event: Event, target: HTMLElement) {
|
function confirm(event: Event, target: HTMLElement) {
|
||||||
|
@ -25,8 +25,7 @@ function disable(event: Event, target: HTMLAnchorElement | HTMLButtonElement | H
|
||||||
if (label) {
|
if (label) {
|
||||||
target.dataset.enableWith = assertNotNull(label.nodeValue);
|
target.dataset.enableWith = assertNotNull(label.nodeValue);
|
||||||
label.nodeValue = ` ${target.dataset.disableWith}`;
|
label.nodeValue = ` ${target.dataset.disableWith}`;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
target.dataset.enableWith = target.innerHTML;
|
target.dataset.enableWith = target.innerHTML;
|
||||||
target.innerHTML = assertNotUndefined(target.dataset.disableWith);
|
target.innerHTML = assertNotUndefined(target.dataset.disableWith);
|
||||||
}
|
}
|
||||||
|
@ -57,8 +56,8 @@ function formRemote(event: Event, target: HTMLFormElement) {
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
method: (target.dataset.method || target.method).toUpperCase(),
|
method: (target.dataset.method || target.method).toUpperCase(),
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
body: new FormData(target)
|
body: new FormData(target),
|
||||||
}).then(response => {
|
}).then((response) => {
|
||||||
fire(target, 'fetchcomplete', response);
|
fire(target, 'fetchcomplete', response);
|
||||||
if (response && response.status === 300) {
|
if (response && response.status === 300) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
@ -67,12 +66,11 @@ function formRemote(event: Event, target: HTMLFormElement) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function formReset(_event: Event | null, target: HTMLElement) {
|
function formReset(_event: Event | null, target: HTMLElement) {
|
||||||
$$<HTMLElement>('[disabled][data-disable-with][data-enable-with]', target).forEach(input => {
|
$$<HTMLElement>('[disabled][data-disable-with][data-enable-with]', target).forEach((input) => {
|
||||||
const label = findFirstTextNode(input);
|
const label = findFirstTextNode(input);
|
||||||
if (label) {
|
if (label) {
|
||||||
label.nodeValue = ` ${input.dataset.enableWith}`;
|
label.nodeValue = ` ${input.dataset.enableWith}`;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
input.innerHTML = assertNotUndefined(input.dataset.enableWith);
|
input.innerHTML = assertNotUndefined(input.dataset.enableWith);
|
||||||
}
|
}
|
||||||
delete input.dataset.enableWith;
|
delete input.dataset.enableWith;
|
||||||
|
@ -86,10 +84,8 @@ function linkRemote(event: Event, target: HTMLAnchorElement) {
|
||||||
fetch(target.href, {
|
fetch(target.href, {
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
method: (target.dataset.method || 'get').toUpperCase(),
|
method: (target.dataset.method || 'get').toUpperCase(),
|
||||||
headers: headers()
|
headers: headers(),
|
||||||
}).then(response =>
|
}).then((response) => fire(target, 'fetchcomplete', response));
|
||||||
fire(target, 'fetchcomplete', response)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate(document, 'click', {
|
delegate(document, 'click', {
|
||||||
|
@ -100,11 +96,11 @@ delegate(document, 'click', {
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, 'submit', {
|
delegate(document, 'submit', {
|
||||||
'form[data-remote]': formRemote
|
'form[data-remote]': formRemote,
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, 'reset', {
|
delegate(document, 'reset', {
|
||||||
form: formReset
|
form: formReset,
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('pageshow', () => {
|
window.addEventListener('pageshow', () => {
|
||||||
|
|
|
@ -11,7 +11,7 @@ const MATROSKA_MAGIC = 0x1a45dfa3;
|
||||||
function scrapeUrl(url) {
|
function scrapeUrl(url) {
|
||||||
return fetchJson('POST', '/images/scrape', { url })
|
return fetchJson('POST', '/images/scrape', { url })
|
||||||
.then(handleError)
|
.then(handleError)
|
||||||
.then(response => response.json());
|
.then((response) => response.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
function elementForEmbeddedImage({ camo_url, type }) {
|
function elementForEmbeddedImage({ camo_url, type }) {
|
||||||
|
@ -34,7 +34,7 @@ function setupImageUpload() {
|
||||||
const [fileField, remoteUrl, scraperError] = $$('.js-scraper', form);
|
const [fileField, remoteUrl, scraperError] = $$('.js-scraper', form);
|
||||||
const descrEl = $('.js-image-descr-input', form);
|
const descrEl = $('.js-image-descr-input', form);
|
||||||
const tagsEl = $('.js-image-tags-input', form);
|
const tagsEl = $('.js-image-tags-input', form);
|
||||||
const sourceEl = $$('.js-source-url', form).find(input => input.value === '');
|
const sourceEl = $$('.js-source-url', form).find((input) => input.value === '');
|
||||||
const fetchButton = $('#js-scraper-preview');
|
const fetchButton = $('#js-scraper-preview');
|
||||||
if (!fetchButton) return;
|
if (!fetchButton) return;
|
||||||
|
|
||||||
|
@ -68,17 +68,25 @@ function setupImageUpload() {
|
||||||
showEl(scraperError);
|
showEl(scraperError);
|
||||||
enableFetch();
|
enableFetch();
|
||||||
}
|
}
|
||||||
function hideError() { hideEl(scraperError); }
|
function hideError() {
|
||||||
function disableFetch() { fetchButton.setAttribute('disabled', ''); }
|
hideEl(scraperError);
|
||||||
function enableFetch() { fetchButton.removeAttribute('disabled'); }
|
}
|
||||||
|
function disableFetch() {
|
||||||
|
fetchButton.setAttribute('disabled', '');
|
||||||
|
}
|
||||||
|
function enableFetch() {
|
||||||
|
fetchButton.removeAttribute('disabled');
|
||||||
|
}
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.addEventListener('load', event => {
|
reader.addEventListener('load', (event) => {
|
||||||
showImages([{
|
showImages([
|
||||||
|
{
|
||||||
camo_url: event.target.result,
|
camo_url: event.target.result,
|
||||||
type: fileField.files[0].type
|
type: fileField.files[0].type,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
// Clear any currently cached data, because the file field
|
// Clear any currently cached data, because the file field
|
||||||
// has higher priority than the scraper:
|
// has higher priority than the scraper:
|
||||||
|
@ -88,7 +96,9 @@ function setupImageUpload() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch for files added to the form
|
// Watch for files added to the form
|
||||||
fileField.addEventListener('change', () => { fileField.files.length && reader.readAsArrayBuffer(fileField.files[0]); });
|
fileField.addEventListener('change', () => {
|
||||||
|
fileField.files.length && reader.readAsArrayBuffer(fileField.files[0]);
|
||||||
|
});
|
||||||
|
|
||||||
// Watch for [Fetch] clicks
|
// Watch for [Fetch] clicks
|
||||||
fetchButton.addEventListener('click', () => {
|
fetchButton.addEventListener('click', () => {
|
||||||
|
@ -96,13 +106,13 @@ function setupImageUpload() {
|
||||||
|
|
||||||
disableFetch();
|
disableFetch();
|
||||||
|
|
||||||
scrapeUrl(remoteUrl.value).then(data => {
|
scrapeUrl(remoteUrl.value)
|
||||||
|
.then((data) => {
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
scraperError.innerText = 'No image found at that address.';
|
scraperError.innerText = 'No image found at that address.';
|
||||||
showError();
|
showError();
|
||||||
return;
|
return;
|
||||||
}
|
} else if (data.errors && data.errors.length > 0) {
|
||||||
else if (data.errors && data.errors.length > 0) {
|
|
||||||
scraperError.innerText = data.errors.join(' ');
|
scraperError.innerText = data.errors.join(' ');
|
||||||
showError();
|
showError();
|
||||||
return;
|
return;
|
||||||
|
@ -121,12 +131,14 @@ function setupImageUpload() {
|
||||||
showImages(data.images);
|
showImages(data.images);
|
||||||
|
|
||||||
enableFetch();
|
enableFetch();
|
||||||
}).catch(showError);
|
})
|
||||||
|
.catch(showError);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch on "enter" in url field
|
// Fetch on "enter" in url field
|
||||||
remoteUrl.addEventListener('keydown', event => {
|
remoteUrl.addEventListener('keydown', (event) => {
|
||||||
if (event.keyCode === 13) { // Hit enter
|
if (event.keyCode === 13) {
|
||||||
|
// Hit enter
|
||||||
fetchButton.click();
|
fetchButton.click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -135,8 +147,7 @@ function setupImageUpload() {
|
||||||
function setFetchEnabled() {
|
function setFetchEnabled() {
|
||||||
if (remoteUrl.value.length > 0) {
|
if (remoteUrl.value.length > 0) {
|
||||||
enableFetch();
|
enableFetch();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
disableFetch();
|
disableFetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,15 +84,17 @@ describe('Array Utilities', () => {
|
||||||
|
|
||||||
// Mixed parameters
|
// Mixed parameters
|
||||||
const mockObject = { value: Math.random() };
|
const mockObject = { value: Math.random() };
|
||||||
expect(arraysEqual(
|
expect(
|
||||||
|
arraysEqual(
|
||||||
['', null, false, uniqueValue, mockObject, Infinity, undefined],
|
['', null, false, uniqueValue, mockObject, Infinity, undefined],
|
||||||
['', null, false, uniqueValue, mockObject, Infinity, undefined]
|
['', null, false, uniqueValue, mockObject, Infinity, undefined],
|
||||||
)).toBe(true);
|
),
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('negative cases', () => {
|
describe('negative cases', () => {
|
||||||
it('should NOT return true for matching only up to the first array\'s length', () => {
|
it("should NOT return true for matching only up to the first array's length", () => {
|
||||||
// Numbers
|
// Numbers
|
||||||
expect(arraysEqual([0], [0, 1])).toBe(false);
|
expect(arraysEqual([0], [0, 1])).toBe(false);
|
||||||
expect(arraysEqual([0, 1], [0, 1, 2])).toBe(false);
|
expect(arraysEqual([0, 1], [0, 1, 2])).toBe(false);
|
||||||
|
@ -108,26 +110,15 @@ describe('Array Utilities', () => {
|
||||||
|
|
||||||
// Mixed parameters
|
// Mixed parameters
|
||||||
const mockObject = { value: Math.random() };
|
const mockObject = { value: Math.random() };
|
||||||
expect(arraysEqual(
|
expect(arraysEqual([''], ['', null, false, mockObject, Infinity, undefined])).toBe(false);
|
||||||
[''],
|
expect(arraysEqual(['', null], ['', null, false, mockObject, Infinity, undefined])).toBe(false);
|
||||||
['', null, false, mockObject, Infinity, undefined]
|
expect(arraysEqual(['', null, false], ['', null, false, mockObject, Infinity, undefined])).toBe(false);
|
||||||
)).toBe(false);
|
expect(arraysEqual(['', null, false, mockObject], ['', null, false, mockObject, Infinity, undefined])).toBe(
|
||||||
expect(arraysEqual(
|
false,
|
||||||
['', null],
|
);
|
||||||
['', null, false, mockObject, Infinity, undefined]
|
expect(
|
||||||
)).toBe(false);
|
arraysEqual(['', null, false, mockObject, Infinity], ['', null, false, mockObject, Infinity, undefined]),
|
||||||
expect(arraysEqual(
|
).toBe(false);
|
||||||
['', null, false],
|
|
||||||
['', null, false, mockObject, Infinity, undefined]
|
|
||||||
)).toBe(false);
|
|
||||||
expect(arraysEqual(
|
|
||||||
['', null, false, mockObject],
|
|
||||||
['', null, false, mockObject, Infinity, undefined]
|
|
||||||
)).toBe(false);
|
|
||||||
expect(arraysEqual(
|
|
||||||
['', null, false, mockObject, Infinity],
|
|
||||||
['', null, false, mockObject, Infinity, undefined]
|
|
||||||
)).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for arrays of different length', () => {
|
it('should return false for arrays of different length', () => {
|
||||||
|
@ -151,7 +142,7 @@ describe('Array Utilities', () => {
|
||||||
expect(arraysEqual([mockObject], [mockObject, mockObject])).toBe(false);
|
expect(arraysEqual([mockObject], [mockObject, mockObject])).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if items up to the first array\'s length differ', () => {
|
it("should return false if items up to the first array's length differ", () => {
|
||||||
// Numbers
|
// Numbers
|
||||||
expect(arraysEqual([0], [1])).toBe(false);
|
expect(arraysEqual([0], [1])).toBe(false);
|
||||||
expect(arraysEqual([0, 1], [1, 2])).toBe(false);
|
expect(arraysEqual([0, 1], [1, 2])).toBe(false);
|
||||||
|
@ -168,22 +159,12 @@ describe('Array Utilities', () => {
|
||||||
expect(arraysEqual([mockObject1], [mockObject2])).toBe(false);
|
expect(arraysEqual([mockObject1], [mockObject2])).toBe(false);
|
||||||
|
|
||||||
// Mixed parameters
|
// Mixed parameters
|
||||||
expect(arraysEqual(
|
expect(arraysEqual(['a'], ['b', null, false, mockObject2, Infinity])).toBe(false);
|
||||||
['a'],
|
expect(arraysEqual(['a', null, true], ['b', null, false, mockObject2, Infinity])).toBe(false);
|
||||||
['b', null, false, mockObject2, Infinity]
|
expect(arraysEqual(['a', null, true, mockObject1], ['b', null, false, mockObject2, Infinity])).toBe(false);
|
||||||
)).toBe(false);
|
expect(arraysEqual(['a', null, true, mockObject1, -Infinity], ['b', null, false, mockObject2, Infinity])).toBe(
|
||||||
expect(arraysEqual(
|
false,
|
||||||
['a', null, true],
|
);
|
||||||
['b', null, false, mockObject2, Infinity]
|
|
||||||
)).toBe(false);
|
|
||||||
expect(arraysEqual(
|
|
||||||
['a', null, true, mockObject1],
|
|
||||||
['b', null, false, mockObject2, Infinity]
|
|
||||||
)).toBe(false);
|
|
||||||
expect(arraysEqual(
|
|
||||||
['a', null, true, mockObject1, -Infinity],
|
|
||||||
['b', null, false, mockObject2, Infinity]
|
|
||||||
)).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -87,11 +87,7 @@ describe('DOM Utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should remove the ${hiddenClass} class from all provided elements`, () => {
|
it(`should remove the ${hiddenClass} class from all provided elements`, () => {
|
||||||
const mockElements = [
|
const mockElements = [createHiddenElement('div'), createHiddenElement('a'), createHiddenElement('strong')];
|
||||||
createHiddenElement('div'),
|
|
||||||
createHiddenElement('a'),
|
|
||||||
createHiddenElement('strong'),
|
|
||||||
];
|
|
||||||
showEl(mockElements);
|
showEl(mockElements);
|
||||||
expect(mockElements[0]).not.toHaveClass(hiddenClass);
|
expect(mockElements[0]).not.toHaveClass(hiddenClass);
|
||||||
expect(mockElements[1]).not.toHaveClass(hiddenClass);
|
expect(mockElements[1]).not.toHaveClass(hiddenClass);
|
||||||
|
@ -99,14 +95,8 @@ describe('DOM Utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should remove the ${hiddenClass} class from elements provided in multiple arrays`, () => {
|
it(`should remove the ${hiddenClass} class from elements provided in multiple arrays`, () => {
|
||||||
const mockElements1 = [
|
const mockElements1 = [createHiddenElement('div'), createHiddenElement('a')];
|
||||||
createHiddenElement('div'),
|
const mockElements2 = [createHiddenElement('strong'), createHiddenElement('em')];
|
||||||
createHiddenElement('a'),
|
|
||||||
];
|
|
||||||
const mockElements2 = [
|
|
||||||
createHiddenElement('strong'),
|
|
||||||
createHiddenElement('em'),
|
|
||||||
];
|
|
||||||
showEl(mockElements1, mockElements2);
|
showEl(mockElements1, mockElements2);
|
||||||
expect(mockElements1[0]).not.toHaveClass(hiddenClass);
|
expect(mockElements1[0]).not.toHaveClass(hiddenClass);
|
||||||
expect(mockElements1[1]).not.toHaveClass(hiddenClass);
|
expect(mockElements1[1]).not.toHaveClass(hiddenClass);
|
||||||
|
@ -135,14 +125,8 @@ describe('DOM Utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should add the ${hiddenClass} class to elements provided in multiple arrays`, () => {
|
it(`should add the ${hiddenClass} class to elements provided in multiple arrays`, () => {
|
||||||
const mockElements1 = [
|
const mockElements1 = [document.createElement('div'), document.createElement('a')];
|
||||||
document.createElement('div'),
|
const mockElements2 = [document.createElement('strong'), document.createElement('em')];
|
||||||
document.createElement('a'),
|
|
||||||
];
|
|
||||||
const mockElements2 = [
|
|
||||||
document.createElement('strong'),
|
|
||||||
document.createElement('em'),
|
|
||||||
];
|
|
||||||
hideEl(mockElements1, mockElements2);
|
hideEl(mockElements1, mockElements2);
|
||||||
expect(mockElements1[0]).toHaveClass(hiddenClass);
|
expect(mockElements1[0]).toHaveClass(hiddenClass);
|
||||||
expect(mockElements1[1]).toHaveClass(hiddenClass);
|
expect(mockElements1[1]).toHaveClass(hiddenClass);
|
||||||
|
@ -159,24 +143,15 @@ describe('DOM Utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the disabled attribute to true on all provided elements', () => {
|
it('should set the disabled attribute to true on all provided elements', () => {
|
||||||
const mockElements = [
|
const mockElements = [document.createElement('input'), document.createElement('button')];
|
||||||
document.createElement('input'),
|
|
||||||
document.createElement('button'),
|
|
||||||
];
|
|
||||||
disableEl(mockElements);
|
disableEl(mockElements);
|
||||||
expect(mockElements[0]).toBeDisabled();
|
expect(mockElements[0]).toBeDisabled();
|
||||||
expect(mockElements[1]).toBeDisabled();
|
expect(mockElements[1]).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the disabled attribute to true on elements provided in multiple arrays', () => {
|
it('should set the disabled attribute to true on elements provided in multiple arrays', () => {
|
||||||
const mockElements1 = [
|
const mockElements1 = [document.createElement('input'), document.createElement('button')];
|
||||||
document.createElement('input'),
|
const mockElements2 = [document.createElement('textarea'), document.createElement('button')];
|
||||||
document.createElement('button'),
|
|
||||||
];
|
|
||||||
const mockElements2 = [
|
|
||||||
document.createElement('textarea'),
|
|
||||||
document.createElement('button'),
|
|
||||||
];
|
|
||||||
disableEl(mockElements1, mockElements2);
|
disableEl(mockElements1, mockElements2);
|
||||||
expect(mockElements1[0]).toBeDisabled();
|
expect(mockElements1[0]).toBeDisabled();
|
||||||
expect(mockElements1[1]).toBeDisabled();
|
expect(mockElements1[1]).toBeDisabled();
|
||||||
|
@ -193,24 +168,15 @@ describe('DOM Utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the disabled attribute to false on all provided elements', () => {
|
it('should set the disabled attribute to false on all provided elements', () => {
|
||||||
const mockElements = [
|
const mockElements = [document.createElement('input'), document.createElement('button')];
|
||||||
document.createElement('input'),
|
|
||||||
document.createElement('button'),
|
|
||||||
];
|
|
||||||
enableEl(mockElements);
|
enableEl(mockElements);
|
||||||
expect(mockElements[0]).toBeEnabled();
|
expect(mockElements[0]).toBeEnabled();
|
||||||
expect(mockElements[1]).toBeEnabled();
|
expect(mockElements[1]).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the disabled attribute to false on elements provided in multiple arrays', () => {
|
it('should set the disabled attribute to false on elements provided in multiple arrays', () => {
|
||||||
const mockElements1 = [
|
const mockElements1 = [document.createElement('input'), document.createElement('button')];
|
||||||
document.createElement('input'),
|
const mockElements2 = [document.createElement('textarea'), document.createElement('button')];
|
||||||
document.createElement('button'),
|
|
||||||
];
|
|
||||||
const mockElements2 = [
|
|
||||||
document.createElement('textarea'),
|
|
||||||
document.createElement('button'),
|
|
||||||
];
|
|
||||||
enableEl(mockElements1, mockElements2);
|
enableEl(mockElements1, mockElements2);
|
||||||
expect(mockElements1[0]).toBeEnabled();
|
expect(mockElements1[0]).toBeEnabled();
|
||||||
expect(mockElements1[1]).toBeEnabled();
|
expect(mockElements1[1]).toBeEnabled();
|
||||||
|
@ -245,14 +211,8 @@ describe('DOM Utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should toggle the ${hiddenClass} class on elements provided in multiple arrays`, () => {
|
it(`should toggle the ${hiddenClass} class on elements provided in multiple arrays`, () => {
|
||||||
const mockElements1 = [
|
const mockElements1 = [createHiddenElement('div'), document.createElement('a')];
|
||||||
createHiddenElement('div'),
|
const mockElements2 = [createHiddenElement('strong'), document.createElement('em')];
|
||||||
document.createElement('a'),
|
|
||||||
];
|
|
||||||
const mockElements2 = [
|
|
||||||
createHiddenElement('strong'),
|
|
||||||
document.createElement('em'),
|
|
||||||
];
|
|
||||||
toggleEl(mockElements1, mockElements2);
|
toggleEl(mockElements1, mockElements2);
|
||||||
expect(mockElements1[0]).not.toHaveClass(hiddenClass);
|
expect(mockElements1[0]).not.toHaveClass(hiddenClass);
|
||||||
expect(mockElements1[1]).toHaveClass(hiddenClass);
|
expect(mockElements1[1]).toHaveClass(hiddenClass);
|
||||||
|
@ -430,8 +390,7 @@ describe('DOM Utilities', () => {
|
||||||
try {
|
try {
|
||||||
whenReady(mockCallback);
|
whenReady(mockCallback);
|
||||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
readyStateSpy.mockRestore();
|
readyStateSpy.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -446,8 +405,7 @@ describe('DOM Utilities', () => {
|
||||||
expect(addEventListenerSpy).toHaveBeenCalledTimes(1);
|
expect(addEventListenerSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(addEventListenerSpy).toHaveBeenNthCalledWith(1, 'DOMContentLoaded', mockCallback);
|
expect(addEventListenerSpy).toHaveBeenNthCalledWith(1, 'DOMContentLoaded', mockCallback);
|
||||||
expect(mockCallback).not.toHaveBeenCalled();
|
expect(mockCallback).not.toHaveBeenCalled();
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
readyStateSpy.mockRestore();
|
readyStateSpy.mockRestore();
|
||||||
addEventListenerSpy.mockRestore();
|
addEventListenerSpy.mockRestore();
|
||||||
}
|
}
|
||||||
|
@ -456,7 +414,9 @@ describe('DOM Utilities', () => {
|
||||||
|
|
||||||
describe('escapeHtml', () => {
|
describe('escapeHtml', () => {
|
||||||
it('should replace only the expected characters with their HTML entity equivalents', () => {
|
it('should replace only the expected characters with their HTML entity equivalents', () => {
|
||||||
expect(escapeHtml('<script src="http://example.com/?a=1&b=2"></script>')).toBe('<script src="http://example.com/?a=1&b=2"></script>');
|
expect(escapeHtml('<script src="http://example.com/?a=1&b=2"></script>')).toBe(
|
||||||
|
'<script src="http://example.com/?a=1&b=2"></script>',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ describe('Draggable Utilities', () => {
|
||||||
items: items as unknown as DataTransferItemList,
|
items: items as unknown as DataTransferItemList,
|
||||||
setData(format: string, data: string) {
|
setData(format: string, data: string) {
|
||||||
items.push({ type: format, getAsString: (callback: FunctionStringCallback) => callback(data) });
|
items.push({ type: format, getAsString: (callback: FunctionStringCallback) => callback(data) });
|
||||||
}
|
},
|
||||||
} as unknown as DataTransfer;
|
} as unknown as DataTransfer;
|
||||||
}
|
}
|
||||||
Object.assign(mockEvent, { dataTransfer });
|
Object.assign(mockEvent, { dataTransfer });
|
||||||
|
@ -44,7 +44,6 @@ describe('Draggable Utilities', () => {
|
||||||
mockDraggable = createDraggableElement();
|
mockDraggable = createDraggableElement();
|
||||||
mockDragContainer.appendChild(mockDraggable);
|
mockDragContainer.appendChild(mockDraggable);
|
||||||
|
|
||||||
|
|
||||||
// Redirect all document event listeners to this element for easier cleanup
|
// Redirect all document event listeners to this element for easier cleanup
|
||||||
documentEventListenerSpy = vi.spyOn(document, 'addEventListener').mockImplementation((...params) => {
|
documentEventListenerSpy = vi.spyOn(document, 'addEventListener').mockImplementation((...params) => {
|
||||||
mockDragContainer.addEventListener(...params);
|
mockDragContainer.addEventListener(...params);
|
||||||
|
@ -67,7 +66,7 @@ describe('Draggable Utilities', () => {
|
||||||
expect(mockDraggable).toHaveClass(draggingClass);
|
expect(mockDraggable).toHaveClass(draggingClass);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add dummy data to the dragstart event if it\'s empty', () => {
|
it("should add dummy data to the dragstart event if it's empty", () => {
|
||||||
initDraggables();
|
initDraggables();
|
||||||
|
|
||||||
const mockEvent = createDragEvent('dragstart');
|
const mockEvent = createDragEvent('dragstart');
|
||||||
|
@ -81,13 +80,13 @@ describe('Draggable Utilities', () => {
|
||||||
expect(dataTransferItem.type).toEqual('text/plain');
|
expect(dataTransferItem.type).toEqual('text/plain');
|
||||||
|
|
||||||
let stringValue: string | undefined;
|
let stringValue: string | undefined;
|
||||||
dataTransferItem.getAsString(value => {
|
dataTransferItem.getAsString((value) => {
|
||||||
stringValue = value;
|
stringValue = value;
|
||||||
});
|
});
|
||||||
expect(stringValue).toEqual('');
|
expect(stringValue).toEqual('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should keep data in the dragstart event if it\'s present', () => {
|
it("should keep data in the dragstart event if it's present", () => {
|
||||||
initDraggables();
|
initDraggables();
|
||||||
|
|
||||||
const mockTransferItemType = getRandomArrayItem(['text/javascript', 'image/jpg', 'application/json']);
|
const mockTransferItemType = getRandomArrayItem(['text/javascript', 'image/jpg', 'application/json']);
|
||||||
|
@ -95,7 +94,9 @@ describe('Draggable Utilities', () => {
|
||||||
type: mockTransferItemType,
|
type: mockTransferItemType,
|
||||||
} as unknown as DataTransferItem;
|
} as unknown as DataTransferItem;
|
||||||
|
|
||||||
const mockEvent = createDragEvent('dragstart', { dataTransfer: { items: [mockDataTransferItem] as unknown as DataTransferItemList } } as DragEventInit);
|
const mockEvent = createDragEvent('dragstart', {
|
||||||
|
dataTransfer: { items: [mockDataTransferItem] as unknown as DataTransferItemList },
|
||||||
|
} as DragEventInit);
|
||||||
expect(mockEvent.dataTransfer?.items).toHaveLength(1);
|
expect(mockEvent.dataTransfer?.items).toHaveLength(1);
|
||||||
|
|
||||||
fireEvent(mockDraggable, mockEvent);
|
fireEvent(mockDraggable, mockEvent);
|
||||||
|
@ -203,8 +204,7 @@ describe('Draggable Utilities', () => {
|
||||||
expect(mockDropEvent.defaultPrevented).toBe(true);
|
expect(mockDropEvent.defaultPrevented).toBe(true);
|
||||||
expect(mockSecondDraggable).not.toHaveClass(draggingClass);
|
expect(mockSecondDraggable).not.toHaveClass(draggingClass);
|
||||||
expect(mockSecondDraggable.nextElementSibling).toBe(mockDraggable);
|
expect(mockSecondDraggable.nextElementSibling).toBe(mockDraggable);
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
boundingBoxSpy.mockRestore();
|
boundingBoxSpy.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -232,8 +232,7 @@ describe('Draggable Utilities', () => {
|
||||||
expect(mockDropEvent.defaultPrevented).toBe(true);
|
expect(mockDropEvent.defaultPrevented).toBe(true);
|
||||||
expect(mockSecondDraggable).not.toHaveClass(draggingClass);
|
expect(mockSecondDraggable).not.toHaveClass(draggingClass);
|
||||||
expect(mockDraggable.nextElementSibling).toBe(mockSecondDraggable);
|
expect(mockDraggable.nextElementSibling).toBe(mockSecondDraggable);
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
boundingBoxSpy.mockRestore();
|
boundingBoxSpy.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -254,7 +253,7 @@ describe('Draggable Utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('dragEnd', () => {
|
describe('dragEnd', () => {
|
||||||
it('should remove dragging class from source and over class from target\'s descendants', () => {
|
it("should remove dragging class from source and over class from target's descendants", () => {
|
||||||
initDraggables();
|
initDraggables();
|
||||||
|
|
||||||
const mockStartEvent = createDragEvent('dragstart');
|
const mockStartEvent = createDragEvent('dragstart');
|
||||||
|
@ -298,8 +297,7 @@ describe('Draggable Utilities', () => {
|
||||||
fireEvent(mockDraggable, mockEvent);
|
fireEvent(mockDraggable, mockEvent);
|
||||||
|
|
||||||
expect(mockEvent.dataTransfer?.effectAllowed).toBeFalsy();
|
expect(mockEvent.dataTransfer?.effectAllowed).toBeFalsy();
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
draggableClosestSpy.mockRestore();
|
draggableClosestSpy.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -60,7 +60,7 @@ describe('Event utils', () => {
|
||||||
const mockButton = document.createElement('button');
|
const mockButton = document.createElement('button');
|
||||||
const mockHandler = vi.fn();
|
const mockHandler = vi.fn();
|
||||||
|
|
||||||
mockButton.addEventListener('click', e => leftClick(mockHandler)(e, mockButton));
|
mockButton.addEventListener('click', (e) => leftClick(mockHandler)(e, mockButton));
|
||||||
|
|
||||||
fireEvent.click(mockButton, { button: 0 });
|
fireEvent.click(mockButton, { button: 0 });
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ describe('Event utils', () => {
|
||||||
const mockHandler = vi.fn();
|
const mockHandler = vi.fn();
|
||||||
const mockButtonNumber = getRandomArrayItem([1, 2, 3, 4, 5]);
|
const mockButtonNumber = getRandomArrayItem([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
mockButton.addEventListener('click', e => leftClick(mockHandler)(e, mockButton));
|
mockButton.addEventListener('click', (e) => leftClick(mockHandler)(e, mockButton));
|
||||||
|
|
||||||
fireEvent.click(mockButton, { button: mockButtonNumber });
|
fireEvent.click(mockButton, { button: mockButtonNumber });
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,7 @@ describe('Image utils', () => {
|
||||||
extension: string;
|
extension: string;
|
||||||
videoClasses?: string[];
|
videoClasses?: string[];
|
||||||
imgClasses?: string[];
|
imgClasses?: string[];
|
||||||
}
|
};
|
||||||
|
|
||||||
const createMockElements = ({ videoClasses, imgClasses, extension }: CreateMockElementsOptions) => {
|
const createMockElements = ({ videoClasses, imgClasses, extension }: CreateMockElementsOptions) => {
|
||||||
const mockElement = document.createElement('div');
|
const mockElement = document.createElement('div');
|
||||||
|
@ -101,7 +101,7 @@ describe('Image utils', () => {
|
||||||
const mockImage = new Image();
|
const mockImage = new Image();
|
||||||
mockImage.src = mockImageUri;
|
mockImage.src = mockImageUri;
|
||||||
if (imgClasses) {
|
if (imgClasses) {
|
||||||
imgClasses.forEach(videoClass => {
|
imgClasses.forEach((videoClass) => {
|
||||||
mockImage.classList.add(videoClass);
|
mockImage.classList.add(videoClass);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ describe('Image utils', () => {
|
||||||
|
|
||||||
const mockVideo = document.createElement('video');
|
const mockVideo = document.createElement('video');
|
||||||
if (videoClasses) {
|
if (videoClasses) {
|
||||||
videoClasses.forEach(videoClass => {
|
videoClasses.forEach((videoClass) => {
|
||||||
mockVideo.classList.add(videoClass);
|
mockVideo.classList.add(videoClass);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -131,15 +131,8 @@ describe('Image utils', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should hide the img element and show the video instead if no picture element is present', () => {
|
it('should hide the img element and show the video instead if no picture element is present', () => {
|
||||||
const {
|
const { mockElement, mockImage, playSpy, mockVideo, mockSize, mockSizeUrls, mockSpoilerOverlay } =
|
||||||
mockElement,
|
createMockElements({
|
||||||
mockImage,
|
|
||||||
playSpy,
|
|
||||||
mockVideo,
|
|
||||||
mockSize,
|
|
||||||
mockSizeUrls,
|
|
||||||
mockSpoilerOverlay,
|
|
||||||
} = createMockElements({
|
|
||||||
extension: 'webm',
|
extension: 'webm',
|
||||||
videoClasses: ['hidden'],
|
videoClasses: ['hidden'],
|
||||||
});
|
});
|
||||||
|
@ -168,7 +161,7 @@ describe('Image utils', () => {
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
['data-size', 'data-uris'].forEach(missingAttributeName => {
|
['data-size', 'data-uris'].forEach((missingAttributeName) => {
|
||||||
it(`should return early if the ${missingAttributeName} attribute is missing`, () => {
|
it(`should return early if the ${missingAttributeName} attribute is missing`, () => {
|
||||||
const { mockElement } = createMockElements({
|
const { mockElement } = createMockElements({
|
||||||
extension: 'webm',
|
extension: 'webm',
|
||||||
|
@ -181,8 +174,7 @@ describe('Image utils', () => {
|
||||||
const result = showThumb(mockElement);
|
const result = showThumb(mockElement);
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
expect(jsonParseSpy).not.toHaveBeenCalled();
|
expect(jsonParseSpy).not.toHaveBeenCalled();
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
jsonParseSpy.mockRestore();
|
jsonParseSpy.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -226,13 +218,8 @@ describe('Image utils', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show the correct thumbnail image for jpg extension', () => {
|
it('should show the correct thumbnail image for jpg extension', () => {
|
||||||
const {
|
const { mockElement, mockSizeImage, mockSizeUrls, mockSize, mockSpoilerOverlay } =
|
||||||
mockElement,
|
createMockElementWithPicture('jpg');
|
||||||
mockSizeImage,
|
|
||||||
mockSizeUrls,
|
|
||||||
mockSize,
|
|
||||||
mockSpoilerOverlay,
|
|
||||||
} = createMockElementWithPicture('jpg');
|
|
||||||
const result = showThumb(mockElement);
|
const result = showThumb(mockElement);
|
||||||
|
|
||||||
expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]);
|
expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]);
|
||||||
|
@ -243,13 +230,8 @@ describe('Image utils', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show the correct thumbnail image for gif extension', () => {
|
it('should show the correct thumbnail image for gif extension', () => {
|
||||||
const {
|
const { mockElement, mockSizeImage, mockSizeUrls, mockSize, mockSpoilerOverlay } =
|
||||||
mockElement,
|
createMockElementWithPicture('gif');
|
||||||
mockSizeImage,
|
|
||||||
mockSizeUrls,
|
|
||||||
mockSize,
|
|
||||||
mockSpoilerOverlay,
|
|
||||||
} = createMockElementWithPicture('gif');
|
|
||||||
const result = showThumb(mockElement);
|
const result = showThumb(mockElement);
|
||||||
|
|
||||||
expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]);
|
expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]);
|
||||||
|
@ -260,13 +242,8 @@ describe('Image utils', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show the correct thumbnail image for webm extension', () => {
|
it('should show the correct thumbnail image for webm extension', () => {
|
||||||
const {
|
const { mockElement, mockSpoilerOverlay, mockSizeImage, mockSizeUrls, mockSize } =
|
||||||
mockElement,
|
createMockElementWithPicture('webm');
|
||||||
mockSpoilerOverlay,
|
|
||||||
mockSizeImage,
|
|
||||||
mockSizeUrls,
|
|
||||||
mockSize,
|
|
||||||
} = createMockElementWithPicture('webm');
|
|
||||||
const result = showThumb(mockElement);
|
const result = showThumb(mockElement);
|
||||||
|
|
||||||
expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize].replace('webm', 'gif'));
|
expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize].replace('webm', 'gif'));
|
||||||
|
@ -284,12 +261,10 @@ describe('Image utils', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkSrcsetAttribute = (size: ImageSize, x2size: ImageSize) => {
|
const checkSrcsetAttribute = (size: ImageSize, x2size: ImageSize) => {
|
||||||
const {
|
const { mockElement, mockSizeImage, mockSizeUrls, mockSpoilerOverlay } = createMockElementWithPicture(
|
||||||
mockElement,
|
'jpg',
|
||||||
mockSizeImage,
|
size,
|
||||||
mockSizeUrls,
|
);
|
||||||
mockSpoilerOverlay,
|
|
||||||
} = createMockElementWithPicture('jpg', size);
|
|
||||||
const result = showThumb(mockElement);
|
const result = showThumb(mockElement);
|
||||||
|
|
||||||
expect(mockSizeImage.src).toBe(mockSizeUrls[size]);
|
expect(mockSizeImage.src).toBe(mockSizeUrls[size]);
|
||||||
|
@ -312,12 +287,10 @@ describe('Image utils', () => {
|
||||||
|
|
||||||
it('should NOT set srcset on img if thumbUri is a gif at small size', () => {
|
it('should NOT set srcset on img if thumbUri is a gif at small size', () => {
|
||||||
const mockSize = 'small';
|
const mockSize = 'small';
|
||||||
const {
|
const { mockElement, mockSizeImage, mockSizeUrls, mockSpoilerOverlay } = createMockElementWithPicture(
|
||||||
mockElement,
|
'gif',
|
||||||
mockSizeImage,
|
mockSize,
|
||||||
mockSizeUrls,
|
);
|
||||||
mockSpoilerOverlay,
|
|
||||||
} = createMockElementWithPicture('gif', mockSize);
|
|
||||||
const result = showThumb(mockElement);
|
const result = showThumb(mockElement);
|
||||||
|
|
||||||
expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]);
|
expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]);
|
||||||
|
@ -336,12 +309,7 @@ describe('Image utils', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if img source already matches thumbUri', () => {
|
it('should return false if img source already matches thumbUri', () => {
|
||||||
const {
|
const { mockElement, mockSizeImage, mockSizeUrls, mockSize } = createMockElementWithPicture('jpg');
|
||||||
mockElement,
|
|
||||||
mockSizeImage,
|
|
||||||
mockSizeUrls,
|
|
||||||
mockSize,
|
|
||||||
} = createMockElementWithPicture('jpg');
|
|
||||||
mockSizeImage.src = mockSizeUrls[mockSize];
|
mockSizeImage.src = mockSizeUrls[mockSize];
|
||||||
const result = showThumb(mockElement);
|
const result = showThumb(mockElement);
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
|
@ -408,8 +376,7 @@ describe('Image utils', () => {
|
||||||
expect(querySelectorSpy).toHaveBeenCalledTimes(2);
|
expect(querySelectorSpy).toHaveBeenCalledTimes(2);
|
||||||
expect(querySelectorSpy).toHaveBeenNthCalledWith(1, 'picture');
|
expect(querySelectorSpy).toHaveBeenNthCalledWith(1, 'picture');
|
||||||
expect(querySelectorSpy).toHaveBeenNthCalledWith(2, 'video');
|
expect(querySelectorSpy).toHaveBeenNthCalledWith(2, 'video');
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
querySelectorSpy.mockRestore();
|
querySelectorSpy.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -430,8 +397,7 @@ describe('Image utils', () => {
|
||||||
expect(querySelectorSpy).toHaveBeenNthCalledWith(3, 'img');
|
expect(querySelectorSpy).toHaveBeenNthCalledWith(3, 'img');
|
||||||
expect(querySelectorSpy).toHaveBeenNthCalledWith(4, `.${spoilerOverlayClass}`);
|
expect(querySelectorSpy).toHaveBeenNthCalledWith(4, `.${spoilerOverlayClass}`);
|
||||||
expect(mockVideo).not.toHaveClass(hiddenClass);
|
expect(mockVideo).not.toHaveClass(hiddenClass);
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
querySelectorSpy.mockRestore();
|
querySelectorSpy.mockRestore();
|
||||||
pauseSpy.mockRestore();
|
pauseSpy.mockRestore();
|
||||||
}
|
}
|
||||||
|
@ -458,8 +424,7 @@ describe('Image utils', () => {
|
||||||
expect(mockVideo).toBeEmptyDOMElement();
|
expect(mockVideo).toBeEmptyDOMElement();
|
||||||
expect(mockVideo).toHaveClass(hiddenClass);
|
expect(mockVideo).toHaveClass(hiddenClass);
|
||||||
expect(pauseSpy).toHaveBeenCalled();
|
expect(pauseSpy).toHaveBeenCalled();
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
pauseSpy.mockRestore();
|
pauseSpy.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -482,8 +447,7 @@ describe('Image utils', () => {
|
||||||
expect(imgQuerySelectorSpy).toHaveBeenNthCalledWith(1, 'picture');
|
expect(imgQuerySelectorSpy).toHaveBeenNthCalledWith(1, 'picture');
|
||||||
expect(pictureQuerySelectorSpy).toHaveBeenNthCalledWith(1, 'img');
|
expect(pictureQuerySelectorSpy).toHaveBeenNthCalledWith(1, 'img');
|
||||||
expect(imgQuerySelectorSpy).toHaveBeenNthCalledWith(2, `.${spoilerOverlayClass}`);
|
expect(imgQuerySelectorSpy).toHaveBeenNthCalledWith(2, `.${spoilerOverlayClass}`);
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
imgQuerySelectorSpy.mockRestore();
|
imgQuerySelectorSpy.mockRestore();
|
||||||
pictureQuerySelectorSpy.mockRestore();
|
pictureQuerySelectorSpy.mockRestore();
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ describe('Local Autocompleter', () => {
|
||||||
let mockData: ArrayBuffer;
|
let mockData: ArrayBuffer;
|
||||||
const defaultK = 5;
|
const defaultK = 5;
|
||||||
|
|
||||||
beforeAll(async() => {
|
beforeAll(async () => {
|
||||||
const mockDataPath = join(__dirname, 'autocomplete-compiled-v2.bin');
|
const mockDataPath = join(__dirname, 'autocomplete-compiled-v2.bin');
|
||||||
/**
|
/**
|
||||||
* Read pre-generated binary autocomplete data
|
* Read pre-generated binary autocomplete data
|
||||||
|
@ -78,9 +78,7 @@ describe('Local Autocompleter', () => {
|
||||||
|
|
||||||
it('should return namespaced suggestions without including namespace', () => {
|
it('should return namespaced suggestions without including namespace', () => {
|
||||||
const result = localAc.topK('test', defaultK);
|
const result = localAc.topK('test', defaultK);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([expect.objectContaining({ name: 'artist:test', imageCount: 1 })]);
|
||||||
expect.objectContaining({ name: 'artist:test', imageCount: 1 }),
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return only the required number of suggestions', () => {
|
it('should return only the required number of suggestions', () => {
|
||||||
|
|
|
@ -29,7 +29,7 @@ describe('Request utils', () => {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-csrf-token': window.booru.csrfToken,
|
'x-csrf-token': window.booru.csrfToken,
|
||||||
'x-requested-with': 'xmlhttprequest'
|
'x-requested-with': 'xmlhttprequest',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -46,12 +46,12 @@ describe('Request utils', () => {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-csrf-token': window.booru.csrfToken,
|
'x-csrf-token': window.booru.csrfToken,
|
||||||
'x-requested-with': 'xmlhttprequest'
|
'x-requested-with': 'xmlhttprequest',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
...mockBody,
|
...mockBody,
|
||||||
_method: mockVerb
|
_method: mockVerb,
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -64,7 +64,7 @@ describe('Request utils', () => {
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: {
|
headers: {
|
||||||
'x-csrf-token': window.booru.csrfToken,
|
'x-csrf-token': window.booru.csrfToken,
|
||||||
'x-requested-with': 'xmlhttprequest'
|
'x-requested-with': 'xmlhttprequest',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -60,9 +60,11 @@ describe('Store utilities', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const initialValueKeys = Object.keys(initialValues) as (keyof typeof initialValues)[];
|
const initialValueKeys = Object.keys(initialValues) as (keyof typeof initialValues)[];
|
||||||
setStorageValue(initialValueKeys.reduce((acc, key) => {
|
setStorageValue(
|
||||||
|
initialValueKeys.reduce((acc, key) => {
|
||||||
return { ...acc, [key]: JSON.stringify(initialValues[key]) };
|
return { ...acc, [key]: JSON.stringify(initialValues[key]) };
|
||||||
}, {}));
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
initialValueKeys.forEach((key, i) => {
|
initialValueKeys.forEach((key, i) => {
|
||||||
const result = store.get(key);
|
const result = store.get(key);
|
||||||
|
@ -166,7 +168,11 @@ describe('Store utilities', () => {
|
||||||
|
|
||||||
expect(setItemSpy).toHaveBeenCalledTimes(2);
|
expect(setItemSpy).toHaveBeenCalledTimes(2);
|
||||||
expect(setItemSpy).toHaveBeenNthCalledWith(1, mockKey, JSON.stringify(mockValue));
|
expect(setItemSpy).toHaveBeenNthCalledWith(1, mockKey, JSON.stringify(mockValue));
|
||||||
expect(setItemSpy).toHaveBeenNthCalledWith(2, mockKey + lastUpdatedSuffix, JSON.stringify(initialDateNow + mockMaxAge));
|
expect(setItemSpy).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
mockKey + lastUpdatedSuffix,
|
||||||
|
JSON.stringify(initialDateNow + mockMaxAge),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ describe('Tag utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getHiddenTags', () => {
|
describe('getHiddenTags', () => {
|
||||||
it('should get a single hidden tag\'s information', () => {
|
it("should get a single hidden tag's information", () => {
|
||||||
window.booru.hiddenTagList = [1, 1];
|
window.booru.hiddenTagList = [1, 1];
|
||||||
|
|
||||||
const result = getHiddenTags();
|
const result = getHiddenTags();
|
||||||
|
@ -72,12 +72,7 @@ describe('Tag utilities', () => {
|
||||||
const result = getHiddenTags();
|
const result = getHiddenTags();
|
||||||
|
|
||||||
expect(result).toHaveLength(4);
|
expect(result).toHaveLength(4);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([mockTagInfo[3], mockTagInfo[2], mockTagInfo[1], mockTagInfo[4]]);
|
||||||
mockTagInfo[3],
|
|
||||||
mockTagInfo[2],
|
|
||||||
mockTagInfo[1],
|
|
||||||
mockTagInfo[4],
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -91,7 +86,7 @@ describe('Tag utilities', () => {
|
||||||
expect(result).toHaveLength(0);
|
expect(result).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get a single spoilered tag\'s information', () => {
|
it("should get a single spoilered tag's information", () => {
|
||||||
window.booru.spoileredTagList = [1, 1];
|
window.booru.spoileredTagList = [1, 1];
|
||||||
window.booru.ignoredTagList = [];
|
window.booru.ignoredTagList = [];
|
||||||
window.booru.spoilerType = getEnabledSpoilerType();
|
window.booru.spoilerType = getEnabledSpoilerType();
|
||||||
|
@ -110,12 +105,7 @@ describe('Tag utilities', () => {
|
||||||
const result = getSpoileredTags();
|
const result = getSpoileredTags();
|
||||||
|
|
||||||
expect(result).toHaveLength(4);
|
expect(result).toHaveLength(4);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([mockTagInfo[2], mockTagInfo[3], mockTagInfo[1], mockTagInfo[4]]);
|
||||||
mockTagInfo[2],
|
|
||||||
mockTagInfo[3],
|
|
||||||
mockTagInfo[1],
|
|
||||||
mockTagInfo[4],
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should omit ignored tags from the list', () => {
|
it('should omit ignored tags from the list', () => {
|
||||||
|
@ -125,10 +115,7 @@ describe('Tag utilities', () => {
|
||||||
const result = getSpoileredTags();
|
const result = getSpoileredTags();
|
||||||
|
|
||||||
expect(result).toHaveLength(2);
|
expect(result).toHaveLength(2);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([mockTagInfo[1], mockTagInfo[4]]);
|
||||||
mockTagInfo[1],
|
|
||||||
mockTagInfo[4],
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -140,10 +127,7 @@ describe('Tag utilities', () => {
|
||||||
|
|
||||||
const result = imageHitsTags(mockImage, [mockTagInfo[1], mockTagInfo[2], mockTagInfo[3], mockTagInfo[4]]);
|
const result = imageHitsTags(mockImage, [mockTagInfo[1], mockTagInfo[2], mockTagInfo[3], mockTagInfo[4]]);
|
||||||
expect(result).toHaveLength(mockImageTags.length);
|
expect(result).toHaveLength(mockImageTags.length);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([mockTagInfo[1], mockTagInfo[4]]);
|
||||||
mockTagInfo[1],
|
|
||||||
mockTagInfo[4],
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty array if data attribute is missing', () => {
|
it('should return empty array if data attribute is missing', () => {
|
||||||
|
@ -174,12 +158,16 @@ describe('Tag utilities', () => {
|
||||||
|
|
||||||
it('should return the correct value for two tags', () => {
|
it('should return the correct value for two tags', () => {
|
||||||
const result = displayTags([mockTagInfo[1], mockTagInfo[4]]);
|
const result = displayTags([mockTagInfo[1], mockTagInfo[4]]);
|
||||||
expect(result).toEqual(`${mockTagInfo[1].name}<span title="${mockTagInfo[4].name}">, ${mockTagInfo[4].name}</span>`);
|
expect(result).toEqual(
|
||||||
|
`${mockTagInfo[1].name}<span title="${mockTagInfo[4].name}">, ${mockTagInfo[4].name}</span>`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the correct value for three tags', () => {
|
it('should return the correct value for three tags', () => {
|
||||||
const result = displayTags([mockTagInfo[1], mockTagInfo[4], mockTagInfo[3]]);
|
const result = displayTags([mockTagInfo[1], mockTagInfo[4], mockTagInfo[3]]);
|
||||||
expect(result).toEqual(`${mockTagInfo[1].name}<span title="${mockTagInfo[4].name}, ${mockTagInfo[3].name}">, ${mockTagInfo[4].name}, ${mockTagInfo[3].name}</span>`);
|
expect(result).toEqual(
|
||||||
|
`${mockTagInfo[1].name}<span title="${mockTagInfo[4].name}, ${mockTagInfo[3].name}">, ${mockTagInfo[4].name}, ${mockTagInfo[3].name}</span>`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should escape HTML in the tag name', () => {
|
it('should escape HTML in the tag name', () => {
|
||||||
|
|
|
@ -5,54 +5,63 @@ type PhilomenaInputElements = HTMLTextAreaElement | HTMLInputElement | HTMLButto
|
||||||
/**
|
/**
|
||||||
* Get the first matching element
|
* Get the first matching element
|
||||||
*/
|
*/
|
||||||
export function $<E extends Element = Element>(selector: string, context: Pick<Document, 'querySelector'> = document): E | null {
|
export function $<E extends Element = Element>(
|
||||||
|
selector: string,
|
||||||
|
context: Pick<Document, 'querySelector'> = document,
|
||||||
|
): E | null {
|
||||||
return context.querySelector<E>(selector);
|
return context.querySelector<E>(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get every matching element as an array
|
* Get every matching element as an array
|
||||||
*/
|
*/
|
||||||
export function $$<E extends Element = Element>(selector: string, context: Pick<Document, 'querySelectorAll'> = document): E[] {
|
export function $$<E extends Element = Element>(
|
||||||
|
selector: string,
|
||||||
|
context: Pick<Document, 'querySelectorAll'> = document,
|
||||||
|
): E[] {
|
||||||
const elements = context.querySelectorAll<E>(selector);
|
const elements = context.querySelectorAll<E>(selector);
|
||||||
|
|
||||||
return [...elements];
|
return [...elements];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
export function showEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
||||||
([] as E[]).concat(...elements).forEach(el => el.classList.remove('hidden'));
|
([] as E[]).concat(...elements).forEach((el) => el.classList.remove('hidden'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hideEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
export function hideEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
||||||
([] as E[]).concat(...elements).forEach(el => el.classList.add('hidden'));
|
([] as E[]).concat(...elements).forEach((el) => el.classList.add('hidden'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
export function toggleEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
||||||
([] as E[]).concat(...elements).forEach(el => el.classList.toggle('hidden'));
|
([] as E[]).concat(...elements).forEach((el) => el.classList.toggle('hidden'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
export function clearEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
||||||
([] as E[]).concat(...elements).forEach(el => {
|
([] as E[]).concat(...elements).forEach((el) => {
|
||||||
while (el.firstChild) el.removeChild(el.firstChild);
|
while (el.firstChild) el.removeChild(el.firstChild);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function disableEl<E extends PhilomenaInputElements>(...elements: E[] | ConcatArray<E>[]) {
|
export function disableEl<E extends PhilomenaInputElements>(...elements: E[] | ConcatArray<E>[]) {
|
||||||
([] as E[]).concat(...elements).forEach(el => {
|
([] as E[]).concat(...elements).forEach((el) => {
|
||||||
el.disabled = true;
|
el.disabled = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enableEl<E extends PhilomenaInputElements>(...elements: E[] | ConcatArray<E>[]) {
|
export function enableEl<E extends PhilomenaInputElements>(...elements: E[] | ConcatArray<E>[]) {
|
||||||
([] as E[]).concat(...elements).forEach(el => {
|
([] as E[]).concat(...elements).forEach((el) => {
|
||||||
el.disabled = false;
|
el.disabled = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
export function removeEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
||||||
([] as E[]).concat(...elements).forEach(el => el.parentNode?.removeChild(el));
|
([] as E[]).concat(...elements).forEach((el) => el.parentNode?.removeChild(el));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeEl<Tag extends keyof HTMLElementTagNameMap>(tag: Tag, attr?: Partial<HTMLElementTagNameMap[Tag]>): HTMLElementTagNameMap[Tag] {
|
export function makeEl<Tag extends keyof HTMLElementTagNameMap>(
|
||||||
|
tag: Tag,
|
||||||
|
attr?: Partial<HTMLElementTagNameMap[Tag]>,
|
||||||
|
): HTMLElementTagNameMap[Tag] {
|
||||||
const el = document.createElement(tag);
|
const el = document.createElement(tag);
|
||||||
if (attr) {
|
if (attr) {
|
||||||
for (const prop in attr) {
|
for (const prop in attr) {
|
||||||
|
@ -65,8 +74,11 @@ export function makeEl<Tag extends keyof HTMLElementTagNameMap>(tag: Tag, attr?:
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onLeftClick(callback: (e: MouseEvent) => boolean | void, context: Pick<GlobalEventHandlers, 'addEventListener' | 'removeEventListener'> = document): VoidFunction {
|
export function onLeftClick(
|
||||||
const handler: typeof callback = event => {
|
callback: (e: MouseEvent) => boolean | void,
|
||||||
|
context: Pick<GlobalEventHandlers, 'addEventListener' | 'removeEventListener'> = document,
|
||||||
|
): VoidFunction {
|
||||||
|
const handler: typeof callback = (event) => {
|
||||||
if (event.button === 0) callback(event);
|
if (event.button === 0) callback(event);
|
||||||
};
|
};
|
||||||
context.addEventListener('click', handler);
|
context.addEventListener('click', handler);
|
||||||
|
@ -80,24 +92,19 @@ export function onLeftClick(callback: (e: MouseEvent) => boolean | void, context
|
||||||
export function whenReady(callback: VoidFunction): void {
|
export function whenReady(callback: VoidFunction): void {
|
||||||
if (document.readyState !== 'loading') {
|
if (document.readyState !== 'loading') {
|
||||||
callback();
|
callback();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
document.addEventListener('DOMContentLoaded', callback);
|
document.addEventListener('DOMContentLoaded', callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function escapeHtml(html: string): string {
|
export function escapeHtml(html: string): string {
|
||||||
return html.replace(/&/g, '&')
|
return html.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"');
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function escapeCss(css: string): string {
|
export function escapeCss(css: string): string {
|
||||||
return css.replace(/\\/g, '\\\\')
|
return css.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||||
.replace(/"/g, '\\"');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findFirstTextNode<N extends Node>(of: Node): N {
|
export function findFirstTextNode<N extends Node>(of: Node): N {
|
||||||
return Array.prototype.filter.call(of.childNodes, el => el.nodeType === Node.TEXT_NODE)[0];
|
return Array.prototype.filter.call(of.childNodes, (el) => el.nodeType === Node.TEXT_NODE)[0];
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,8 +47,7 @@ function drop(event: DragEvent, target: HTMLElement) {
|
||||||
|
|
||||||
if (event.clientX < detX) {
|
if (event.clientX < detX) {
|
||||||
target.insertAdjacentElement('beforebegin', dragSrcEl);
|
target.insertAdjacentElement('beforebegin', dragSrcEl);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
target.insertAdjacentElement('afterend', dragSrcEl);
|
target.insertAdjacentElement('afterend', dragSrcEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,18 +56,18 @@ function dragEnd(event: DragEvent, target: HTMLElement) {
|
||||||
clearDragSource();
|
clearDragSource();
|
||||||
|
|
||||||
if (target.parentNode) {
|
if (target.parentNode) {
|
||||||
$$('.over', target.parentNode).forEach(t => t.classList.remove('over'));
|
$$('.over', target.parentNode).forEach((t) => t.classList.remove('over'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initDraggables() {
|
export function initDraggables() {
|
||||||
const draggableSelector = '.drag-container [draggable]';
|
const draggableSelector = '.drag-container [draggable]';
|
||||||
delegate(document, 'dragstart', { [draggableSelector]: dragStart});
|
delegate(document, 'dragstart', { [draggableSelector]: dragStart });
|
||||||
delegate(document, 'dragover', { [draggableSelector]: dragOver});
|
delegate(document, 'dragover', { [draggableSelector]: dragOver });
|
||||||
delegate(document, 'dragenter', { [draggableSelector]: dragEnter});
|
delegate(document, 'dragenter', { [draggableSelector]: dragEnter });
|
||||||
delegate(document, 'dragleave', { [draggableSelector]: dragLeave});
|
delegate(document, 'dragleave', { [draggableSelector]: dragLeave });
|
||||||
delegate(document, 'dragend', { [draggableSelector]: dragEnd});
|
delegate(document, 'dragend', { [draggableSelector]: dragEnd });
|
||||||
delegate(document, 'drop', { [draggableSelector]: drop});
|
delegate(document, 'drop', { [draggableSelector]: drop });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearDragSource() {
|
export function clearDragSource() {
|
||||||
|
|
|
@ -3,16 +3,16 @@
|
||||||
import '../../types/ujs';
|
import '../../types/ujs';
|
||||||
|
|
||||||
export interface PhilomenaAvailableEventsMap {
|
export interface PhilomenaAvailableEventsMap {
|
||||||
dragstart: DragEvent,
|
dragstart: DragEvent;
|
||||||
dragover: DragEvent,
|
dragover: DragEvent;
|
||||||
dragenter: DragEvent,
|
dragenter: DragEvent;
|
||||||
dragleave: DragEvent,
|
dragleave: DragEvent;
|
||||||
dragend: DragEvent,
|
dragend: DragEvent;
|
||||||
drop: DragEvent,
|
drop: DragEvent;
|
||||||
click: MouseEvent,
|
click: MouseEvent;
|
||||||
submit: Event,
|
submit: Event;
|
||||||
reset: Event,
|
reset: Event;
|
||||||
fetchcomplete: FetchcompleteEvent
|
fetchcomplete: FetchcompleteEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PhilomenaEventElement {
|
export interface PhilomenaEventElement {
|
||||||
|
@ -20,7 +20,7 @@ export interface PhilomenaEventElement {
|
||||||
type: K,
|
type: K,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
listener: (this: Document | HTMLElement, ev: PhilomenaAvailableEventsMap[K]) => any,
|
listener: (this: Document | HTMLElement, ev: PhilomenaAvailableEventsMap[K]) => any,
|
||||||
options?: boolean | AddEventListenerOptions | undefined
|
options?: boolean | AddEventListenerOptions | undefined,
|
||||||
): void;
|
): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,21 +30,25 @@ export function fire<El extends Element, D>(el: El, event: string, detail: D) {
|
||||||
|
|
||||||
export function on<K extends keyof PhilomenaAvailableEventsMap>(
|
export function on<K extends keyof PhilomenaAvailableEventsMap>(
|
||||||
node: PhilomenaEventElement,
|
node: PhilomenaEventElement,
|
||||||
event: K, selector: string, func: ((e: PhilomenaAvailableEventsMap[K], target: Element) => boolean)
|
event: K,
|
||||||
|
selector: string,
|
||||||
|
func: (e: PhilomenaAvailableEventsMap[K], target: Element) => boolean,
|
||||||
) {
|
) {
|
||||||
delegate(node, event, { [selector]: func });
|
delegate(node, event, { [selector]: func });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function leftClick<E extends MouseEvent, Target extends EventTarget>(func: (e: E, t: Target) => void) {
|
export function leftClick<E extends MouseEvent, Target extends EventTarget>(func: (e: E, t: Target) => void) {
|
||||||
return (event: E, target: Target) => { if (event.button === 0) return func(event, target); };
|
return (event: E, target: Target) => {
|
||||||
|
if (event.button === 0) return func(event, target);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function delegate<K extends keyof PhilomenaAvailableEventsMap, Target extends Element>(
|
export function delegate<K extends keyof PhilomenaAvailableEventsMap, Target extends Element>(
|
||||||
node: PhilomenaEventElement,
|
node: PhilomenaEventElement,
|
||||||
event: K,
|
event: K,
|
||||||
selectors: Record<string, ((e: PhilomenaAvailableEventsMap[K], target: Target) => void | boolean)>
|
selectors: Record<string, (e: PhilomenaAvailableEventsMap[K], target: Target) => void | boolean>,
|
||||||
) {
|
) {
|
||||||
node.addEventListener(event, e => {
|
node.addEventListener(event, (e) => {
|
||||||
for (const selector in selectors) {
|
for (const selector in selectors) {
|
||||||
const evtTarget = e.target as EventTarget | Target | null;
|
const evtTarget = e.target as EventTarget | Target | null;
|
||||||
if (evtTarget && 'closest' in evtTarget && typeof evtTarget.closest === 'function') {
|
if (evtTarget && 'closest' in evtTarget && typeof evtTarget.closest === 'function') {
|
||||||
|
|
|
@ -53,8 +53,7 @@ export function showThumb(img: HTMLDivElement) {
|
||||||
if (uris[size].indexOf('.webm') !== -1) {
|
if (uris[size].indexOf('.webm') !== -1) {
|
||||||
overlay.classList.remove('hidden');
|
overlay.classList.remove('hidden');
|
||||||
overlay.innerHTML = 'WebM';
|
overlay.innerHTML = 'WebM';
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
overlay.classList.add('hidden');
|
overlay.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +117,9 @@ export function spoilerThumb(img: HTMLDivElement, spoilerUri: string, reason: st
|
||||||
|
|
||||||
switch (window.booru.spoilerType) {
|
switch (window.booru.spoilerType) {
|
||||||
case 'click':
|
case 'click':
|
||||||
img.addEventListener('click', event => { if (showThumb(img)) event.preventDefault(); });
|
img.addEventListener('click', (event) => {
|
||||||
|
if (showThumb(img)) event.preventDefault();
|
||||||
|
});
|
||||||
img.addEventListener('mouseleave', () => hideThumb(img, spoilerUri, reason));
|
img.addEventListener('mouseleave', () => hideThumb(img, spoilerUri, reason));
|
||||||
break;
|
break;
|
||||||
case 'hover':
|
case 'hover':
|
||||||
|
|
|
@ -5,8 +5,11 @@
|
||||||
// clamp the value between min and max, depending on whether
|
// clamp the value between min and max, depending on whether
|
||||||
// the delta >= 1 or <= 0.
|
// the delta >= 1 or <= 0.
|
||||||
export function lerp(delta: number, from: number, to: number): number {
|
export function lerp(delta: number, from: number, to: number): number {
|
||||||
if (delta >= 1) { return to; }
|
if (delta >= 1) {
|
||||||
else if (delta <= 0) { return from; }
|
return to;
|
||||||
|
} else if (delta <= 0) {
|
||||||
|
return from;
|
||||||
|
}
|
||||||
|
|
||||||
return from + (to - from) * delta;
|
return from + (to - from) * delta;
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@ export class LocalAutocompleter {
|
||||||
associations.push(this.view.getUint32(location + 1 + nameLength + 1 + i * 4, true));
|
associations.push(this.view.getUint32(location + 1 + nameLength + 1 + i * 4, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
return [ name, associations ];
|
return [name, associations];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -79,14 +79,14 @@ export class LocalAutocompleter {
|
||||||
getResultAt(i: number): [string, Result] {
|
getResultAt(i: number): [string, Result] {
|
||||||
const nameLocation = this.view.getUint32(this.referenceStart + i * 8, true);
|
const nameLocation = this.view.getUint32(this.referenceStart + i * 8, true);
|
||||||
const imageCount = this.view.getInt32(this.referenceStart + i * 8 + 4, true);
|
const imageCount = this.view.getInt32(this.referenceStart + i * 8 + 4, true);
|
||||||
const [ name, associations ] = this.getTagFromLocation(nameLocation);
|
const [name, associations] = this.getTagFromLocation(nameLocation);
|
||||||
|
|
||||||
if (imageCount < 0) {
|
if (imageCount < 0) {
|
||||||
// This is actually an alias, so follow it
|
// This is actually an alias, so follow it
|
||||||
return [ name, this.getResultAt(-imageCount - 1)[1] ];
|
return [name, this.getResultAt(-imageCount - 1)[1]];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [ name, { name, imageCount, associations } ];
|
return [name, { name, imageCount, associations }];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,7 +100,11 @@ export class LocalAutocompleter {
|
||||||
/**
|
/**
|
||||||
* Perform a binary search to fetch all results matching a condition.
|
* Perform a binary search to fetch all results matching a condition.
|
||||||
*/
|
*/
|
||||||
scanResults(getResult: (i: number) => [string, Result], compare: (name: string) => number, results: Record<string, Result>) {
|
scanResults(
|
||||||
|
getResult: (i: number) => [string, Result],
|
||||||
|
compare: (name: string) => number,
|
||||||
|
results: Record<string, Result>,
|
||||||
|
) {
|
||||||
const unfilter = store.get('unfilter_tag_suggestions');
|
const unfilter = store.get('unfilter_tag_suggestions');
|
||||||
|
|
||||||
let min = 0;
|
let min = 0;
|
||||||
|
@ -109,14 +113,13 @@ export class LocalAutocompleter {
|
||||||
const hiddenTags = window.booru.hiddenTagList;
|
const hiddenTags = window.booru.hiddenTagList;
|
||||||
|
|
||||||
while (min < max - 1) {
|
while (min < max - 1) {
|
||||||
const med = min + (max - min) / 2 | 0;
|
const med = (min + (max - min) / 2) | 0;
|
||||||
const sortKey = getResult(med)[0];
|
const sortKey = getResult(med)[0];
|
||||||
|
|
||||||
if (compare(sortKey) >= 0) {
|
if (compare(sortKey) >= 0) {
|
||||||
// too large, go left
|
// too large, go left
|
||||||
max = med;
|
max = med;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// too small, go right
|
// too small, go right
|
||||||
min = med;
|
min = med;
|
||||||
}
|
}
|
||||||
|
@ -124,13 +127,13 @@ export class LocalAutocompleter {
|
||||||
|
|
||||||
// Scan forward until no more matches occur
|
// Scan forward until no more matches occur
|
||||||
while (min < this.numTags - 1) {
|
while (min < this.numTags - 1) {
|
||||||
const [ sortKey, result ] = getResult(++min);
|
const [sortKey, result] = getResult(++min);
|
||||||
if (compare(sortKey) !== 0) {
|
if (compare(sortKey) !== 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add if not filtering or no associations are filtered
|
// Add if not filtering or no associations are filtered
|
||||||
if (unfilter || hiddenTags.findIndex(ht => result.associations.includes(ht)) === -1) {
|
if (unfilter || hiddenTags.findIndex((ht) => result.associations.includes(ht)) === -1) {
|
||||||
results[result.name] = result;
|
results[result.name] = result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export function fetchJson(verb: HttpMethod, endpoint: string, body?: Record<stri
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-csrf-token': window.booru.csrfToken,
|
'x-csrf-token': window.booru.csrfToken,
|
||||||
'x-requested-with': 'xmlhttprequest'
|
'x-requested-with': 'xmlhttprequest',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ export function fetchHtml(endpoint: string): Promise<Response> {
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: {
|
headers: {
|
||||||
'x-csrf-token': window.booru.csrfToken,
|
'x-csrf-token': window.booru.csrfToken,
|
||||||
'x-requested-with': 'xmlhttprequest'
|
'x-requested-with': 'xmlhttprequest',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,11 @@
|
||||||
export const lastUpdatedSuffix = '__lastUpdated';
|
export const lastUpdatedSuffix = '__lastUpdated';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
set(key: string, value: unknown) {
|
set(key: string, value: unknown) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(key, JSON.stringify(value));
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
return true;
|
return true;
|
||||||
}
|
} catch {
|
||||||
catch {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -21,8 +19,7 @@ export default {
|
||||||
if (value === null) return null;
|
if (value === null) return null;
|
||||||
try {
|
try {
|
||||||
return JSON.parse(value);
|
return JSON.parse(value);
|
||||||
}
|
} catch {
|
||||||
catch {
|
|
||||||
return value as unknown as Value;
|
return value as unknown as Value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -31,8 +28,7 @@ export default {
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
return true;
|
return true;
|
||||||
}
|
} catch {
|
||||||
catch {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -61,5 +57,4 @@ export default {
|
||||||
|
|
||||||
return lastUpdatedTime === null || Date.now() > lastUpdatedTime;
|
return lastUpdatedTime === null || Date.now() > lastUpdatedTime;
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,7 +30,7 @@ function sortTags(hidden: boolean, a: TagData, b: TagData): number {
|
||||||
|
|
||||||
export function getHiddenTags(): TagData[] {
|
export function getHiddenTags(): TagData[] {
|
||||||
return unique(window.booru.hiddenTagList)
|
return unique(window.booru.hiddenTagList)
|
||||||
.map(tagId => getTag(tagId))
|
.map((tagId) => getTag(tagId))
|
||||||
.sort(sortTags.bind(null, true));
|
.sort(sortTags.bind(null, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,8 +38,8 @@ export function getSpoileredTags(): TagData[] {
|
||||||
if (window.booru.spoilerType === 'off') return [];
|
if (window.booru.spoilerType === 'off') return [];
|
||||||
|
|
||||||
return unique(window.booru.spoileredTagList)
|
return unique(window.booru.spoileredTagList)
|
||||||
.filter(tagId => window.booru.ignoredTagList.indexOf(tagId) === -1)
|
.filter((tagId) => window.booru.ignoredTagList.indexOf(tagId) === -1)
|
||||||
.map(tagId => getTag(tagId))
|
.map((tagId) => getTag(tagId))
|
||||||
.sort(sortTags.bind(null, false));
|
.sort(sortTags.bind(null, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ export function imageHitsTags(img: HTMLElement, matchTags: TagData[]): TagData[]
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const imageTags = JSON.parse(imageTagsString);
|
const imageTags = JSON.parse(imageTagsString);
|
||||||
return matchTags.filter(t => imageTags.indexOf(t.id) !== -1);
|
return matchTags.filter((t) => imageTags.indexOf(t.id) !== -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function imageHitsComplex(img: HTMLElement, matchComplex: AstMatcher) {
|
export function imageHitsComplex(img: HTMLElement, matchComplex: AstMatcher) {
|
||||||
|
@ -57,11 +57,13 @@ export function imageHitsComplex(img: HTMLElement, matchComplex: AstMatcher) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function displayTags(tags: TagData[]): string {
|
export function displayTags(tags: TagData[]): string {
|
||||||
const mainTag = tags[0], otherTags = tags.slice(1);
|
const mainTag = tags[0],
|
||||||
let list = escapeHtml(mainTag.name), extras;
|
otherTags = tags.slice(1);
|
||||||
|
let list = escapeHtml(mainTag.name),
|
||||||
|
extras;
|
||||||
|
|
||||||
if (otherTags.length > 0) {
|
if (otherTags.length > 0) {
|
||||||
extras = otherTags.map(tag => escapeHtml(tag.name)).join(', ');
|
extras = otherTags.map((tag) => escapeHtml(tag.name)).join(', ');
|
||||||
list += `<span title="${extras}">, ${extras}</span>`;
|
list += `<span title="${extras}">, ${extras}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,6 @@ import { sizeGraphs } from './graph';
|
||||||
import { setupSliders } from './slider';
|
import { setupSliders } from './slider';
|
||||||
|
|
||||||
whenReady(() => {
|
whenReady(() => {
|
||||||
|
|
||||||
loadBooruData();
|
loadBooruData();
|
||||||
listenAutocomplete();
|
listenAutocomplete();
|
||||||
registerEvents();
|
registerEvents();
|
||||||
|
@ -67,5 +66,4 @@ whenReady(() => {
|
||||||
imageSourcesCreator();
|
imageSourcesCreator();
|
||||||
setupSliders();
|
setupSliders();
|
||||||
sizeGraphs();
|
sizeGraphs();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
345
assets/package-lock.json
generated
345
assets/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -14,11 +14,8 @@
|
||||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||||
"@types/postcss-mixins": "^9.0.5",
|
"@types/postcss-mixins": "^9.0.5",
|
||||||
"@types/web": "^0.0.148",
|
"@types/web": "^0.0.148",
|
||||||
"typescript-eslint": "8.0.0-alpha.39",
|
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^9.4.0",
|
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"postcss-mixins": "^10.0.1",
|
"postcss-mixins": "^10.0.1",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
@ -31,12 +28,16 @@
|
||||||
"@types/chai-dom": "^1.11.3",
|
"@types/chai-dom": "^1.11.3",
|
||||||
"@vitest/coverage-v8": "^1.6.0",
|
"@vitest/coverage-v8": "^1.6.0",
|
||||||
"chai": "^5",
|
"chai": "^5",
|
||||||
|
"eslint": "^9.4.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-vitest": "^0.5.4",
|
"eslint-plugin-vitest": "^0.5.4",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jsdom": "^24.1.0",
|
"jsdom": "^24.1.0",
|
||||||
"prettier": "^3.3.2",
|
"prettier": "^3.3.2",
|
||||||
"stylelint": "^16.6.1",
|
"stylelint": "^16.6.1",
|
||||||
"stylelint-config-standard": "^36.0.0",
|
"stylelint-config-standard": "^36.0.0",
|
||||||
"stylelint-prettier": "^5.0.0",
|
"stylelint-prettier": "^5.0.0",
|
||||||
|
"typescript-eslint": "8.0.0-alpha.39",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0",
|
||||||
"vitest-fetch-mock": "^0.2.2"
|
"vitest-fetch-mock": "^0.2.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ export function fixEventListeners(t: EventTarget) {
|
||||||
eventListeners = {};
|
eventListeners = {};
|
||||||
const oldAddEventListener = t.addEventListener;
|
const oldAddEventListener = t.addEventListener;
|
||||||
|
|
||||||
t.addEventListener = function(type: string, listener: any, options: any): void {
|
t.addEventListener = (type: string, listener: any, options: any): void => {
|
||||||
eventListeners[type] = eventListeners[type] || [];
|
eventListeners[type] = eventListeners[type] || [];
|
||||||
eventListeners[type].push(listener);
|
eventListeners[type].push(listener);
|
||||||
return oldAddEventListener(type, listener, options);
|
return oldAddEventListener(type, listener, options);
|
||||||
|
|
|
@ -2,7 +2,9 @@ import { MockInstance } from 'vitest';
|
||||||
|
|
||||||
type MockStorageKeys = 'getItem' | 'setItem' | 'removeItem';
|
type MockStorageKeys = 'getItem' | 'setItem' | 'removeItem';
|
||||||
|
|
||||||
export function mockStorage<Keys extends MockStorageKeys>(options: Pick<Storage, Keys>): { [k in `${Keys}Spy`]: MockInstance } {
|
export function mockStorage<Keys extends MockStorageKeys>(
|
||||||
|
options: Pick<Storage, Keys>,
|
||||||
|
): { [k in `${Keys}Spy`]: MockInstance } {
|
||||||
const getItemSpy = 'getItem' in options ? vi.spyOn(Storage.prototype, 'getItem') : undefined;
|
const getItemSpy = 'getItem' in options ? vi.spyOn(Storage.prototype, 'getItem') : undefined;
|
||||||
const setItemSpy = 'setItem' in options ? vi.spyOn(Storage.prototype, 'setItem') : undefined;
|
const setItemSpy = 'setItem' in options ? vi.spyOn(Storage.prototype, 'setItem') : undefined;
|
||||||
const removeItemSpy = 'removeItem' in options ? vi.spyOn(Storage.prototype, 'removeItem') : undefined;
|
const removeItemSpy = 'removeItem' in options ? vi.spyOn(Storage.prototype, 'removeItem') : undefined;
|
||||||
|
@ -33,18 +35,18 @@ type MockStorageImplApi = { [k in `${MockStorageKeys}Spy`]: MockInstance } & {
|
||||||
* Forces the mock storage back to its default (empty) state
|
* Forces the mock storage back to its default (empty) state
|
||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
clearStorage: VoidFunction,
|
clearStorage: VoidFunction;
|
||||||
/**
|
/**
|
||||||
* Forces the mock storage to be in the specific state provided as the parameter
|
* Forces the mock storage to be in the specific state provided as the parameter
|
||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
setStorageValue: (value: Record<string, string>) => void,
|
setStorageValue: (value: Record<string, string>) => void;
|
||||||
/**
|
/**
|
||||||
* Forces the mock storage to throw an error for the duration of the provided function's execution,
|
* Forces the mock storage to throw an error for the duration of the provided function's execution,
|
||||||
* or in case a promise is returned by the function, until that promise is resolved.
|
* or in case a promise is returned by the function, until that promise is resolved.
|
||||||
*/
|
*/
|
||||||
forceStorageError: <Args, Return>(func: (...args: Args[]) => Return | Promise<Return>) => void
|
forceStorageError: <Args, Return>(func: (...args: Args[]) => Return | Promise<Return>) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function mockStorageImpl(): MockStorageImplApi {
|
export function mockStorageImpl(): MockStorageImplApi {
|
||||||
let shouldThrow = false;
|
let shouldThrow = false;
|
||||||
|
@ -66,7 +68,7 @@ export function mockStorageImpl(): MockStorageImplApi {
|
||||||
delete tempStorage[key];
|
delete tempStorage[key];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const forceStorageError: MockStorageImplApi['forceStorageError'] = func => {
|
const forceStorageError: MockStorageImplApi['forceStorageError'] = (func) => {
|
||||||
shouldThrow = true;
|
shouldThrow = true;
|
||||||
const value = func();
|
const value = func();
|
||||||
if (!(value instanceof Promise)) {
|
if (!(value instanceof Promise)) {
|
||||||
|
@ -78,7 +80,7 @@ export function mockStorageImpl(): MockStorageImplApi {
|
||||||
shouldThrow = false;
|
shouldThrow = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const setStorageValue: MockStorageImplApi['setStorageValue'] = value => {
|
const setStorageValue: MockStorageImplApi['setStorageValue'] = (value) => {
|
||||||
tempStorage = value;
|
tempStorage = value;
|
||||||
};
|
};
|
||||||
const clearStorage = () => setStorageValue({});
|
const clearStorage = () => setStorageValue({});
|
||||||
|
|
|
@ -21,7 +21,7 @@ window.booru = {
|
||||||
spoileredFilter: matchNone(),
|
spoileredFilter: matchNone(),
|
||||||
interactions: [],
|
interactions: [],
|
||||||
tagsVersion: 5,
|
tagsVersion: 5,
|
||||||
galleryImages: []
|
galleryImages: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://github.com/jsdom/jsdom/issues/1721#issuecomment-1484202038
|
// https://github.com/jsdom/jsdom/issues/1721#issuecomment-1484202038
|
||||||
|
@ -31,6 +31,7 @@ Object.assign(globalThis, { URL, Blob });
|
||||||
|
|
||||||
// Prevents an error when calling `form.submit()` directly in
|
// Prevents an error when calling `form.submit()` directly in
|
||||||
// the code that is being tested
|
// the code that is being tested
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
HTMLFormElement.prototype.submit = function() {
|
HTMLFormElement.prototype.submit = function() {
|
||||||
fireEvent.submit(this);
|
fireEvent.submit(this);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@ export {};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface FetchcompleteEvent extends CustomEvent<Response> {
|
interface FetchcompleteEvent extends CustomEvent<Response> {
|
||||||
target: HTMLElement,
|
target: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
|
|
|
@ -11,18 +11,16 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
||||||
const isDev = command !== 'build' && mode !== 'test';
|
const isDev = command !== 'build' && mode !== 'test';
|
||||||
const targets = new Map();
|
const targets = new Map();
|
||||||
|
|
||||||
fs.readdirSync(path.resolve(__dirname, 'css/themes/')).forEach(name => {
|
fs.readdirSync(path.resolve(__dirname, 'css/themes/')).forEach((name) => {
|
||||||
const m = name.match(/([-a-z]+).css/);
|
const m = name.match(/([-a-z]+).css/);
|
||||||
|
|
||||||
if (m)
|
if (m) targets.set(`css/${m[1]}`, `./css/themes/${m[1]}.css`);
|
||||||
targets.set(`css/${m[1]}`, `./css/themes/${m[1]}.css`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.readdirSync(path.resolve(__dirname, 'css/options/')).forEach(name => {
|
fs.readdirSync(path.resolve(__dirname, 'css/options/')).forEach((name) => {
|
||||||
const m = name.match(/([-a-z]+).css/);
|
const m = name.match(/([-a-z]+).css/);
|
||||||
|
|
||||||
if (m)
|
if (m) targets.set(`css/options/${m[1]}`, `./css/options/${m[1]}.css`);
|
||||||
targets.set(`css/options/${m[1]}`, `./css/options/${m[1]}.css`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -37,8 +35,8 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
||||||
common: path.resolve(__dirname, 'css/common/'),
|
common: path.resolve(__dirname, 'css/common/'),
|
||||||
views: path.resolve(__dirname, 'css/views/'),
|
views: path.resolve(__dirname, 'css/views/'),
|
||||||
elements: path.resolve(__dirname, 'css/elements/'),
|
elements: path.resolve(__dirname, 'css/elements/'),
|
||||||
themes: path.resolve(__dirname, 'css/themes/')
|
themes: path.resolve(__dirname, 'css/themes/'),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
target: ['es2016', 'chrome67', 'firefox62', 'edge18', 'safari12'],
|
target: ['es2016', 'chrome67', 'firefox62', 'edge18', 'safari12'],
|
||||||
|
@ -51,19 +49,19 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
||||||
input: {
|
input: {
|
||||||
'js/app': './js/app.ts',
|
'js/app': './js/app.ts',
|
||||||
'css/application': './css/application.css',
|
'css/application': './css/application.css',
|
||||||
...Object.fromEntries(targets)
|
...Object.fromEntries(targets),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: '[name].js',
|
entryFileNames: '[name].js',
|
||||||
chunkFileNames: '[name].js',
|
chunkFileNames: '[name].js',
|
||||||
assetFileNames: '[name][extname]'
|
assetFileNames: '[name][extname]',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
postcss: {
|
postcss: {
|
||||||
plugins: [postcssMixins(), postcssSimpleVars(), postcssRelativeColor(), autoprefixer]
|
plugins: [postcssMixins(), postcssSimpleVars(), postcssRelativeColor(), autoprefixer],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
|
@ -74,11 +72,7 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
||||||
coverage: {
|
coverage: {
|
||||||
reporter: ['text', 'html'],
|
reporter: ['text', 'html'],
|
||||||
include: ['js/**/*.{js,ts}'],
|
include: ['js/**/*.{js,ts}'],
|
||||||
exclude: [
|
exclude: ['node_modules/', '.*\\.test\\.ts$', '.*\\.d\\.ts$'],
|
||||||
'node_modules/',
|
|
||||||
'.*\\.test\\.ts$',
|
|
||||||
'.*\\.d\\.ts$',
|
|
||||||
],
|
|
||||||
thresholds: {
|
thresholds: {
|
||||||
statements: 0,
|
statements: 0,
|
||||||
branches: 0,
|
branches: 0,
|
||||||
|
@ -90,8 +84,8 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
||||||
functions: 100,
|
functions: 100,
|
||||||
lines: 100,
|
lines: 100,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue