port a bunch of stuff to typescript (untested)

This commit is contained in:
Luna D. 2024-06-06 18:48:16 +02:00
parent b6973eb437
commit 8ca3902005
No known key found for this signature in database
GPG key ID: 4B1C63448394F688
27 changed files with 431 additions and 370 deletions

View file

@ -14,6 +14,7 @@ import './when-ready';
// Would typically be either the theme file, or any additional file // Would typically be either the theme file, or any additional file
// you later intend to put in the <link> tag. // you later intend to put in the <link> tag.
// import '../css/themes/default.scss'; import '../css/application.css';
import '../css/themes/dark-blue.css';
// import '../css/themes/dark.scss'; // import '../css/themes/dark.scss';
// import '../css/themes/red.scss'; // import '../css/themes/red.scss';

View file

@ -1,18 +1,20 @@
import { delegate, leftClick } from './utils/events'; import { delegate, leftClick } from './utils/events';
import { clearEl, makeEl } from './utils/dom'; import { clearEl, makeEl } from './utils/dom';
function insertCaptcha(_event, target) { function insertCaptcha(_event: Event, target: HTMLInputElement) {
const { parentNode, dataset: { sitekey } } = target; const { parentElement, dataset: { sitekey } } = target;
if (!parentElement) { return; }
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 = sitekey; frame.dataset.sitekey = sitekey;
clearEl(parentNode); clearEl(parentElement);
parentNode.insertAdjacentElement('beforeend', frame); parentElement.insertAdjacentElement('beforeend', frame);
parentNode.insertAdjacentElement('beforeend', script); parentElement.insertAdjacentElement('beforeend', script);
} }
export function bindCaptchaLinks() { export function bindCaptchaLinks() {

View file

@ -3,24 +3,21 @@
*/ */
import { $ } from './utils/dom'; import { $ } from './utils/dom';
import { showOwnedComments } from './communications/comment';
import { filterNode } from './imagesclientside'; import { filterNode } from './imagesclientside';
import { fetchHtml } from './utils/requests'; import { fetchHtml } from './utils/requests';
import { timeAgo } from './timeago'; import { timeAgo } from './timeago';
function handleError(response) { function handleError(response) {
const errorMessage = '<div>Comment failed to load!</div>'; const errorMessage = '<div>Comment failed to load!</div>';
if (!response.ok) { if (!response.ok) {
return errorMessage; return errorMessage;
} }
return response.text();
return response.text();
} }
function commentPosted(response) { function commentPosted(response) {
const commentEditTab = $('#js-comment-form a[data-click-tab="write"]'), const commentEditTab = $('#js-comment-form a[data-click-tab="write"]'),
commentEditForm = $('#js-comment-form'), commentEditForm = $('#js-comment-form'),
container = document.getElementById('comments'), container = document.getElementById('comments'),
@ -43,11 +40,9 @@ function commentPosted(response) {
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)
} }
} }
function loadParentPost(event) { function loadParentPost(event) {
const clickedLink = event.target, const clickedLink = event.target,
// Find the comment containing the link that was clicked // Find the comment containing the link that was clicked
fullComment = clickedLink.closest('article.block'), fullComment = clickedLink.closest('article.block'),
@ -62,7 +57,6 @@ 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;
@ -74,13 +68,10 @@ function loadParentPost(event) {
}); });
return true; return true;
} }
} }
function insertParentPost(data, clickedLink, fullComment) { function insertParentPost(data, clickedLink, fullComment) {
// Add the 'subthread' class to the comment with the clicked link // Add the 'subthread' class to the comment with the clicked link
fullComment.classList.add('subthread'); fullComment.classList.add('subthread');
@ -99,11 +90,9 @@ function insertParentPost(data, clickedLink, fullComment) {
// Filter images (if any) in the loaded comment // Filter images (if any) in the loaded comment
filterNode(fullComment.previousSibling); filterNode(fullComment.previousSibling);
} }
function clearParentPost(clickedLink, fullComment) { function clearParentPost(clickedLink, fullComment) {
// Remove any previous siblings with the class fetched-comment // Remove any previous siblings with the class fetched-comment
while (fullComment.previousSibling && fullComment.previousSibling.classList.contains('fetched-comment')) { while (fullComment.previousSibling && fullComment.previousSibling.classList.contains('fetched-comment')) {
fullComment.previousSibling.parentNode.removeChild(fullComment.previousSibling); fullComment.previousSibling.parentNode.removeChild(fullComment.previousSibling);
@ -118,11 +107,9 @@ function clearParentPost(clickedLink, fullComment) {
if (!fullComment.classList.contains('fetched-comment')) { if (!fullComment.classList.contains('fetched-comment')) {
fullComment.classList.remove('subthread'); fullComment.classList.remove('subthread');
} }
} }
function displayComments(container, commentsHtml) { function displayComments(container, commentsHtml) {
container.innerHTML = commentsHtml; container.innerHTML = commentsHtml;
// Execute timeago on comments // Execute timeago on comments
@ -130,14 +117,9 @@ function displayComments(container, commentsHtml) {
// Filter images in the comments // Filter images in the comments
filterNode(container); filterNode(container);
// Show options on own comments
showOwnedComments();
} }
function loadComments(event) { 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]+)/),
@ -147,7 +129,6 @@ function loadComments(event) {
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
@ -159,7 +140,6 @@ function loadComments(event) {
}); });
return true; return true;
} }
function setupComments() { function setupComments() {
@ -175,7 +155,6 @@ function setupComments() {
} }
else { else {
filterNode(comments); filterNode(comments);
showOwnedComments();
} }
} }

