diff --git a/assets/js/app.js b/assets/js/app.ts
similarity index 91%
rename from assets/js/app.js
rename to assets/js/app.ts
index 4f23d655..863f4ff2 100644
--- a/assets/js/app.js
+++ b/assets/js/app.ts
@@ -14,6 +14,7 @@ import './when-ready';
// Would typically be either the theme file, or any additional file
// you later intend to put in the tag.
-// import '../css/themes/default.scss';
+import '../css/application.css';
+import '../css/themes/dark-blue.css';
// import '../css/themes/dark.scss';
// import '../css/themes/red.scss';
diff --git a/assets/js/captcha.js b/assets/js/captcha.ts
similarity index 57%
rename from assets/js/captcha.js
rename to assets/js/captcha.ts
index ec0b4f32..eef2f0b2 100644
--- a/assets/js/captcha.js
+++ b/assets/js/captcha.ts
@@ -1,18 +1,20 @@
import { delegate, leftClick } from './utils/events';
import { clearEl, makeEl } from './utils/dom';
-function insertCaptcha(_event, target) {
- const { parentNode, dataset: { sitekey } } = target;
+function insertCaptcha(_event: Event, target: HTMLInputElement) {
+ const { parentElement, dataset: { sitekey } } = target;
+
+ if (!parentElement) { return; }
const script = makeEl('script', {src: 'https://hcaptcha.com/1/api.js', async: true, defer: true});
const frame = makeEl('div', {className: 'h-captcha'});
frame.dataset.sitekey = sitekey;
- clearEl(parentNode);
+ clearEl(parentElement);
- parentNode.insertAdjacentElement('beforeend', frame);
- parentNode.insertAdjacentElement('beforeend', script);
+ parentElement.insertAdjacentElement('beforeend', frame);
+ parentElement.insertAdjacentElement('beforeend', script);
}
export function bindCaptchaLinks() {
diff --git a/assets/js/comment.js b/assets/js/comment.js
index de245fa8..feb05b59 100644
--- a/assets/js/comment.js
+++ b/assets/js/comment.js
@@ -3,24 +3,21 @@
*/
import { $ } from './utils/dom';
-import { showOwnedComments } from './communications/comment';
import { filterNode } from './imagesclientside';
import { fetchHtml } from './utils/requests';
import { timeAgo } from './timeago';
function handleError(response) {
-
const errorMessage = '
Comment failed to load!
';
if (!response.ok) {
return errorMessage;
}
- return response.text();
+ return response.text();
}
function commentPosted(response) {
-
const commentEditTab = $('#js-comment-form a[data-click-tab="write"]'),
commentEditForm = $('#js-comment-form'),
container = document.getElementById('comments'),
@@ -43,11 +40,9 @@ function commentPosted(response) {
window.location.reload();
window.scrollTo(0, 0); // Error message is displayed at the top of the page (flash)
}
-
}
function loadParentPost(event) {
-
const clickedLink = event.target,
// Find the comment containing the link that was clicked
fullComment = clickedLink.closest('article.block'),
@@ -62,7 +57,6 @@ function loadParentPost(event) {
}
if (commentMatches) {
-
// If the regex matched, get the image and comment ID
const [ , imageId, commentId ] = commentMatches;
@@ -74,13 +68,10 @@ function loadParentPost(event) {
});
return true;
-
}
-
}
function insertParentPost(data, clickedLink, fullComment) {
-
// Add the 'subthread' class to the comment with the clicked link
fullComment.classList.add('subthread');
@@ -99,11 +90,9 @@ function insertParentPost(data, clickedLink, fullComment) {
// Filter images (if any) in the loaded comment
filterNode(fullComment.previousSibling);
-
}
function clearParentPost(clickedLink, fullComment) {
-
// Remove any previous siblings with the class fetched-comment
while (fullComment.previousSibling && fullComment.previousSibling.classList.contains('fetched-comment')) {
fullComment.previousSibling.parentNode.removeChild(fullComment.previousSibling);
@@ -118,11 +107,9 @@ function clearParentPost(clickedLink, fullComment) {
if (!fullComment.classList.contains('fetched-comment')) {
fullComment.classList.remove('subthread');
}
-
}
function displayComments(container, commentsHtml) {
-
container.innerHTML = commentsHtml;
// Execute timeago on comments
@@ -130,14 +117,9 @@ function displayComments(container, commentsHtml) {
// Filter images in the comments
filterNode(container);
-
- // Show options on own comments
- showOwnedComments();
-
}
function loadComments(event) {
-
const container = document.getElementById('comments'),
hasHref = event.target && event.target.getAttribute('href'),
hasHash = window.location.hash && window.location.hash.match(/#comment_([a-f0-9]+)/),
@@ -147,7 +129,6 @@ function loadComments(event) {
fetchHtml(getURL)
.then(handleError)
.then(data => {
-
displayComments(container, data);
// Make sure the :target CSS selector applies to the inserted content
@@ -159,7 +140,6 @@ function loadComments(event) {
});
return true;
-
}
function setupComments() {
@@ -175,7 +155,6 @@ function setupComments() {
}
else {
filterNode(comments);
- showOwnedComments();
}
}
diff --git a/assets/js/communications/comment.js b/assets/js/communications/comment.js
deleted file mode 100644
index a4661c61..00000000
--- a/assets/js/communications/comment.js
+++ /dev/null
@@ -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 };
diff --git a/assets/js/communications/post.js b/assets/js/communications/post.js
deleted file mode 100644
index d2172220..00000000
--- a/assets/js/communications/post.js
+++ /dev/null
@@ -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 };
diff --git a/assets/js/duplicate_reports.js b/assets/js/duplicate_reports.js
deleted file mode 100644
index a6794ec4..00000000
--- a/assets/js/duplicate_reports.js
+++ /dev/null
@@ -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 };
diff --git a/assets/js/duplicate_reports.ts b/assets/js/duplicate_reports.ts
new file mode 100644
index 00000000..12eb8c2c
--- /dev/null
+++ b/assets/js/duplicate_reports.ts
@@ -0,0 +1,45 @@
+/**
+ * Interactive behavior for duplicate reports.
+ */
+
+import { $, $$ } from './utils/dom';
+
+function setupDupeReports() {
+ const onion = $('.onion-skin__image');
+ const slider = $('.onion-skin__slider');
+ const swipe = $('.swipe__image');
+
+ if (swipe) setupSwipe(swipe);
+ if (onion && slider) setupOnionSkin(onion, slider);
+}
+
+function setupSwipe(swipe: SVGSVGElement) {
+ const [ clip, divider ] = $$('#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 = $('#target', onion);
+
+ function setOpacity() {
+ if (target) {
+ target.setAttribute('opacity', slider.value);
+ }
+ }
+
+ setOpacity();
+ slider.addEventListener('input', setOpacity);
+}
+
+export { setupDupeReports };
diff --git a/assets/js/fp.ts b/assets/js/fp.ts
index 84a1557b..49182bc9 100644
--- a/assets/js/fp.ts
+++ b/assets/js/fp.ts
@@ -19,9 +19,9 @@ interface RealUserAgentData {
}
interface RealNavigator extends Navigator {
- deviceMemory: number,
- keyboard: RealKeyboard,
- userAgentData: RealUserAgentData,
+ deviceMemory: number | null,
+ keyboard: RealKeyboard | null,
+ userAgentData: RealUserAgentData | null,
}
/**
@@ -81,7 +81,7 @@ async function createFp(): Promise {
}
let width: string | null = store.get('cached_rem_size');
- const body = $('body');
+ const body = $('body');
if (!width && body) {
const testElement = document.createElement('span');
@@ -127,7 +127,7 @@ async function createFp(): Promise {
/**
* 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 attempts to generate a new value for the `_ses` cookie
* based on various browser attributes.
diff --git a/assets/js/galleries.js b/assets/js/galleries.ts
similarity index 51%
rename from assets/js/galleries.js
rename to assets/js/galleries.ts
index e881fa65..7866de74 100644
--- a/assets/js/galleries.js
+++ b/assets/js/galleries.ts
@@ -8,11 +8,13 @@ import { initDraggables } from './utils/draggable';
import { fetchJson } from './utils/requests';
export function setupGalleryEditing() {
- if (!$('.rearrange-button')) return;
+ if (!$('.rearrange-button')) return;
- const [ rearrangeEl, saveEl ] = $$('.rearrange-button');
- const sortableEl = $('#sortable');
- const containerEl = $('.media-list');
+ const [ rearrangeEl, saveEl ] = $$('.rearrange-button');
+ const sortableEl = $('#sortable');
+ const containerEl = $('.media-list');
+
+ if (!sortableEl || !containerEl || !saveEl || !rearrangeEl) { return; }
// Copy array
let oldImages = window.booru.galleryImages.slice();
@@ -20,7 +22,7 @@ export function setupGalleryEditing() {
initDraggables();
- $$('.media-box', containerEl).forEach(i => i.draggable = true);
+ $$('.media-box', containerEl).forEach(i => i.draggable = true);
rearrangeEl.addEventListener('click', () => {
sortableEl.classList.add('editing');
@@ -31,15 +33,17 @@ export function setupGalleryEditing() {
sortableEl.classList.remove('editing');
containerEl.classList.remove('drag-container');
- newImages = $$('.image-container', containerEl).map(i => parseInt(i.dataset.imageId, 10));
+ newImages = $$('.image-container', containerEl).map(i => parseInt(i.dataset.imageId || '-1', 10));
// If nothing changed, don't bother.
if (arraysEqual(newImages, oldImages)) return;
- fetchJson('PATCH', saveEl.dataset.reorderPath, {
- image_ids: newImages,
+ if (saveEl.dataset.reorderPath) {
+ fetchJson('PATCH', saveEl.dataset.reorderPath, {
+ image_ids: newImages,
- // copy the array again so that we have the newly updated set
- }).then(() => oldImages = newImages.slice());
+ // copy the array again so that we have the newly updated set
+ }).then(() => oldImages = newImages.slice());
+ }
});
}
diff --git a/assets/js/imagesclientside.js b/assets/js/imagesclientside.js
deleted file mode 100644
index 7dc3fb31..00000000
--- a/assets/js/imagesclientside.js
+++ /dev/null
@@ -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] (Complex Filter)'); }
-function spoilerThumbComplex(img) { spoilerThumb(img, window.booru.hiddenTag, '(Complex Filter)'); }
-
-function filterBlockSimple(img, tagsHit) { spoilerBlock(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, `This image is tagged ${escapeHtml(tagsHit[0].name)}
, which is hidden by `); }
-function spoilerBlockSimple(img, tagsHit) { spoilerBlock(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, `This image is tagged ${escapeHtml(tagsHit[0].name)}
, 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 };
diff --git a/assets/js/imagesclientside.ts b/assets/js/imagesclientside.ts
new file mode 100644
index 00000000..47fef0e9
--- /dev/null
+++ b/assets/js/imagesclientside.ts
@@ -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] (Complex Filter)');
+}
+
+function spoilerThumbComplex(img: HTMLDivElement) {
+ spoilerThumb(img, window.booru.hiddenTag, '(Complex Filter)');
+}
+
+function filterBlockSimple(img: HTMLDivElement, tagsHit: TagData[]) {
+ spoilerBlock(
+ img,
+ tagsHit[0].spoiler_image_uri || window.booru.hiddenTag,
+ `This image is tagged ${escapeHtml(tagsHit[0].name)}
, which is hidden by `
+ );
+}
+
+function spoilerBlockSimple(img: HTMLDivElement, tagsHit: TagData[]) {
+ spoilerBlock(
+ img,
+ tagsHit[0].spoiler_image_uri || window.booru.hiddenTag,
+ `This image is tagged ${escapeHtml(tagsHit[0].name)}
, 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) {
+ 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 };
diff --git a/assets/js/pmwarning.js b/assets/js/pmwarning.ts
similarity index 80%
rename from assets/js/pmwarning.js
rename to assets/js/pmwarning.ts
index 104bd09c..524f0f12 100644
--- a/assets/js/pmwarning.js
+++ b/assets/js/pmwarning.ts
@@ -7,8 +7,8 @@
import { $ } from './utils/dom';
function warnAboutPMs() {
- const textarea = $('.js-toolbar-input');
- const warning = $('.js-hidden-warning');
+ const textarea = $('.js-toolbar-input');
+ const warning = $('.js-hidden-warning');
const imageEmbedRegex = /!+\[/g;
if (!warning || !textarea) return;
diff --git a/assets/js/poll.js b/assets/js/poll.ts
similarity index 81%
rename from assets/js/poll.js
rename to assets/js/poll.ts
index 68debf5d..38c74907 100644
--- a/assets/js/poll.js
+++ b/assets/js/poll.ts
@@ -1,6 +1,6 @@
import { inputDuplicatorCreator } from './input-duplicator';
-function pollOptionCreator() {
+export function pollOptionCreator() {
inputDuplicatorCreator({
addButtonSelector: '.js-poll-add-option',
fieldSelector: '.js-poll-option',
@@ -8,5 +8,3 @@ function pollOptionCreator() {
removeButtonSelector: '.js-option-remove',
});
}
-
-export { pollOptionCreator };
diff --git a/assets/js/search.js b/assets/js/search.js
deleted file mode 100644
index 864ea231..00000000
--- a/assets/js/search.js
+++ /dev/null
@@ -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 };
diff --git a/assets/js/search.ts b/assets/js/search.ts
new file mode 100644
index 00000000..068ff068
--- /dev/null
+++ b/assets/js/search.ts
@@ -0,0 +1,50 @@
+import { $, $$ } from './utils/dom';
+import { addTag } from './tagsinput';
+
+function showHelp(subject: string, type: string | null) {
+ $$('[data-search-help]').forEach(helpBox => {
+ if (helpBox.getAttribute('data-search-help') === type) {
+ const searchSubject = $('.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 = $('.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 = $('.js-search-form');
+
+ form && form.addEventListener('click', executeFormHelper as EventListener);
+}
diff --git a/assets/js/settings.js b/assets/js/settings.ts
similarity index 50%
rename from assets/js/settings.js
rename to assets/js/settings.ts
index 360228ee..d6b71d44 100644
--- a/assets/js/settings.js
+++ b/assets/js/settings.ts
@@ -6,12 +6,11 @@ import { $, $$ } from './utils/dom';
import store from './utils/store';
export function setupSettings() {
+ if (!$('#js-setting-table')) return;
- if (!$('#js-setting-table')) return;
-
- const localCheckboxes = $$('[data-tab="local"] input[type="checkbox"]');
- const themeSelect = $('#user_theme');
- const styleSheet = $('head link[rel="stylesheet"]');
+ const localCheckboxes = $$('[data-tab="local"] input[type="checkbox"]');
+ const themeSelect = $('#user_theme');
+ const styleSheet = $('head link[rel="stylesheet"]');
// Local settings
localCheckboxes.forEach(checkbox => {
@@ -22,7 +21,8 @@ export function setupSettings() {
// Theme preview
themeSelect && themeSelect.addEventListener('change', () => {
- styleSheet.href = themeSelect.options[themeSelect.selectedIndex].dataset.themePath;
+ if (styleSheet) {
+ styleSheet.href = themeSelect.options[themeSelect.selectedIndex].dataset.themePath || '#';
+ }
});
-
}
diff --git a/assets/js/shortcuts.js b/assets/js/shortcuts.js
deleted file mode 100644
index 67d1acbd..00000000
--- a/assets/js/shortcuts.js
+++ /dev/null
@@ -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 };
diff --git a/assets/js/shortcuts.ts b/assets/js/shortcuts.ts
new file mode 100644
index 00000000..c76a0458
--- /dev/null
+++ b/assets/js/shortcuts.ts
@@ -0,0 +1,76 @@
+/**
+ * Keyboard shortcuts
+ */
+
+import { $ } from './utils/dom';
+
+interface ShortcutKeycodes {
+ [key: string]: () => void
+}
+
+function getHover(): string | null {
+ const thumbBoxHover = $('.media-box:hover');
+
+ return thumbBoxHover && (thumbBoxHover.dataset.imageId || null);
+}
+
+function openFullView() {
+ const imageHover = $('[data-uris]:hover');
+
+ if (!imageHover || !imageHover.dataset.uris) return;
+
+ window.location = JSON.parse(imageHover.dataset.uris).full;
+}
+
+function openFullViewNewTab() {
+ const imageHover = $('[data-uris]:hover');
+
+ if (!imageHover || !imageHover.dataset.uris) return;
+
+ window.open(JSON.parse(imageHover.dataset.uris).full);
+}
+
+function click(selector: string) {
+ const el = $(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 };
diff --git a/assets/js/sources.js b/assets/js/sources.ts
similarity index 58%
rename from assets/js/sources.js
rename to assets/js/sources.ts
index bb4ae3ad..b2c29c45 100644
--- a/assets/js/sources.js
+++ b/assets/js/sources.ts
@@ -1,5 +1,10 @@
+import { $ } from './utils/dom';
import { inputDuplicatorCreator } from './input-duplicator';
+export interface SourcesEvent extends CustomEvent {
+ target: HTMLElement,
+}
+
function setupInputs() {
inputDuplicatorCreator({
addButtonSelector: '.js-image-add-source',
@@ -11,16 +16,17 @@ function setupInputs() {
function imageSourcesCreator() {
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 = $('.js-sourcesauce');
+
+ if (sourceSauce && target && target.matches('#source-form')) {
detail.text().then(text => {
sourceSauce.outerHTML = text;
setupInputs();
});
}
- });
+ }) as EventListener);
}
export { imageSourcesCreator };
diff --git a/assets/js/tagsmisc.js b/assets/js/tagsmisc.js
deleted file mode 100644
index 29dc9dbe..00000000
--- a/assets/js/tagsmisc.js
+++ /dev/null
@@ -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 };
diff --git a/assets/js/tagsmisc.ts b/assets/js/tagsmisc.ts
new file mode 100644
index 00000000..ea5f4976
--- /dev/null
+++ b/assets/js/tagsmisc.ts
@@ -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]($('image_tag_input'));
+ }
+ }
+}
+
+function setupTags() {
+ $$('.js-tag-block').forEach(el => {
+ setupTagsInput(el);
+ el.classList.remove('js-tag-block');
+ });
+}
+
+function updateTagSauce({target, detail}: SourcesEvent) {
+ const tagSauce = $('.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 };
diff --git a/assets/js/utils/tag.ts b/assets/js/utils/tag.ts
index ea104213..26c6bdf9 100644
--- a/assets/js/utils/tag.ts
+++ b/assets/js/utils/tag.ts
@@ -28,13 +28,13 @@ function sortTags(hidden: boolean, a: TagData, b: TagData): number {
return a.spoiler_image_uri ? -1 : 1;
}
-export function getHiddenTags() {
+export function getHiddenTags(): TagData[] {
return unique(window.booru.hiddenTagList)
.map(tagId => getTag(tagId))
.sort(sortTags.bind(null, true));
}
-export function getSpoileredTags() {
+export function getSpoileredTags(): TagData[] {
if (window.booru.spoilerType === 'off') return [];
return unique(window.booru.spoileredTagList)
diff --git a/assets/js/when-ready.js b/assets/js/when-ready.ts
similarity index 93%
rename from assets/js/when-ready.js
rename to assets/js/when-ready.ts
index f39cec69..03cfee2a 100644
--- a/assets/js/when-ready.js
+++ b/assets/js/when-ready.ts
@@ -4,9 +4,6 @@
import { whenReady } from './utils/dom';
-import { showOwnedComments } from './communications/comment';
-import { showOwnedPosts } from './communications/post';
-
import { listenAutocomplete } from './autocomplete';
import { loadBooruData } from './booru';
import { registerEvents } from './boorujs';
@@ -40,8 +37,6 @@ import { setupSliders } from './slider';
whenReady(() => {
- showOwnedComments();
- showOwnedPosts();
loadBooruData();
listenAutocomplete();
registerEvents();
diff --git a/assets/test/vitest-setup.ts b/assets/test/vitest-setup.ts
index e4cd1314..446beac6 100644
--- a/assets/test/vitest-setup.ts
+++ b/assets/test/vitest-setup.ts
@@ -21,7 +21,8 @@ window.booru = {
spoileredFilter: matchNone(),
interactions: [],
tagsVersion: 5,
- hideStaffTools: 'false'
+ hideStaffTools: 'false',
+ galleryImages: []
};
// https://github.com/jsdom/jsdom/issues/1721#issuecomment-1484202038
diff --git a/assets/types/booru-object.d.ts b/assets/types/booru-object.d.ts
index 2a5949c0..e23e7bce 100644
--- a/assets/types/booru-object.d.ts
+++ b/assets/types/booru-object.d.ts
@@ -69,6 +69,10 @@ interface BooruObject {
* Indicates whether sensitive staff-only info should be hidden or not.
*/
hideStaffTools: string;
+ /**
+ * List of image IDs in the current gallery.
+ */
+ galleryImages: number[]
}
declare global {
diff --git a/assets/vite.config.ts b/assets/vite.config.ts
index e31cc1ef..c677a7ff 100644
--- a/assets/vite.config.ts
+++ b/assets/vite.config.ts
@@ -45,7 +45,7 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
cssCodeSplit: true,
rollupOptions: {
input: {
- 'js/app': './js/app.js',
+ 'js/app': './js/app.ts',
'css/application': './css/application.css',
...Object.fromEntries(targets)
},
diff --git a/lib/philomena_web/templates/layout/app.html.slime b/lib/philomena_web/templates/layout/app.html.slime
index e529f4f5..a4a0e66c 100644
--- a/lib/philomena_web/templates/layout/app.html.slime
+++ b/lib/philomena_web/templates/layout/app.html.slime
@@ -24,7 +24,7 @@ html lang="en"
= vite_hmr? do
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
script type="text/javascript" src=Routes.static_path(@conn, "/js/app.js") async="async"
= render PhilomenaWeb.LayoutView, "_opengraph.html", assigns