mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
convert array, dom and tag utils to typescript
This commit is contained in:
parent
c401695513
commit
0628eb0c23
12 changed files with 1208 additions and 8268 deletions
|
@ -2,12 +2,25 @@ import { $ } from './utils/dom';
|
||||||
import parseSearch from './match_query';
|
import parseSearch from './match_query';
|
||||||
import store from './utils/store';
|
import store from './utils/store';
|
||||||
|
|
||||||
/* Store a tag locally, marking the retrieval time */
|
/**
|
||||||
|
* Store a tag locally, marking the retrieval time
|
||||||
|
* @param {TagData} tagData
|
||||||
|
*/
|
||||||
function persistTag(tagData) {
|
function persistTag(tagData) {
|
||||||
tagData.fetchedAt = new Date().getTime() / 1000;
|
/**
|
||||||
store.set(`bor_tags_${tagData.id}`, tagData);
|
* @type {TagData}
|
||||||
|
*/
|
||||||
|
const persistData = {
|
||||||
|
...tagData,
|
||||||
|
fetchedAt: new Date().getTime() / 1000,
|
||||||
|
};
|
||||||
|
store.set(`bor_tags_${tagData.id}`, persistData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {TagData} tag
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
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);
|
||||||
|
@ -21,11 +34,31 @@ function clearTags() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Returns a single tag, or a dummy tag object if we don't know about it yet */
|
/**
|
||||||
|
* @param {unknown} value
|
||||||
|
* @returns {value is TagData}
|
||||||
|
*/
|
||||||
|
function isValidStoredTag(value) {
|
||||||
|
if ('id' in value && 'name' in value && 'images' in value && 'spoiler_image_uri' in value) {
|
||||||
|
return typeof value.id === 'number'
|
||||||
|
&& typeof value.name === 'string'
|
||||||
|
&& typeof value.images === 'number'
|
||||||
|
&& (value.spoiler_image_uri === null || typeof value.spoiler_image_uri === 'string')
|
||||||
|
&& (value.fetchedAt === null || typeof value.fetchedAt === 'number');
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a single tag, or a dummy tag object if we don't know about it yet
|
||||||
|
* @param {number} tagId
|
||||||
|
* @returns {TagData}
|
||||||
|
*/
|
||||||
function getTag(tagId) {
|
function getTag(tagId) {
|
||||||
const stored = store.get(`bor_tags_${tagId}`);
|
const stored = store.get(`bor_tags_${tagId}`);
|
||||||
|
|
||||||
if (stored) {
|
if (isValidStoredTag(stored)) {
|
||||||
return stored;
|
return stored;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,10 +67,14 @@ function getTag(tagId) {
|
||||||
name: '(unknown tag)',
|
name: '(unknown tag)',
|
||||||
images: 0,
|
images: 0,
|
||||||
spoiler_image_uri: null,
|
spoiler_image_uri: null,
|
||||||
|
fetchedAt: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fetches lots of tags in batches and stores them locally */
|
/**
|
||||||
|
* Fetches lots of tags in batches and stores them locally
|
||||||
|
* @param {number[]} tagIds
|
||||||
|
*/
|
||||||
function fetchAndPersistTags(tagIds) {
|
function fetchAndPersistTags(tagIds) {
|
||||||
if (!tagIds.length) return;
|
if (!tagIds.length) return;
|
||||||
|
|
||||||
|
@ -50,7 +87,10 @@ function fetchAndPersistTags(tagIds) {
|
||||||
.then(() => fetchAndPersistTags(remaining));
|
.then(() => fetchAndPersistTags(remaining));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Figure out which tags in the list we don't know about */
|
/**
|
||||||
|
* Figure out which tags in the list we don't know about
|
||||||
|
* @param {number[]} tagIds
|
||||||
|
*/
|
||||||
function fetchNewOrStaleTags(tagIds) {
|
function fetchNewOrStaleTags(tagIds) {
|
||||||
const fetchIds = [];
|
const fetchIds = [];
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
import { $$ } from './utils/dom';
|
import { $$ } from './utils/dom';
|
||||||
import store from './utils/store';
|
import store from './utils/store';
|
||||||
import { initTagDropdown} from './tags';
|
import { initTagDropdown } from './tags';
|
||||||
import { setupTagsInput, reloadTagsInput } from './tagsinput';
|
import { setupTagsInput, reloadTagsInput } from './tagsinput';
|
||||||
|
|
||||||
function tagInputButtons({target}) {
|
function tagInputButtons({target}) {
|
||||||
|
|
|
@ -44,6 +44,7 @@ describe('Array Utilities', () => {
|
||||||
moveElement(input, 2, 1);
|
moveElement(input, 2, 1);
|
||||||
expect(input).toEqual(['a', 'c', 'b', 'd']);
|
expect(input).toEqual(['a', 'c', 'b', 'd']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with ascending index parameters', () => {
|
it('should work with ascending index parameters', () => {
|
||||||
const input = ['a', 'b', 'c', 'd'];
|
const input = ['a', 'b', 'c', 'd'];
|
||||||
moveElement(input, 1, 2);
|
moveElement(input, 1, 2);
|
||||||
|
@ -88,50 +89,48 @@ describe('Array Utilities', () => {
|
||||||
['', null, false, uniqueValue, mockObject, Infinity, undefined]
|
['', null, false, uniqueValue, mockObject, Infinity, undefined]
|
||||||
)).toBe(true);
|
)).toBe(true);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should return true for matching up to the first array\'s length', () => {
|
describe('negative cases', () => {
|
||||||
|
it('should NOT return true for matching only up to the first array\'s length', () => {
|
||||||
// Numbers
|
// Numbers
|
||||||
expect(arraysEqual([0], [0, 1])).toBe(true);
|
expect(arraysEqual([0], [0, 1])).toBe(false);
|
||||||
expect(arraysEqual([0, 1], [0, 1, 2])).toBe(true);
|
expect(arraysEqual([0, 1], [0, 1, 2])).toBe(false);
|
||||||
|
|
||||||
// Strings
|
// Strings
|
||||||
expect(arraysEqual(['a'], ['a', 'b'])).toBe(true);
|
expect(arraysEqual(['a'], ['a', 'b'])).toBe(false);
|
||||||
expect(arraysEqual(['a', 'b'], ['a', 'b', 'c'])).toBe(true);
|
expect(arraysEqual(['a', 'b'], ['a', 'b', 'c'])).toBe(false);
|
||||||
|
|
||||||
// Object by reference
|
// Object by reference
|
||||||
const uniqueValue1 = Symbol('item1');
|
const uniqueValue1 = Symbol('item1');
|
||||||
const uniqueValue2 = Symbol('item2');
|
const uniqueValue2 = Symbol('item2');
|
||||||
expect(arraysEqual([uniqueValue1], [uniqueValue1, uniqueValue2])).toBe(true);
|
expect(arraysEqual([uniqueValue1], [uniqueValue1, uniqueValue2])).toBe(false);
|
||||||
|
|
||||||
// Mixed parameters
|
// Mixed parameters
|
||||||
const mockObject = { value: Math.random() };
|
const mockObject = { value: Math.random() };
|
||||||
expect(arraysEqual(
|
expect(arraysEqual(
|
||||||
[''],
|
[''],
|
||||||
['', null, false, mockObject, Infinity, undefined]
|
['', null, false, mockObject, Infinity, undefined]
|
||||||
)).toBe(true);
|
)).toBe(false);
|
||||||
expect(arraysEqual(
|
expect(arraysEqual(
|
||||||
['', null],
|
['', null],
|
||||||
['', null, false, mockObject, Infinity, undefined]
|
['', null, false, mockObject, Infinity, undefined]
|
||||||
)).toBe(true);
|
)).toBe(false);
|
||||||
expect(arraysEqual(
|
expect(arraysEqual(
|
||||||
['', null, false],
|
['', null, false],
|
||||||
['', null, false, mockObject, Infinity, undefined]
|
['', null, false, mockObject, Infinity, undefined]
|
||||||
)).toBe(true);
|
)).toBe(false);
|
||||||
expect(arraysEqual(
|
expect(arraysEqual(
|
||||||
['', null, false, mockObject],
|
['', null, false, mockObject],
|
||||||
['', null, false, mockObject, Infinity, undefined]
|
['', null, false, mockObject, Infinity, undefined]
|
||||||
)).toBe(true);
|
)).toBe(false);
|
||||||
expect(arraysEqual(
|
expect(arraysEqual(
|
||||||
['', null, false, mockObject, Infinity],
|
['', null, false, mockObject, Infinity],
|
||||||
['', null, false, mockObject, Infinity, undefined]
|
['', null, false, mockObject, Infinity, undefined]
|
||||||
)).toBe(true);
|
)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('negative cases', () => {
|
it('should return false for arrays of different length', () => {
|
||||||
// FIXME This case should be handled
|
|
||||||
// eslint-disable-next-line jest/no-disabled-tests
|
|
||||||
it.skip('should return false for arrays of different length', () => {
|
|
||||||
// Numbers
|
// Numbers
|
||||||
expect(arraysEqual([], [0])).toBe(false);
|
expect(arraysEqual([], [0])).toBe(false);
|
||||||
expect(arraysEqual([0], [])).toBe(false);
|
expect(arraysEqual([0], [])).toBe(false);
|
||||||
|
|
|
@ -245,11 +245,6 @@ describe('DOM Utilities', () => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if element has no parent', () => {
|
|
||||||
const detachedElement = document.createElement('div');
|
|
||||||
expect(() => removeEl(detachedElement)).toThrow(/propert(y|ies).*null/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call the native removeElement method on parent', () => {
|
it('should call the native removeElement method on parent', () => {
|
||||||
const parentNode = document.createElement('div');
|
const parentNode = document.createElement('div');
|
||||||
const childNode = document.createElement('p');
|
const childNode = document.createElement('p');
|
||||||
|
@ -310,6 +305,7 @@ describe('DOM Utilities', () => {
|
||||||
expect(mockParent.children[0].tagName).toBe('STRONG');
|
expect(mockParent.children[0].tagName).toBe('STRONG');
|
||||||
expect(mockParent.children[1].tagName).toBe('SPAN');
|
expect(mockParent.children[1].tagName).toBe('SPAN');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should insert between two elements', () => {
|
it('should insert between two elements', () => {
|
||||||
const mockParent = document.createElement('p');
|
const mockParent = document.createElement('p');
|
||||||
const mockFirstExisingElement = document.createElement('span');
|
const mockFirstExisingElement = document.createElement('span');
|
||||||
|
@ -325,15 +321,6 @@ describe('DOM Utilities', () => {
|
||||||
expect(mockParent.children[1].tagName).toBe('STRONG');
|
expect(mockParent.children[1].tagName).toBe('STRONG');
|
||||||
expect(mockParent.children[2].tagName).toBe('EM');
|
expect(mockParent.children[2].tagName).toBe('EM');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if there is no parent', () => {
|
|
||||||
const mockParent = document.createElement('p');
|
|
||||||
const mockNewElement = document.createElement('em');
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
insertBefore(mockParent, mockNewElement);
|
|
||||||
}).toThrow(/propert(y|ies).*null/);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onLeftClick', () => {
|
describe('onLeftClick', () => {
|
||||||
|
|
|
@ -1,48 +1,45 @@
|
||||||
import { displayTags, getHiddenTags, getSpoileredTags, imageHitsComplex, imageHitsTags } from '../tag';
|
import { displayTags, getHiddenTags, getSpoileredTags, imageHitsComplex, imageHitsTags, TagData } from '../tag';
|
||||||
import { mockStorage } from '../../../test/mock-storage';
|
import { mockStorage } from '../../../test/mock-storage';
|
||||||
import { getRandomArrayItem } from '../../../test/randomness';
|
import { getRandomArrayItem } from '../../../test/randomness';
|
||||||
import parseSearch from '../../match_query';
|
import parseSearch from '../../match_query';
|
||||||
|
|
||||||
// TODO Move to source file when rewriting in TypeScript
|
|
||||||
interface StorageTagInfo {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
images: number;
|
|
||||||
spoiler_image_uri: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Tag utilities', () => {
|
describe('Tag utilities', () => {
|
||||||
const tagStorageKeyPrefix = 'bor_tags_';
|
const tagStorageKeyPrefix = 'bor_tags_';
|
||||||
const mockTagInfo: Record<string, StorageTagInfo> = {
|
const mockTagInfo: Record<string, TagData> = {
|
||||||
1: {
|
1: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'safe',
|
name: 'safe',
|
||||||
images: 69,
|
images: 69,
|
||||||
spoiler_image_uri: null,
|
spoiler_image_uri: null,
|
||||||
|
fetchedAt: null,
|
||||||
},
|
},
|
||||||
2: {
|
2: {
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'fox',
|
name: 'fox',
|
||||||
images: 1,
|
images: 1,
|
||||||
spoiler_image_uri: '/mock-fox-spoiler-image.svg',
|
spoiler_image_uri: '/mock-fox-spoiler-image.svg',
|
||||||
|
fetchedAt: null,
|
||||||
},
|
},
|
||||||
3: {
|
3: {
|
||||||
id: 3,
|
id: 3,
|
||||||
name: 'paw pads',
|
name: 'paw pads',
|
||||||
images: 42,
|
images: 42,
|
||||||
spoiler_image_uri: '/mock-paw-pads-spoiler-image.svg',
|
spoiler_image_uri: '/mock-paw-pads-spoiler-image.svg',
|
||||||
|
fetchedAt: null,
|
||||||
},
|
},
|
||||||
4: {
|
4: {
|
||||||
id: 4,
|
id: 4,
|
||||||
name: 'whiskers',
|
name: 'whiskers',
|
||||||
images: 42,
|
images: 42,
|
||||||
spoiler_image_uri: null,
|
spoiler_image_uri: null,
|
||||||
|
fetchedAt: null,
|
||||||
},
|
},
|
||||||
5: {
|
5: {
|
||||||
id: 5,
|
id: 5,
|
||||||
name: 'lilo & stitch',
|
name: 'lilo & stitch',
|
||||||
images: 6,
|
images: 6,
|
||||||
spoiler_image_uri: null,
|
spoiler_image_uri: null,
|
||||||
|
fetchedAt: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const getEnabledSpoilerType = () => getRandomArrayItem<SpoilerType>(['click', 'hover', 'static']);
|
const getEnabledSpoilerType = () => getRandomArrayItem<SpoilerType>(['click', 'hover', 'static']);
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
// http://stackoverflow.com/a/5306832/1726690
|
|
||||||
export function moveElement(array, from, to) {
|
|
||||||
array.splice(to, 0, array.splice(from, 1)[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function arraysEqual(array1, array2) {
|
|
||||||
for (let i = 0; i < array1.length; ++i) {
|
|
||||||
if (array1[i] !== array2[i]) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
10
assets/js/utils/array.ts
Normal file
10
assets/js/utils/array.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// http://stackoverflow.com/a/5306832/1726690
|
||||||
|
export function moveElement<Items>(array: Items[], from: number, to: number): void {
|
||||||
|
array.splice(to, 0, array.splice(from, 1)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arraysEqual(array1: unknown[], array2: unknown[]): boolean {
|
||||||
|
if (array1.length !== array2.length) return false;
|
||||||
|
|
||||||
|
return array1.every((item, index) => item === array2[index]);
|
||||||
|
}
|
|
@ -1,74 +0,0 @@
|
||||||
/**
|
|
||||||
* DOM Utils
|
|
||||||
*/
|
|
||||||
|
|
||||||
function $(selector, context = document) { // Get the first matching element
|
|
||||||
const element = context.querySelector(selector);
|
|
||||||
|
|
||||||
return element || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function $$(selector, context = document) { // Get every matching element as an array
|
|
||||||
const elements = context.querySelectorAll(selector);
|
|
||||||
|
|
||||||
return [].slice.call(elements);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showEl(...elements) {
|
|
||||||
[].concat(...elements).forEach(el => el.classList.remove('hidden'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideEl(...elements) {
|
|
||||||
[].concat(...elements).forEach(el => el.classList.add('hidden'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleEl(...elements) {
|
|
||||||
[].concat(...elements).forEach(el => el.classList.toggle('hidden'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearEl(...elements) {
|
|
||||||
[].concat(...elements).forEach(el => { while (el.firstChild) el.removeChild(el.firstChild); });
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeEl(...elements) {
|
|
||||||
[].concat(...elements).forEach(el => el.parentNode.removeChild(el));
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeEl(tag, attr = {}) {
|
|
||||||
const el = document.createElement(tag);
|
|
||||||
for (const prop in attr) el[prop] = attr[prop];
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertBefore(existingElement, newElement) {
|
|
||||||
existingElement.parentNode.insertBefore(newElement, existingElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLeftClick(callback, context = document) {
|
|
||||||
context.addEventListener('click', event => {
|
|
||||||
if (event.button === 0) callback(event);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function whenReady(callback) { // Execute a function when the DOM is ready
|
|
||||||
if (document.readyState !== 'loading') callback();
|
|
||||||
else document.addEventListener('DOMContentLoaded', callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(html) {
|
|
||||||
return html.replace(/&/g, '&')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeCss(css) {
|
|
||||||
return css.replace(/\\/g, '\\\\')
|
|
||||||
.replace(/"/g, '\\"');
|
|
||||||
}
|
|
||||||
|
|
||||||
function findFirstTextNode(of) {
|
|
||||||
return Array.prototype.filter.call(of.childNodes, el => el.nodeType === Node.TEXT_NODE)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export { $, $$, showEl, hideEl, toggleEl, clearEl, removeEl, makeEl, insertBefore, onLeftClick, whenReady, escapeHtml, escapeCss, findFirstTextNode };
|
|
95
assets/js/utils/dom.ts
Normal file
95
assets/js/utils/dom.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
/**
|
||||||
|
* DOM Utils
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first matching element
|
||||||
|
*/
|
||||||
|
export function $<E extends Element = Element>(selector: string, context: Pick<Document, 'querySelector'> = document): E | null {
|
||||||
|
return context.querySelector<E>(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get every matching element as an array
|
||||||
|
*/
|
||||||
|
export function $$<E extends Element = Element>(selector: string, context: Pick<Document, 'querySelectorAll'> = document): E[] {
|
||||||
|
const elements = context.querySelectorAll<E>(selector);
|
||||||
|
|
||||||
|
return [...elements];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
||||||
|
([] as E[]).concat(...elements).forEach(el => el.classList.remove('hidden'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
||||||
|
([] as E[]).concat(...elements).forEach(el => el.classList.add('hidden'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
||||||
|
([] as E[]).concat(...elements).forEach(el => el.classList.toggle('hidden'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
||||||
|
([] as E[]).concat(...elements).forEach(el => {
|
||||||
|
while (el.firstChild) el.removeChild(el.firstChild);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeEl<E extends HTMLElement>(...elements: E[] | ConcatArray<E>[]) {
|
||||||
|
([] 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] {
|
||||||
|
const el = document.createElement(tag);
|
||||||
|
if (attr) {
|
||||||
|
for (const prop in attr) {
|
||||||
|
const newValue = attr[prop];
|
||||||
|
if (typeof newValue !== 'undefined') {
|
||||||
|
el[prop] = newValue as Exclude<typeof newValue, undefined>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insertBefore(existingElement: HTMLElement, newElement: HTMLElement) {
|
||||||
|
existingElement.parentNode?.insertBefore(newElement, existingElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onLeftClick(callback: (e: MouseEvent) => boolean | void, context: Pick<GlobalEventHandlers, 'addEventListener' | 'removeEventListener'> = document): VoidFunction {
|
||||||
|
const handler: typeof callback = event => {
|
||||||
|
if (event.button === 0) callback(event);
|
||||||
|
};
|
||||||
|
context.addEventListener('click', handler);
|
||||||
|
|
||||||
|
return () => context.removeEventListener('click', handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a function when the DOM is ready
|
||||||
|
*/
|
||||||
|
export function whenReady(callback: VoidFunction): void {
|
||||||
|
if (document.readyState !== 'loading') {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
document.addEventListener('DOMContentLoaded', callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeHtml(html: string): string {
|
||||||
|
return html.replace(/&/g, '&')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeCss(css: string): string {
|
||||||
|
return css.replace(/\\/g, '\\\\')
|
||||||
|
.replace(/"/g, '\\"');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findFirstTextNode<N extends Node>(of: Node): N {
|
||||||
|
return Array.prototype.filter.call(of.childNodes, el => el.nodeType === Node.TEXT_NODE)[0];
|
||||||
|
}
|
|
@ -1,11 +1,19 @@
|
||||||
import { escapeHtml } from './dom';
|
import { escapeHtml } from './dom';
|
||||||
import { getTag } from '../booru';
|
import { getTag } from '../booru';
|
||||||
|
|
||||||
function unique(array) {
|
export interface TagData {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
images: number;
|
||||||
|
spoiler_image_uri: string | null;
|
||||||
|
fetchedAt: null | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unique<Item>(array: Item[]): Item[] {
|
||||||
return array.filter((a, b, c) => c.indexOf(a) === b);
|
return array.filter((a, b, c) => c.indexOf(a) === b);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortTags(hidden, a, b) {
|
function sortTags(hidden: boolean, a: TagData, b: TagData): number {
|
||||||
// If both tags have a spoiler image, sort by images count desc (hidden) or asc (spoilered)
|
// If both tags have a spoiler image, sort by images count desc (hidden) or asc (spoilered)
|
||||||
if (a.spoiler_image_uri && b.spoiler_image_uri) {
|
if (a.spoiler_image_uri && b.spoiler_image_uri) {
|
||||||
return hidden ? b.images - a.images : a.images - b.images;
|
return hidden ? b.images - a.images : a.images - b.images;
|
||||||
|
@ -34,16 +42,20 @@ export function getSpoileredTags() {
|
||||||
.sort(sortTags.bind(null, false));
|
.sort(sortTags.bind(null, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function imageHitsTags(img, matchTags) {
|
export function imageHitsTags(img: HTMLImageElement, matchTags: TagData[]): TagData[] {
|
||||||
const imageTags = JSON.parse(img.dataset.imageTags);
|
const imageTagsString = img.dataset.imageTags;
|
||||||
|
if (typeof imageTagsString === 'undefined') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
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, matchComplex) {
|
export function imageHitsComplex(img: HTMLImageElement, matchComplex: { hitsImage: (img: HTMLImageElement) => boolean }) {
|
||||||
return matchComplex.hitsImage(img);
|
return matchComplex.hitsImage(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function displayTags(tags) {
|
export function displayTags(tags: TagData[]): string {
|
||||||
const mainTag = tags[0], otherTags = tags.slice(1);
|
const mainTag = tags[0], otherTags = tags.slice(1);
|
||||||
let list = escapeHtml(mainTag.name), extras;
|
let list = escapeHtml(mainTag.name), extras;
|
||||||
|
|
9142
assets/package-lock.json
generated
9142
assets/package-lock.json
generated
File diff suppressed because it is too large
Load diff
1
assets/types/booru-object.d.ts
vendored
1
assets/types/booru-object.d.ts
vendored
|
@ -51,6 +51,7 @@ interface BooruObject {
|
||||||
* @type {import('../js/match_query.js').SearchAST}
|
* @type {import('../js/match_query.js').SearchAST}
|
||||||
*/
|
*/
|
||||||
spoileredFilter: unknown;
|
spoileredFilter: unknown;
|
||||||
|
tagsVersion: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|
Loading…
Reference in a new issue