View file

@ -1,10 +0,0 @@
import { $ } from '../utils/dom';
function showOwnedComments() {
const editableComments = $('.js-editable-comments');
const editableCommentIds = editableComments && JSON.parse(editableComments.dataset.editable);
if (editableCommentIds) editableCommentIds.forEach(id => $(`#comment_${id} .owner-options`).classList.remove('hidden'));
}
export { showOwnedComments };

View file

@ -1,10 +0,0 @@
import { $ } from '../utils/dom';
function showOwnedPosts() {
const editablePost = $('.js-editable-posts');
const editablePostIds = editablePost && JSON.parse(editablePost.dataset.editable);
if (editablePostIds) editablePostIds.forEach(id => $(`#post_${id} .owner-options`).classList.remove('hidden'));
}
export { showOwnedPosts };

View file

@ -1,42 +0,0 @@
/**
* Interactive behavior for duplicate reports.
*/
import { $, $$ } from './utils/dom';
function setupDupeReports() {
const [ onion, slider ] = $$('.onion-skin__image, .onion-skin__slider');
const swipe = $('.swipe__image');
if (swipe) setupSwipe(swipe);
if (onion) setupOnionSkin(onion, slider);
}
function setupSwipe(swipe) {
const [ clip, divider ] = $$('#clip rect, #divider', swipe);
const { width } = swipe.viewBox.baseVal;
function moveDivider({ clientX }) {
// Move center to cursor
const rect = swipe.getBoundingClientRect();
const newX = (clientX - rect.left) * (width / rect.width);
divider.setAttribute('x', newX);
clip.setAttribute('width', newX);
}
swipe.addEventListener('mousemove', moveDivider);
}
function setupOnionSkin(onion, slider) {
const target = $('#target', onion);
function setOpacity() {
target.setAttribute('opacity', slider.value);
}
setOpacity();
slider.addEventListener('input', setOpacity);
}
export { setupDupeReports };

View file

@ -0,0 +1,45 @@
/**
* Interactive behavior for duplicate reports.
*/
import { $, $$ } from './utils/dom';
function setupDupeReports() {
const onion = $<SVGSVGElement>('.onion-skin__image');
const slider = $<HTMLInputElement>('.onion-skin__slider');
const swipe = $<SVGSVGElement>('.swipe__image');
if (swipe) setupSwipe(swipe);
if (onion && slider) setupOnionSkin(onion, slider);
}
function setupSwipe(swipe: SVGSVGElement) {
const [ clip, divider ] = $$<SVGRectElement>('#clip rect, #divider', swipe);
const { width } = swipe.viewBox.baseVal;
function moveDivider({ clientX }: MouseEvent) {
// Move center to cursor
const rect = swipe.getBoundingClientRect();
const newX = (clientX - rect.left) * (width / rect.width);
divider.setAttribute('x', newX.toString());
clip.setAttribute('width', newX.toString());
}
swipe.addEventListener('mousemove', moveDivider);
}
function setupOnionSkin(onion: SVGSVGElement, slider: HTMLInputElement) {
const target = $<HTMLImageElement>('#target', onion);
function setOpacity() {
if (target) {
target.setAttribute('opacity', slider.value);
}
}
setOpacity();
slider.addEventListener('input', setOpacity);
}
export { setupDupeReports };

View file

