diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-10-19 11:36:00 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-19 11:36:00 +0900 |
| commit | d98bf012b59b343559dd679f0b4ae370ebd75079 (patch) | |
| tree | 00aa135936d6836e8d051ab9f5648464ba057e41 /packages/frontend | |
| parent | Revert typeorm patches (#16664) (diff) | |
| download | misskey-d98bf012b59b343559dd679f0b4ae370ebd75079.tar.gz misskey-d98bf012b59b343559dd679f0b4ae370ebd75079.tar.bz2 misskey-d98bf012b59b343559dd679f0b4ae370ebd75079.zip | |
refactor(frontend): カスタムディレクティブの型付け (#16659)
* refactor(frontend): カスタムディレクティブの型付け
* fix
Diffstat (limited to 'packages/frontend')
| -rw-r--r-- | packages/frontend/src/directives/adaptive-bg.ts | 6 | ||||
| -rw-r--r-- | packages/frontend/src/directives/adaptive-border.ts | 8 | ||||
| -rw-r--r-- | packages/frontend/src/directives/anim.ts | 8 | ||||
| -rw-r--r-- | packages/frontend/src/directives/appear.ts | 14 | ||||
| -rw-r--r-- | packages/frontend/src/directives/click-anime.ts | 6 | ||||
| -rw-r--r-- | packages/frontend/src/directives/follow-append.ts | 12 | ||||
| -rw-r--r-- | packages/frontend/src/directives/get-size.ts | 10 | ||||
| -rw-r--r-- | packages/frontend/src/directives/hotkey.ts | 11 | ||||
| -rw-r--r-- | packages/frontend/src/directives/index.ts | 66 | ||||
| -rw-r--r-- | packages/frontend/src/directives/panel.ts | 6 | ||||
| -rw-r--r-- | packages/frontend/src/directives/ripple.ts | 7 | ||||
| -rw-r--r-- | packages/frontend/src/directives/tooltip.ts | 48 | ||||
| -rw-r--r-- | packages/frontend/src/directives/user-preview.ts | 50 |
13 files changed, 158 insertions, 94 deletions
diff --git a/packages/frontend/src/directives/adaptive-bg.ts b/packages/frontend/src/directives/adaptive-bg.ts index a68cd1b18b..25e9ae1c9e 100644 --- a/packages/frontend/src/directives/adaptive-bg.ts +++ b/packages/frontend/src/directives/adaptive-bg.ts @@ -6,8 +6,8 @@ import type { Directive } from 'vue'; import { getBgColor } from '@/utility/get-bg-color.js'; -export default { - mounted(src, binding, vn) { +export const adaptiveBgDirective = { + mounted(src) { const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const myBg = window.getComputedStyle(src).backgroundColor; @@ -18,4 +18,4 @@ export default { src.style.backgroundColor = myBg; } }, -} as Directive; +} as Directive<HTMLElement>; diff --git a/packages/frontend/src/directives/adaptive-border.ts b/packages/frontend/src/directives/adaptive-border.ts index 8072a1ffd9..749861fd94 100644 --- a/packages/frontend/src/directives/adaptive-border.ts +++ b/packages/frontend/src/directives/adaptive-border.ts @@ -9,8 +9,8 @@ import { globalEvents } from '@/events.js'; const handlerMap = new WeakMap<any, any>(); -export default { - mounted(src, binding, vn) { +export const adaptiveBorderDirective = { + mounted(src) { function calc() { const parentBg = getBgColor(src.parentElement) ?? 'transparent'; @@ -30,7 +30,7 @@ export default { globalEvents.on('themeChanged', calc); }, - unmounted(src, binding, vn) { + unmounted(src) { globalEvents.off('themeChanged', handlerMap.get(src)); }, -} as Directive; +} as Directive<HTMLElement>; diff --git a/packages/frontend/src/directives/anim.ts b/packages/frontend/src/directives/anim.ts index ad0cb5ed81..a165fa11e0 100644 --- a/packages/frontend/src/directives/anim.ts +++ b/packages/frontend/src/directives/anim.ts @@ -5,8 +5,8 @@ import type { Directive } from 'vue'; -export default { - beforeMount(src, binding, vn) { +export const animDirective = { + beforeMount(src) { src.style.opacity = '0'; src.style.transform = 'scale(0.9)'; // ページネーションと相性が悪いので @@ -14,10 +14,10 @@ export default { src.classList.add('_zoom'); }, - mounted(src, binding, vn) { + mounted(src) { window.setTimeout(() => { src.style.opacity = '1'; src.style.transform = 'none'; }, 1); }, -} as Directive; +} as Directive<HTMLElement>; diff --git a/packages/frontend/src/directives/appear.ts b/packages/frontend/src/directives/appear.ts index f5fec108dc..f714871420 100644 --- a/packages/frontend/src/directives/appear.ts +++ b/packages/frontend/src/directives/appear.ts @@ -6,12 +6,16 @@ import { throttle } from 'throttle-debounce'; import type { Directive } from 'vue'; -export default { - mounted(src, binding, vn) { +interface HTMLElementWithObserver extends HTMLElement { + _observer_?: IntersectionObserver; +} + +export const appearDirective = { + mounted(src, binding) { const fn = binding.value; if (fn == null) return; - const check = throttle(1000, (entries) => { + const check = throttle<IntersectionObserverCallback>(1000, (entries) => { if (entries.some(entry => entry.isIntersecting)) { fn(); } @@ -24,7 +28,7 @@ export default { src._observer_ = observer; }, - unmounted(src, binding, vn) { + unmounted(src) { if (src._observer_) src._observer_.disconnect(); }, -} as Directive; +} as Directive<HTMLElementWithObserver, () => void>; diff --git a/packages/frontend/src/directives/click-anime.ts b/packages/frontend/src/directives/click-anime.ts index c34f351fb3..7891e8092c 100644 --- a/packages/frontend/src/directives/click-anime.ts +++ b/packages/frontend/src/directives/click-anime.ts @@ -6,8 +6,8 @@ import type { Directive } from 'vue'; import { prefer } from '@/preferences.js'; -export default { - mounted(el: HTMLElement, binding, vn) { +export const clickAnimeDirective = { + mounted(el) { if (!prefer.s.animation) return; const target = el.children[0]; @@ -37,4 +37,4 @@ export default { target.classList.add('_anime_bounce_standBy'); }); }, -} as Directive; +} as Directive<HTMLElement>; diff --git a/packages/frontend/src/directives/follow-append.ts b/packages/frontend/src/directives/follow-append.ts index f3eaac10e3..303dcb842a 100644 --- a/packages/frontend/src/directives/follow-append.ts +++ b/packages/frontend/src/directives/follow-append.ts @@ -6,8 +6,12 @@ import type { Directive } from 'vue'; import { getScrollContainer, getScrollPosition } from '@@/js/scroll.js'; -export default { - mounted(src, binding, vn) { +interface HTMLElementWithRO extends HTMLElement { + _ro_?: ResizeObserver; +} + +export const followAppendDirective = { + mounted(src, binding) { if (binding.value === false) return; let isBottom = true; @@ -34,7 +38,7 @@ export default { src._ro_ = ro; }, - unmounted(src, binding, vn) { + unmounted(src) { if (src._ro_) src._ro_.unobserve(src); }, -} as Directive; +} as Directive<HTMLElementWithRO, boolean>; diff --git a/packages/frontend/src/directives/get-size.ts b/packages/frontend/src/directives/get-size.ts index 488f201a0d..42660987dd 100644 --- a/packages/frontend/src/directives/get-size.ts +++ b/packages/frontend/src/directives/get-size.ts @@ -37,8 +37,10 @@ function calc(src: Element) { info.fn(width, height); } -export default { - mounted(src, binding, vn) { +type SizeCallback = (w: number, h: number) => void; + +export const getSizeDirective = { + mounted(src, binding) { const resize = new ResizeObserver((entries, observer) => { calc(src); }); @@ -48,7 +50,7 @@ export default { calc(src); }, - unmounted(src, binding, vn) { + unmounted(src, binding) { binding.value(0, 0); const info = mountings.get(src); if (!info) return; @@ -56,4 +58,4 @@ export default { if (info.intersection) info.intersection.disconnect(); mountings.delete(src); }, -} as Directive<Element, (w: number, h: number) => void>; +} as Directive<Element, SizeCallback>; diff --git a/packages/frontend/src/directives/hotkey.ts b/packages/frontend/src/directives/hotkey.ts index 63637ab2ba..d8fdfe647a 100644 --- a/packages/frontend/src/directives/hotkey.ts +++ b/packages/frontend/src/directives/hotkey.ts @@ -5,8 +5,14 @@ import type { Directive } from 'vue'; import { makeHotkey } from '@/utility/hotkey.js'; +import type { Keymap } from '@/utility/hotkey.js'; -export default { +interface HTMLElementWithHotkey extends HTMLElement { + _hotkey_global?: boolean; + _keyHandler?: (ev: KeyboardEvent) => void; +} + +export const hotkeyDirective = { mounted(el, binding) { el._hotkey_global = binding.modifiers.global === true; @@ -20,10 +26,11 @@ export default { }, unmounted(el) { + if (el._keyHandler == null) return; if (el._hotkey_global) { window.document.removeEventListener('keydown', el._keyHandler); } else { el.removeEventListener('keydown', el._keyHandler); } }, -} as Directive; +} as Directive<HTMLElementWithHotkey, Keymap>; diff --git a/packages/frontend/src/directives/index.ts b/packages/frontend/src/directives/index.ts index 9555045afe..07b756b95d 100644 --- a/packages/frontend/src/directives/index.ts +++ b/packages/frontend/src/directives/index.ts @@ -3,19 +3,19 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { App } from 'vue'; +import type { App, Directive } from 'vue'; -import userPreview from './user-preview.js'; -import getSize from './get-size.js'; -import ripple from './ripple.js'; -import tooltip from './tooltip.js'; -import hotkey from './hotkey.js'; -import appear from './appear.js'; -import anim from './anim.js'; -import clickAnime from './click-anime.js'; -import panel from './panel.js'; -import adaptiveBorder from './adaptive-border.js'; -import adaptiveBg from './adaptive-bg.js'; +import { userPreviewDirective } from './user-preview.js'; +import { getSizeDirective } from './get-size.js'; +import { rippleDirective } from './ripple.js'; +import { tooltipDirective } from './tooltip.js'; +import { hotkeyDirective } from './hotkey.js'; +import { appearDirective } from './appear.js'; +import { animDirective } from './anim.js'; +import { clickAnimeDirective } from './click-anime.js'; +import { panelDirective } from './panel.js'; +import { adaptiveBorderDirective } from './adaptive-border.js'; +import { adaptiveBgDirective } from './adaptive-bg.js'; export default function(app: App) { for (const [key, value] of Object.entries(directives)) { @@ -24,16 +24,32 @@ export default function(app: App) { } export const directives = { - 'userPreview': userPreview, - 'user-preview': userPreview, - 'get-size': getSize, - 'ripple': ripple, - 'tooltip': tooltip, - 'hotkey': hotkey, - 'appear': appear, - 'anim': anim, - 'click-anime': clickAnime, - 'panel': panel, - 'adaptive-border': adaptiveBorder, - 'adaptive-bg': adaptiveBg, -}; + 'userPreview': userPreviewDirective, + 'user-preview': userPreviewDirective, + 'get-size': getSizeDirective, + 'ripple': rippleDirective, + 'tooltip': tooltipDirective, + 'hotkey': hotkeyDirective, + 'appear': appearDirective, + 'anim': animDirective, + 'click-anime': clickAnimeDirective, + 'panel': panelDirective, + 'adaptive-border': adaptiveBorderDirective, + 'adaptive-bg': adaptiveBgDirective, +} as Record<string, Directive>; + +declare module 'vue' { + export interface ComponentCustomProperties { + vUserPreview: typeof userPreviewDirective; + vGetSize: typeof getSizeDirective; + vRipple: typeof rippleDirective; + vTooltip: typeof tooltipDirective; + vHotkey: typeof hotkeyDirective; + vAppear: typeof appearDirective; + vAnim: typeof animDirective; + vClickAnime: typeof clickAnimeDirective; + vPanel: typeof panelDirective; + vAdaptiveBorder: typeof adaptiveBorderDirective; + vAdaptiveBg: typeof adaptiveBgDirective; + } +} diff --git a/packages/frontend/src/directives/panel.ts b/packages/frontend/src/directives/panel.ts index 0af19e6ca3..7913baefe4 100644 --- a/packages/frontend/src/directives/panel.ts +++ b/packages/frontend/src/directives/panel.ts @@ -6,8 +6,8 @@ import type { Directive } from 'vue'; import { getBgColor } from '@/utility/get-bg-color.js'; -export default { - mounted(src, binding, vn) { +export const panelDirective = { + mounted(src) { const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const myBg = getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'); @@ -18,4 +18,4 @@ export default { src.style.backgroundColor = 'var(--MI_THEME-panel)'; } }, -} as Directive; +} as Directive<HTMLElement>; diff --git a/packages/frontend/src/directives/ripple.ts b/packages/frontend/src/directives/ripple.ts index 614cd37011..bacf49ab72 100644 --- a/packages/frontend/src/directives/ripple.ts +++ b/packages/frontend/src/directives/ripple.ts @@ -3,12 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import type { Directive } from 'vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { prefer } from '@/preferences.js'; import { popup } from '@/os.js'; -export default { - mounted(el, binding, vn) { +export const rippleDirective = { + mounted(el, binding) { // 明示的に false であればバインドしない if (binding.value === false) return; if (!prefer.s.animation) return; @@ -24,4 +25,4 @@ export default { }); }); }, -}; +} as Directive<HTMLElement, boolean | undefined>; diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts index 62aecbc87c..9cfa8d657d 100644 --- a/packages/frontend/src/directives/tooltip.ts +++ b/packages/frontend/src/directives/tooltip.ts @@ -14,13 +14,30 @@ import { popup, alert } from '@/os.js'; const start = isTouchUsing ? 'touchstart' : 'mouseenter'; const end = isTouchUsing ? 'touchend' : 'mouseleave'; -export default { - mounted(el: HTMLElement, binding, vn) { +type TooltipDirectiveState = { + text: string; + _close: null | (() => void); + showTimer: number | null; + hideTimer: number | null; + checkTimer: number | null; + show: () => void; + close: () => void; +}; + +interface TooltipDirectiveElement extends HTMLElement { + _tooltipDirective_?: TooltipDirectiveState; +} + +type TooltipDirectiveModifiers = 'left' | 'right' | 'top' | 'bottom' | 'mfm' | 'noDelay'; +type TooltipDirectiveArg = 'dialog'; + +export const tooltipDirective = { + mounted(el, binding) { const delay = binding.modifiers.noDelay ? 0 : 100; - const self = (el as any)._tooltipDirective_ = {} as any; + const self = el._tooltipDirective_ = {} as TooltipDirectiveState; - self.text = binding.value as string; + self.text = binding.value; self._close = null; self.showTimer = null; self.hideTimer = null; @@ -28,7 +45,7 @@ export default { self.close = () => { if (self._close) { - window.clearInterval(self.checkTimer); + if (self.checkTimer) window.clearInterval(self.checkTimer); self._close(); self._close = null; } @@ -72,8 +89,8 @@ export default { }); el.addEventListener(start, (ev) => { - window.clearTimeout(self.showTimer); - window.clearTimeout(self.hideTimer); + if (self.showTimer) window.clearTimeout(self.showTimer); + if (self.hideTimer) window.clearTimeout(self.hideTimer); if (delay === 0) { self.show(); } else { @@ -82,8 +99,8 @@ export default { }, { passive: true }); el.addEventListener(end, () => { - window.clearTimeout(self.showTimer); - window.clearTimeout(self.hideTimer); + if (self.showTimer) window.clearTimeout(self.showTimer); + if (self.hideTimer) window.clearTimeout(self.hideTimer); if (delay === 0) { self.close(); } else { @@ -92,18 +109,23 @@ export default { }, { passive: true }); el.addEventListener('click', () => { - window.clearTimeout(self.showTimer); + if (self.showTimer) window.clearTimeout(self.showTimer); self.close(); }); }, updated(el, binding) { const self = el._tooltipDirective_; + if (self == null) return; self.text = binding.value as string; }, - unmounted(el, binding, vn) { + unmounted(el) { const self = el._tooltipDirective_; - window.clearInterval(self.checkTimer); + if (self == null) return; + if (self.showTimer) window.clearTimeout(self.showTimer); + if (self.hideTimer) window.clearTimeout(self.hideTimer); + if (self.checkTimer) window.clearTimeout(self.checkTimer); + self.close(); }, -} as Directive; +} as Directive<TooltipDirectiveElement, string, TooltipDirectiveModifiers, TooltipDirectiveArg>; diff --git a/packages/frontend/src/directives/user-preview.ts b/packages/frontend/src/directives/user-preview.ts index b11ef8f088..76e345a108 100644 --- a/packages/frontend/src/directives/user-preview.ts +++ b/packages/frontend/src/directives/user-preview.ts @@ -5,18 +5,19 @@ import { defineAsyncComponent, ref } from 'vue'; import type { Directive } from 'vue'; +import * as Misskey from 'misskey-js'; import { popup } from '@/os.js'; import { isTouchUsing } from '@/utility/touch.js'; export class UserPreview { - private el; - private user; - private showTimer; - private hideTimer; - private checkTimer; - private promise; + private el: HTMLElement; + private user: string | Misskey.entities.UserDetailed; + private showTimer: number | null = null; + private hideTimer: number | null = null; + private checkTimer: number | null = null; + private promise: null | { cancel: () => void } = null; - constructor(el, user) { + constructor(el: HTMLElement, user: string | Misskey.entities.UserDetailed) { this.el = el; this.user = user; @@ -43,10 +44,10 @@ export class UserPreview { source: this.el, }, { mouseover: () => { - window.clearTimeout(this.hideTimer); + if (this.hideTimer) window.clearTimeout(this.hideTimer); }, mouseleave: () => { - window.clearTimeout(this.showTimer); + if (this.showTimer) window.clearTimeout(this.showTimer); this.hideTimer = window.setTimeout(this.close, 500); }, closed: () => dispose(), @@ -60,8 +61,8 @@ export class UserPreview { this.checkTimer = window.setInterval(() => { if (!window.document.body.contains(this.el)) { - window.clearTimeout(this.showTimer); - window.clearTimeout(this.hideTimer); + if (this.showTimer) window.clearTimeout(this.showTimer); + if (this.hideTimer) window.clearTimeout(this.hideTimer); this.close(); } }, 1000); @@ -69,26 +70,26 @@ export class UserPreview { private close() { if (this.promise) { - window.clearInterval(this.checkTimer); + if (this.checkTimer) window.clearInterval(this.checkTimer); this.promise.cancel(); this.promise = null; } } private onMouseover() { - window.clearTimeout(this.showTimer); - window.clearTimeout(this.hideTimer); + if (this.showTimer) window.clearTimeout(this.showTimer); + if (this.hideTimer) window.clearTimeout(this.hideTimer); this.showTimer = window.setTimeout(this.show, 500); } private onMouseleave() { - window.clearTimeout(this.showTimer); - window.clearTimeout(this.hideTimer); + if (this.showTimer) window.clearTimeout(this.showTimer); + if (this.hideTimer) window.clearTimeout(this.hideTimer); this.hideTimer = window.setTimeout(this.close, 500); } private onClick() { - window.clearTimeout(this.showTimer); + if (this.showTimer) window.clearTimeout(this.showTimer); this.close(); } @@ -105,8 +106,14 @@ export class UserPreview { } } -export default { - mounted(el: HTMLElement, binding, vn) { +interface UserPreviewDirectiveElement extends HTMLElement { + _userPreviewDirective_?: { + preview: UserPreview; + }; +} + +export const userPreviewDirective = { + mounted(el, binding) { if (binding.value == null) return; if (isTouchUsing) return; @@ -117,10 +124,11 @@ export default { self.preview = new UserPreview(el, binding.value); }, - unmounted(el, binding, vn) { + unmounted(el, binding) { if (binding.value == null) return; const self = el._userPreviewDirective_; + if (self == null) return; self.preview.detach(); }, -} as Directive; +} as Directive<UserPreviewDirectiveElement, string | Misskey.entities.UserDetailed>; |