diff --git a/assets/js/burger.js b/assets/js/burger.ts similarity index 58% rename from assets/js/burger.js rename to assets/js/burger.ts index c7a573f4f..5d3993d96 100644 --- a/assets/js/burger.js +++ b/assets/js/burger.ts @@ -2,12 +2,14 @@ * Hamburger menu. */ -function switchClasses(element, oldClass, newClass) { +import { assertNotNull, assertType } from './utils/assert'; + +function switchClasses(element: HTMLElement, oldClass: string, newClass: string) { element.classList.remove(oldClass); element.classList.add(newClass); } -function open(burger, content, body, root) { +function open(burger: HTMLElement, content: HTMLElement, body: HTMLElement, root: HTMLElement) { switchClasses(content, 'close', 'open'); switchClasses(burger, 'close', 'open'); @@ -15,23 +17,23 @@ function open(burger, content, body, root) { body.classList.add('no-overflow'); } -function close(burger, content, body, root) { +function close(burger: HTMLElement, content: HTMLElement, body: HTMLElement, root: HTMLElement) { switchClasses(content, 'open', 'close'); switchClasses(burger, 'open', 'close'); - /* the CSS animation closing the menu finishes in 300ms */ + // The CSS animation closing the menu finishes in 300ms setTimeout(() => { root.classList.remove('no-overflow-x'); body.classList.remove('no-overflow'); }, 300); } -function copyArtistLinksTo(burger) { - const copy = links => { +function copyArtistLinksTo(burger: HTMLElement) { + const copy = (links: HTMLCollection) => { burger.appendChild(document.createElement('hr')); - [].slice.call(links).forEach(link => { - const burgerLink = link.cloneNode(true); + [...links].forEach(link => { + const burgerLink = assertType(link.cloneNode(true), HTMLElement); burgerLink.className = ''; burger.appendChild(burgerLink); @@ -40,13 +42,13 @@ function copyArtistLinksTo(burger) { const linksContainers = document.querySelectorAll('.js-burger-links'); - [].slice.call(linksContainers).forEach(container => copy(container.children)); + [...linksContainers].forEach(container => copy(container.children)); } function setupBurgerMenu() { - const burger = document.getElementById('burger'); - const toggle = document.getElementById('js-burger-toggle'); - const content = document.getElementById('container'); + const burger = assertNotNull(document.getElementById('burger')); + const toggle = assertNotNull(document.getElementById('js-burger-toggle')); + const content = assertNotNull(document.getElementById('container')); const body = document.body; const root = document.documentElement; diff --git a/assets/js/cable.js b/assets/js/cable.js deleted file mode 100644 index 03ed74e21..000000000 --- a/assets/js/cable.js +++ /dev/null @@ -1,11 +0,0 @@ -// Action Cable provides the framework to deal with WebSockets in Rails. -// You can generate new channels where WebSocket features live using the rails generate channel command. -let cable; - -function setupCable() { - if (window.booru.userIsSignedIn) { - cable = ActionCable.createConsumer(); - } -} - -export { cable, setupCable }; diff --git a/assets/js/captcha.js b/assets/js/captcha.ts similarity index 54% rename from assets/js/captcha.js rename to assets/js/captcha.ts index ec0b4f32c..2c40c19dd 100644 --- a/assets/js/captcha.js +++ b/assets/js/captcha.ts @@ -1,18 +1,21 @@ 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: HTMLElement) { + const { parentElement, dataset: { sitekey } } = target; + if (!parentElement) { + throw new Error('Captcha target not found'); + } 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/duplicate_reports.js b/assets/js/duplicate_reports.ts similarity index 56% rename from assets/js/duplicate_reports.js rename to assets/js/duplicate_reports.ts index a6794ec49..1863824cc 100644 --- a/assets/js/duplicate_reports.js +++ b/assets/js/duplicate_reports.ts @@ -2,34 +2,40 @@ * Interactive behavior for duplicate reports. */ +import { assertNotNull, assertType } from './utils/assert'; 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); + if (swipe) { + setupSwipe(assertType(swipe, SVGSVGElement)); + } + + if (onion) { + setupOnionSkin(assertType(onion, SVGSVGElement), assertType(slider, HTMLInputElement)); + } } -function setupSwipe(swipe) { +function setupSwipe(swipe: SVGSVGElement) { const [ clip, divider ] = $$('#clip rect, #divider', swipe); const { width } = swipe.viewBox.baseVal; - function moveDivider({ clientX }) { + function moveDivider({ clientX }: { clientX: number }) { // Move center to cursor const rect = swipe.getBoundingClientRect(); const newX = (clientX - rect.left) * (width / rect.width); - divider.setAttribute('x', newX); - clip.setAttribute('width', newX); + divider.setAttribute('x', `${newX}`); + clip.setAttribute('width', `${newX}`); } swipe.addEventListener('mousemove', moveDivider); } -function setupOnionSkin(onion, slider) { - const target = $('#target', onion); +function setupOnionSkin(onion: SVGSVGElement, slider: HTMLInputElement) { + const target = assertNotNull($('#target', onion)); function setOpacity() { target.setAttribute('opacity', slider.value); diff --git a/assets/js/notifications.js b/assets/js/notifications.ts similarity index 85% rename from assets/js/notifications.js rename to assets/js/notifications.ts index 2447debbf..7f8541de2 100644 --- a/assets/js/notifications.js +++ b/assets/js/notifications.ts @@ -6,9 +6,9 @@ import { fetchJson, handleError } from './utils/requests'; import { $ } from './utils/dom'; import { delegate } from './utils/events'; import store from './utils/store'; +import { assertNotNull } from './utils/assert'; -const NOTIFICATION_INTERVAL = 600000, - NOTIFICATION_EXPIRES = 300000; +const notificationInterval = 600000; function makeRequest(verb) { return fetchJson(verb, '/notifications/unread').then(handleError); @@ -17,7 +17,7 @@ function makeRequest(verb) { function bindSubscriptionLinks() { delegate(document, 'fetchcomplete', { '.js-subscription-link': event => { - const target = event.target.closest('.js-subscription-target'); + const target = assertNotNull(event.target).closest('.js-subscription-target'); event.detail.text().then(text => { target.outerHTML = text; }); @@ -34,7 +34,7 @@ function getNewNotifications() { updateNotificationTicker(notifications); storeNotificationCount(notifications); - setTimeout(getNewNotifications, NOTIFICATION_INTERVAL); + setTimeout(getNewNotifications, notificationInterval); }); } @@ -49,7 +49,7 @@ function updateNotificationTicker(notificationCount) { function storeNotificationCount(notificationCount) { // The current number of notifications are stored along with the time when the data expires - store.setWithExpireTime('notificationCount', notificationCount, NOTIFICATION_EXPIRES); + store.setWithExpireTime('notificationCount', notificationCount, notificationTtl); } @@ -57,7 +57,7 @@ function setupNotifications() { if (!window.booru.userIsSignedIn) return; // Fetch notifications from the server at a regular interval - setTimeout(getNewNotifications, NOTIFICATION_INTERVAL); + setTimeout(getNewNotifications, notificationInterval); // Update the current number of notifications based on the latest page load storeNotificationCount($('.js-notification-ticker').dataset.notificationCount); diff --git a/assets/js/utils/assert.ts b/assets/js/utils/assert.ts new file mode 100644 index 000000000..4ff6a7cba --- /dev/null +++ b/assets/js/utils/assert.ts @@ -0,0 +1,26 @@ +export function assertNotNull(value: T | null): T { + if (value === null) { + throw new Error('Expected non-null value'); + } + + return value; +} + +export function assertNotUndefined(value: T | undefined): T { + // eslint-disable-next-line no-undefined + if (value === undefined) { + throw new Error('Expected non-undefined value'); + } + + return value; +} + +type Constructor = { new (...args: any[]): T }; + +export function assertType(value: any, c: Constructor): T { + if (value instanceof c) { + return value; + } + + throw new Error(`Expected value of type '${c}'`); +} diff --git a/assets/js/utils/events.ts b/assets/js/utils/events.ts index 65c2c4960..9aa9e9849 100644 --- a/assets/js/utils/events.ts +++ b/assets/js/utils/events.ts @@ -9,7 +9,8 @@ export interface PhilomenaAvailableEventsMap { drop: DragEvent, click: MouseEvent, submit: Event, - reset: Event + reset: Event, + fetchcomplete: CustomEvent, } export interface PhilomenaEventElement {