@ -19,9 +19,9 @@ interface RealUserAgentData {
} }
interface RealNavigator extends Navigator { interface RealNavigator extends Navigator {
deviceMemory: number, deviceMemory: number | null,
keyboard: RealKeyboard, keyboard: RealKeyboard | null,
userAgentData: RealUserAgentData, userAgentData: RealUserAgentData | null,
} }
/** /**
@ -81,7 +81,7 @@ async function createFp(): Promise<string> {
} }
let width: string | null = store.get('cached_rem_size'); let width: string | null = store.get('cached_rem_size');
const body = $('body'); const body = $<HTMLBodyElement>('body');
if (!width && body) { if (!width && body) {
const testElement = document.createElement('span'); const testElement = document.createElement('span');
@ -127,7 +127,7 @@ async function createFp(): Promise<string> {
/** /**
* Sets the `_ses` cookie. * Sets the `_ses` cookie.
* *
* If `cached_ses_value` is present in local storage, uses that instead. * If `cached_ses_value` is present in local storage, uses it to set the `_ses` cookie.
* Otherwise if the `_ses` cookie already exits, uses its value instead. * Otherwise if the `_ses` cookie already exits, uses its value instead.
* Otherwise attempts to generate a new value for the `_ses` cookie * Otherwise attempts to generate a new value for the `_ses` cookie
* based on various browser attributes. * based on various browser attributes.

View file

@ -8,11 +8,13 @@ import { initDraggables } from './utils/draggable';
import { fetchJson } from './utils/requests'; import { fetchJson } from './utils/requests';
export function setupGalleryEditing() { export function setupGalleryEditing() {
if (!$('.rearrange-button')) return; if (!$<HTMLElement>('.rearrange-button')) return;
const [ rearrangeEl, saveEl ] = $$('.rearrange-button'); const [ rearrangeEl, saveEl ] = $$<HTMLElement>('.rearrange-button');
const sortableEl = $('#sortable'); const sortableEl = $<HTMLDivElement>('#sortable');
const containerEl = $('.media-list'); const containerEl = $<HTMLDivElement>('.media-list');
if (!sortableEl || !containerEl || !saveEl || !rearrangeEl) { return; }
// Copy array // Copy array
let oldImages = window.booru.galleryImages.slice(); let oldImages = window.booru.galleryImages.slice();
@ -20,7 +22,7 @@ export function setupGalleryEditing() {
initDraggables(); initDraggables();
$$('.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');
@ -31,15 +33,17 @@ export function setupGalleryEditing() {
sortableEl.classList.remove('editing'); sortableEl.classList.remove('editing');
containerEl.classList.remove('drag-container'); containerEl.classList.remove('drag-container');
newImages = $$('.image-container', containerEl).map(i => parseInt(i.dataset.imageId, 10)); newImages = $$<HTMLDivElement>('.image-container', containerEl).map(i => parseInt(i.dataset.imageId || '-1', 10));
// If nothing changed, don't bother. // If nothing changed, don't bother.
if (arraysEqual(newImages, oldImages)) return; if (arraysEqual(newImages, oldImages)) return;
if (saveEl.dataset.reorderPath) {
fetchJson('PATCH', saveEl.dataset.reorderPath, { fetchJson('PATCH', saveEl.dataset.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());
}
}); });
} }

View file

@ -1,76 +0,0 @@
/**
* Client-side image filtering/spoilering.
*/
import { $$, escapeHtml } from './utils/dom';
import { setupInteractions } from './interactions';
import { showThumb, showBlock, spoilerThumb, spoilerBlock, hideThumb } from './utils/image';
import { getHiddenTags, getSpoileredTags, imageHitsTags, imageHitsComplex, displayTags } from './utils/tag';
function runFilter(img, test, runCallback) {
if (!test || test.length === 0) return false;
runCallback(img, test);
// I don't like this.
window.booru.imagesWithDownvotingDisabled.push(img.dataset.imageId);
return true;
}
// ---
function filterThumbSimple(img, tagsHit) { hideThumb(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, `[HIDDEN] ${displayTags(tagsHit)}`); }
function spoilerThumbSimple(img, tagsHit) { spoilerThumb(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, displayTags(tagsHit)); }
function filterThumbComplex(img) { hideThumb(img, window.booru.hiddenTag, '[HIDDEN] <i>(Complex Filter)</i>'); }
function spoilerThumbComplex(img) { spoilerThumb(img, window.booru.hiddenTag, '<i>(Complex Filter)</i>'); }
function filterBlockSimple(img, tagsHit) { spoilerBlock(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, `This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is hidden by `); }
function spoilerBlockSimple(img, tagsHit) { spoilerBlock(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, `This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is spoilered by `); }
function filterBlockComplex(img) { spoilerBlock(img, window.booru.hiddenTag, 'This image was hidden by a complex tag expression in '); }
function spoilerBlockComplex(img) { spoilerBlock(img, window.booru.hiddenTag, 'This image was spoilered by a complex tag expression in '); }
// ---
function thumbTagFilter(tags, img) { return runFilter(img, imageHitsTags(img, tags), filterThumbSimple); }
function thumbComplexFilter(complex, img) { return runFilter(img, imageHitsComplex(img, complex), filterThumbComplex); }
function thumbTagSpoiler(tags, img) { return runFilter(img, imageHitsTags(img, tags), spoilerThumbSimple); }
function thumbComplexSpoiler(complex, img) { return runFilter(img, imageHitsComplex(img, complex), spoilerThumbComplex); }
function blockTagFilter(tags, img) { return runFilter(img, imageHitsTags(img, tags), filterBlockSimple); }
function blockComplexFilter(complex, img) { return runFilter(img, imageHitsComplex(img, complex), filterBlockComplex); }
function blockTagSpoiler(tags, img) { return runFilter(img, imageHitsTags(img, tags), spoilerBlockSimple); }
function blockComplexSpoiler(complex, img) { return runFilter(img, imageHitsComplex(img, complex), spoilerBlockComplex); }
// ---
function filterNode(node = document) {
const hiddenTags = getHiddenTags(), spoileredTags = getSpoileredTags();
const { hiddenFilter, spoileredFilter } = window.booru;
// Image thumb boxes with vote and fave buttons on them
$$('.image-container', node)
.filter(img => !thumbTagFilter(hiddenTags, img))
.filter(img => !thumbComplexFilter(hiddenFilter, img))
.filter(img => !thumbTagSpoiler(spoileredTags, img))
.filter(img => !thumbComplexSpoiler(spoileredFilter, img))
.forEach(img => showThumb(img));
// Individual image pages and images in posts/comments
$$('.image-show-container', node)
.filter(img => !blockTagFilter(hiddenTags, img))
.filter(img => !blockComplexFilter(hiddenFilter, img))
.filter(img => !blockTagSpoiler(spoileredTags, img))
.filter(img => !blockComplexSpoiler(spoileredFilter, img))
.forEach(img => showBlock(img));
}
function initImagesClientside() {
window.booru.imagesWithDownvotingDisabled = [];
// This fills the imagesWithDownvotingDisabled array
filterNode(document);
// Once the array is populated, we can initialize interactions
setupInteractions();
}
export { initImagesClientside, filterNode };

View file

@ -0,0 +1,131 @@
/**
* Client-side image filtering/spoilering.
*/
import { $$, escapeHtml } from './utils/dom';
import { setupInteractions } from './interactions';
import { showThumb, showBlock, spoilerThumb, spoilerBlock, hideThumb } from './utils/image';
import { TagData, getHiddenTags, getSpoileredTags, imageHitsTags, imageHitsComplex, displayTags } from './utils/tag';
import { AstMatcher } from './query/types';
type RunFilterCallback = (img: HTMLDivElement, test: TagData[]) => void;
function runFilter(img: HTMLDivElement, test: TagData[] | boolean, runCallback: RunFilterCallback) {
if (!test || typeof test !== 'boolean' && test.length === 0) { return false; }
runCallback(img, test as TagData[]);
// I don't like this.
img.dataset.imageId && window.booru.imagesWithDownvotingDisabled.push(img.dataset.imageId);
return true;
}
// ---
function filterThumbSimple(img: HTMLDivElement, tagsHit: TagData[]) {
hideThumb(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, `[HIDDEN] ${displayTags(tagsHit)}`);
}
function spoilerThumbSimple(img: HTMLDivElement, tagsHit: TagData[]) {
spoilerThumb(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, displayTags(tagsHit));
}
function filterThumbComplex(img: HTMLDivElement) {
hideThumb(img, window.booru.hiddenTag, '[HIDDEN] <i>(Complex Filter)</i>');
}
function spoilerThumbComplex(img: HTMLDivElement) {
spoilerThumb(img, window.booru.hiddenTag, '<i>(Complex Filter)</i>');
}
function filterBlockSimple(img: HTMLDivElement, tagsHit: TagData[]) {
spoilerBlock(
img,
tagsHit[0].spoiler_image_uri || window.booru.hiddenTag,
`This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is hidden by `
);
}
function spoilerBlockSimple(img: HTMLDivElement, tagsHit: TagData[]) {
spoilerBlock(
img,
tagsHit[0].spoiler_image_uri || window.booru.hiddenTag,
`This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is spoilered by `
);
}
function filterBlockComplex(img: HTMLDivElement) {
spoilerBlock(img, window.booru.hiddenTag, 'This image was hidden by a complex tag expression in ');
}
function spoilerBlockComplex(img: HTMLDivElement) {
spoilerBlock(img, window.booru.hiddenTag, 'This image was spoilered by a complex tag expression in ');
}
// ---
function thumbTagFilter(tags: TagData[], img: HTMLDivElement) {
return runFilter(img, imageHitsTags(img, tags), filterThumbSimple);
}
function thumbComplexFilter(complex: AstMatcher, img: HTMLDivElement) {
return runFilter(img, imageHitsComplex(img, complex), filterThumbComplex);
}
function thumbTagSpoiler(tags: TagData[], img: HTMLDivElement) {
return runFilter(img, imageHitsTags(img, tags), spoilerThumbSimple);
}
function thumbComplexSpoiler(complex: AstMatcher, img: HTMLDivElement) {
return runFilter(img, imageHitsComplex(img, complex), spoilerThumbComplex);
}
function blockTagFilter(tags: TagData[], img: HTMLDivElement) {
return runFilter(img, imageHitsTags(img, tags), filterBlockSimple);
}
function blockComplexFilter(complex: AstMatcher, img: HTMLDivElement) {
return runFilter(img, imageHitsComplex(img, complex), filterBlockComplex);
}
function blockTagSpoiler(tags: TagData[], img: HTMLDivElement) {
return runFilter(img, imageHitsTags(img, tags), spoilerBlockSimple);
}
function blockComplexSpoiler(complex: AstMatcher, img: HTMLDivElement) {
return runFilter(img, imageHitsComplex(img, complex), spoilerBlockComplex);
}
// ---
function filterNode(node: Pick<Document, 'querySelectorAll'>) {
const hiddenTags = getHiddenTags(), spoileredTags = getSpoileredTags();
const { hiddenFilter, spoileredFilter } = window.booru;
// Image thumb boxes with vote and fave buttons on them
$$<HTMLDivElement>('.image-container', node)
.filter(img => !thumbTagFilter(hiddenTags, img))
.filter(img => !thumbComplexFilter(hiddenFilter, img))
.filter(img => !thumbTagSpoiler(spoileredTags, img))
.filter(img => !thumbComplexSpoiler(spoileredFilter, img))
.forEach(img => showThumb(img));
// Individual image pages and images in posts/comments
$$<HTMLDivElement>('.image-show-container', node)
.filter(img => !blockTagFilter(hiddenTags, img))
.filter(img => !blockComplexFilter(hiddenFilter, img))
.filter(img => !blockTagSpoiler(spoileredTags, img))
.filter(img => !blockComplexSpoiler(spoileredFilter, img))
.forEach(img => showBlock(img));
}
function initImagesClientside() {
window.booru.imagesWithDownvotingDisabled = [];
// This fills the imagesWithDownvotingDisabled array
filterNode(document);
// Once the array is populated, we can initialize interactions
setupInteractions();
}
export { initImagesClientside, filterNode };

View file

@ -7,8 +7,8 @@
import { $ } from './utils/dom'; import { $ } from './utils/dom';
function warnAboutPMs() { function warnAboutPMs() {
const textarea = $('.js-toolbar-input'); const textarea = $<HTMLTextAreaElement>('.js-toolbar-input');
const warning = $('.js-hidden-warning'); const warning = $<HTMLDivElement>('.js-hidden-warning');
const imageEmbedRegex = /!+\[/g; const imageEmbedRegex = /!+\[/g;
if (!warning || !textarea) return; if (!warning || !textarea) return;

View file

@ -1,6 +1,6 @@
import { inputDuplicatorCreator } from './input-duplicator'; import { inputDuplicatorCreator } from './input-duplicator';
function pollOptionCreator() { export function pollOptionCreator() {
inputDuplicatorCreator({ inputDuplicatorCreator({
addButtonSelector: '.js-poll-add-option', addButtonSelector: '.js-poll-add-option',
fieldSelector: '.js-poll-option', fieldSelector: '.js-poll-option',
@ -8,5 +8,3 @@ function pollOptionCreator() {
removeButtonSelector: '.js-option-remove', removeButtonSelector: '.js-option-remove',
}); });
} }
export { pollOptionCreator };

View file

@ -1,45 +0,0 @@
import { $, $$ } from './utils/dom';
import { addTag } from './tagsinput';
function showHelp(subject, type) {
$$('[data-search-help]').forEach(helpBox => {
if (helpBox.getAttribute('data-search-help') === type) {
$('.js-search-help-subject', helpBox).textContent = subject;
helpBox.classList.remove('hidden');
}
else {
helpBox.classList.add('hidden');
}
});
}
function prependToLast(field, value) {
const separatorIndex = field.value.lastIndexOf(',');
const advanceBy = field.value[separatorIndex + 1] === ' ' ? 2 : 1;
field.value = field.value.slice(0, separatorIndex + advanceBy) + value + field.value.slice(separatorIndex + advanceBy);
}
function selectLast(field, characterCount) {
field.focus();
field.selectionStart = field.value.length - characterCount;
field.selectionEnd = field.value.length;
}
function executeFormHelper(e) {
const searchField = $('.js-search-field');
const attr = name => e.target.getAttribute(name);
attr('data-search-add') && addTag(searchField, attr('data-search-add'));
attr('data-search-show-help') && showHelp(e.target.textContent, attr('data-search-show-help'));
attr('data-search-select-last') && selectLast(searchField, parseInt(attr('data-search-select-last'), 10));
attr('data-search-prepend') && prependToLast(searchField, attr('data-search-prepend'));
}
function setupSearch() {
const form = $('.js-search-form');
form && form.addEventListener('click', executeFormHelper);
}
export { setupSearch };

50
assets/js/search.ts Normal file
View file

@ -0,0 +1,50 @@
import { $, $$ } from './utils/dom';
import { addTag } from './tagsinput';
function showHelp(subject: string, type: string | null) {
$$<HTMLElement>('[data-search-help]').forEach(helpBox => {
if (helpBox.getAttribute('data-search-help') === type) {
const searchSubject = $<HTMLElement>('.js-search-help-subject', helpBox);
if (searchSubject) {
searchSubject.textContent = subject;
}
helpBox.classList.remove('hidden');
}
else {
helpBox.classList.add('hidden');
}
});
}
function prependToLast(field: HTMLInputElement, value: string) {
const separatorIndex = field.value.lastIndexOf(',');
const advanceBy = field.value[separatorIndex + 1] === ' ' ? 2 : 1;
field.value = field.value.slice(0, separatorIndex + advanceBy) + value + field.value.slice(separatorIndex + advanceBy);
}
function selectLast(field: HTMLInputElement, characterCount: number) {
field.focus();
field.selectionStart = field.value.length - characterCount;
field.selectionEnd = field.value.length;
}
function executeFormHelper(e: PointerEvent) {
if (!e.target) { return; }
const searchField = $<HTMLInputElement>('.js-search-field');
const attr = (name: string) => e.target && (e.target as HTMLElement).getAttribute(name);
attr('data-search-add') && addTag(searchField, attr('data-search-add'));
attr('data-search-show-help') && showHelp((e.target as Node).textContent || '', attr('data-search-show-help'));
attr('data-search-select-last') && searchField && selectLast(searchField, parseInt(attr('data-search-select-last') || '', 10));
attr('data-search-prepend') && searchField && prependToLast(searchField, attr('data-search-prepend') || '');
}
export function setupSearch() {
const form = $<HTMLInputElement>('.js-search-form');
form && form.addEventListener('click', executeFormHelper as EventListener);
}

View file

@ -6,12 +6,11 @@ import { $, $$ } from './utils/dom';
import store from './utils/store'; import store from './utils/store';
export function setupSettings() { export function setupSettings() {
if (!$<HTMLElement>('#js-setting-table')) return;
if (!$('#js-setting-table')) return; const localCheckboxes = $$<HTMLInputElement>('[data-tab="local"] input[type="checkbox"]');
const themeSelect = $<HTMLSelectElement>('#user_theme');
const localCheckboxes = $$('[data-tab="local"] input[type="checkbox"]'); const styleSheet = $<HTMLLinkElement>('head link[rel="stylesheet"]');
const themeSelect = $('#user_theme');
const styleSheet = $('head link[rel="stylesheet"]');
// Local settings // Local settings
localCheckboxes.forEach(checkbox => { localCheckboxes.forEach(checkbox => {
@ -22,7 +21,8 @@ export function setupSettings() {
// Theme preview // Theme preview
themeSelect && themeSelect.addEventListener('change', () => { themeSelect && themeSelect.addEventListener('change', () => {
styleSheet.href = themeSelect.options[themeSelect.selectedIndex].dataset.themePath; if (styleSheet) {
styleSheet.href = themeSelect.options[themeSelect.selectedIndex].dataset.themePath || '#';
}
}); });
} }

View file

@ -1,62 +0,0 @@
/**
* Keyboard shortcuts
*/
import { $ } from './utils/dom';
function getHover() {
const thumbBoxHover = $('.media-box:hover');
if (thumbBoxHover) return thumbBoxHover.dataset.imageId;
}
function openFullView() {
const imageHover = $('[data-uris]:hover');
if (!imageHover) return;
window.location = JSON.parse(imageHover.dataset.uris).full;
}
function openFullViewNewTab() {
const imageHover = $('[data-uris]:hover');
if (!imageHover) return;
window.open(JSON.parse(imageHover.dataset.uris).full);
}
function click(selector) {
const el = $(selector);
if (el) el.click();
}
function isOK(event) {
return !event.altKey && !event.ctrlKey && !event.metaKey &&
document.activeElement.tagName !== 'INPUT' &&
document.activeElement.tagName !== 'TEXTAREA';
}
const keyCodes = {
74() { click('.js-prev'); }, // J - go to previous image
73() { click('.js-up'); }, // I - go to index page
75() { click('.js-next'); }, // K - go to next image
82() { click('.js-rand'); }, // R - go to random image
83() { click('.js-source-link'); }, // S - go to image source
76() { click('.js-tag-sauce-toggle'); }, // L - edit tags
79() { openFullView(); }, // O - open original
86() { openFullViewNewTab(); }, // V - open original in a new tab
70() { // F - favourite image
getHover() ? click(`a.interaction--fave[data-image-id="${getHover()}"]`)
: click('.block__header a.interaction--fave');
},
85() { // U - upvote image
getHover() ? click(`a.interaction--upvote[data-image-id="${getHover()}"]`)
: click('.block__header a.interaction--upvote');
},
};
function listenForKeys() {
document.addEventListener('keydown', event => {
if (isOK(event) && keyCodes[event.keyCode]) { keyCodes[event.keyCode](); event.preventDefault(); }
});
}
export { listenForKeys };

76
assets/js/shortcuts.ts Normal file
View file

@ -0,0 +1,76 @@
/**
* Keyboard shortcuts
*/
import { $ } from './utils/dom';
interface ShortcutKeycodes {
[key: string]: () => void
}
function getHover(): string | null {
const thumbBoxHover = $<HTMLDivElement>('.media-box:hover');
return thumbBoxHover && (thumbBoxHover.dataset.imageId || null);
}
function openFullView() {
const imageHover = $<HTMLDivElement>('[data-uris]:hover');
if (!imageHover || !imageHover.dataset.uris) return;
window.location = JSON.parse(imageHover.dataset.uris).full;
}
function openFullViewNewTab() {
const imageHover = $<HTMLDivElement>('[data-uris]:hover');
if (!imageHover || !imageHover.dataset.uris) return;
window.open(JSON.parse(imageHover.dataset.uris).full);
}
function click(selector: string) {
const el = $<HTMLElement>(selector);
if (el) {
el.click();
}
}
function isOK(event: KeyboardEvent): boolean {
return !event.altKey && !event.ctrlKey && !event.metaKey &&
document.activeElement !== null &&
document.activeElement.tagName !== 'INPUT' &&
document.activeElement.tagName !== 'TEXTAREA';
}
const keyCodes: ShortcutKeycodes = {
KeyJ() { click('.js-prev'); }, // J - go to previous image
KeyI() { click('.js-up'); }, // I - go to index page
KeyK() { click('.js-next'); }, // K - go to next image
KeyR() { click('.js-rand'); }, // R - go to random image
KeyS() { click('.js-source-link'); }, // S - go to image source
KeyL() { click('.js-tag-sauce-toggle'); }, // L - edit tags
KeyO() { openFullView(); }, // O - open original
KeyV() { openFullViewNewTab(); }, // V - open original in a new tab
KeyF() { // F - favourite image
getHover() ? click(`a.interaction--fave[data-image-id="${getHover()}"]`)
: click('.block__header a.interaction--fave');
},
KeyU() { // U - upvote image
getHover() ? click(`a.interaction--upvote[data-image-id="${getHover()}"]`)
: click('.block__header a.interaction--upvote');
},
};
function listenForKeys() {
document.addEventListener('keydown', (event: KeyboardEvent) => {
if (isOK(event) && keyCodes[event.code]) {
keyCodes[event.code]();
event.preventDefault();
}
});
}
export { listenForKeys };

View file

@ -1,5 +1,10 @@
import { $ } from './utils/dom';
import { inputDuplicatorCreator } from './input-duplicator'; import { inputDuplicatorCreator } from './input-duplicator';
export interface SourcesEvent extends CustomEvent<Response> {
target: HTMLElement,
}
function setupInputs() { function setupInputs() {
inputDuplicatorCreator({ inputDuplicatorCreator({
addButtonSelector: '.js-image-add-source', addButtonSelector: '.js-image-add-source',
@ -11,16 +16,17 @@ function setupInputs() {
function imageSourcesCreator() { function imageSourcesCreator() {
setupInputs(); setupInputs();
document.addEventListener('fetchcomplete', ({ target, detail }) => {
const sourceSauce = document.querySelector('.js-sourcesauce');
if (target.matches('#source-form')) { document.addEventListener('fetchcomplete', (({ target, detail }: SourcesEvent) => {
const sourceSauce = $<HTMLElement>('.js-sourcesauce');
if (sourceSauce && target && target.matches('#source-form')) {
detail.text().then(text => { detail.text().then(text => {
sourceSauce.outerHTML = text; sourceSauce.outerHTML = text;
setupInputs(); setupInputs();
}); });
} }
}); }) as EventListener);
} }
export { imageSourcesCreator }; export { imageSourcesCreator };

View file

@ -1,56 +0,0 @@
/**
* Tags Misc
*/
import { $$ } from './utils/dom';
import store from './utils/store';
import { initTagDropdown } from './tags';
import { setupTagsInput, reloadTagsInput } from './tagsinput';
function tagInputButtons({target}) {
const actions = {
save(tagInput) {
store.set('tag_input', tagInput.value);
},
load(tagInput) {
// If entry 'tag_input' does not exist, try to use the current list
tagInput.value = store.get('tag_input') || tagInput.value;
reloadTagsInput(tagInput);
},
clear(tagInput) {
tagInput.value = '';
reloadTagsInput(tagInput);
},
};
for (const action in actions) {
if (target.matches(`#tagsinput-${action}`)) actions[action](document.getElementById('image_tag_input'));
}
}
function setupTags() {
$$('.js-tag-block').forEach(el => {
setupTagsInput(el);
el.classList.remove('js-tag-block');
});
}
function updateTagSauce({target, detail}) {
const tagSauce = document.querySelector('.js-tagsauce');
if (target.matches('#tags-form')) {
detail.text().then(text => {
tagSauce.outerHTML = text;
setupTags();
initTagDropdown();
});
}
}
function setupTagEvents() {
setupTags();
document.addEventListener('fetchcomplete', updateTagSauce);
document.addEventListener('click', tagInputButtons);
}
export { setupTagEvents };

70
assets/js/tagsmisc.ts Normal file
View file

@ -0,0 +1,70 @@
/**
* Tags Misc
*/
import { $, $$ } from './utils/dom';
import store from './utils/store';
import { initTagDropdown } from './tags';
import { setupTagsInput, reloadTagsInput } from './tagsinput';
import { SourcesEvent } from './sources';
type TagInputActionFunction = (tagInput: HTMLTextAreaElement | null) => void
type TagInputActionList = {
save: TagInputActionFunction,
load: TagInputActionFunction,
clear: TagInputActionFunction,
}
function tagInputButtons({target}: PointerEvent) {
const actions: TagInputActionList = {
save(tagInput: HTMLTextAreaElement | null) {
tagInput && store.set('tag_input', tagInput.value);
},
load(tagInput: HTMLTextAreaElement | null) {
if (!tagInput) { return; }
// If entry 'tag_input' does not exist, try to use the current list
tagInput.value = store.get('tag_input') || tagInput.value;
reloadTagsInput(tagInput);
},
clear(tagInput: HTMLTextAreaElement | null) {
if (!tagInput) { return; }
tagInput.value = '';
reloadTagsInput(tagInput);
},
};
for (const action in actions) {
if (target && (target as HTMLElement).matches(`#tagsinput-${action}`)) {
actions[action as keyof TagInputActionList]($<HTMLTextAreaElement>('image_tag_input'));
}
}
}
function setupTags() {
$$<HTMLDivElement>('.js-tag-block').forEach(el => {
setupTagsInput(el);
el.classList.remove('js-tag-block');
});
}
function updateTagSauce({target, detail}: SourcesEvent) {
const tagSauce = $<HTMLDivElement>('.js-tagsauce');
if (tagSauce && target.matches('#tags-form')) {
detail.text().then(text => {
tagSauce.outerHTML = text;
setupTags();
initTagDropdown();
});
}
}
function setupTagEvents() {
setupTags();
document.addEventListener('fetchcomplete', updateTagSauce as EventListener);
document.addEventListener('click', tagInputButtons as EventListener);
}
export { setupTagEvents };

View file

@ -28,13 +28,13 @@ function sortTags(hidden: boolean, a: TagData, b: TagData): number {
return a.spoiler_image_uri ? -1 : 1; return a.spoiler_image_uri ? -1 : 1;
} }
export function getHiddenTags() { 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));
} }
export function getSpoileredTags() { 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)

View file

@ -4,9 +4,6 @@
import { whenReady } from './utils/dom'; import { whenReady } from './utils/dom';
import { showOwnedComments } from './communications/comment';
import { showOwnedPosts } from './communications/post';
import { listenAutocomplete } from './autocomplete'; import { listenAutocomplete } from './autocomplete';
import { loadBooruData } from './booru'; import { loadBooruData } from './booru';
import { registerEvents } from './boorujs'; import { registerEvents } from './boorujs';
@ -40,8 +37,6 @@ import { setupSliders } from './slider';
whenReady(() => { whenReady(() => {
showOwnedComments();
showOwnedPosts();
loadBooruData(); loadBooruData();
listenAutocomplete(); listenAutocomplete();
registerEvents(); registerEvents();

View file

@ -21,7 +21,8 @@ window.booru = {
spoileredFilter: matchNone(), spoileredFilter: matchNone(),
interactions: [], interactions: [],
tagsVersion: 5, tagsVersion: 5,
hideStaffTools: 'false' hideStaffTools: 'false',
galleryImages: []
}; };
// https://github.com/jsdom/jsdom/issues/1721#issuecomment-1484202038 // https://github.com/jsdom/jsdom/issues/1721#issuecomment-1484202038

View file

@ -69,6 +69,10 @@ interface BooruObject {
* Indicates whether sensitive staff-only info should be hidden or not. * Indicates whether sensitive staff-only info should be hidden or not.
*/ */
hideStaffTools: string; hideStaffTools: string;
/**
* List of image IDs in the current gallery.
*/
galleryImages: number[]
} }
declare global { declare global {

View file

@ -45,7 +45,7 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
cssCodeSplit: true, cssCodeSplit: true,
rollupOptions: { rollupOptions: {
input: { input: {
'js/app': './js/app.js', 'js/app': './js/app.ts',
'css/application': './css/application.css', 'css/application': './css/application.css',
...Object.fromEntries(targets) ...Object.fromEntries(targets)
}, },

View file

@ -24,7 +24,7 @@ html lang="en"
= vite_hmr? do = vite_hmr? do
script type="module" src="http://localhost:5173/@vite/client" script type="module" src="http://localhost:5173/@vite/client"
script type="module" src="http://localhost:5173/js/app.js" script type="module" src="http://localhost:5173/js/app.ts"
- else - else
script type="text/javascript" src=Routes.static_path(@conn, "/js/app.js") async="async" script type="text/javascript" src=Routes.static_path(@conn, "/js/app.js") async="async"
= render PhilomenaWeb.LayoutView, "_opengraph.html", assigns = render PhilomenaWeb.LayoutView, "_opengraph.html", assigns