diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts index d000a28232..28f796bf89 100644 --- a/packages/frontend/.storybook/preview.ts +++ b/packages/frontend/.storybook/preview.ts @@ -64,13 +64,13 @@ initialize({ initLocalStorage(); queueMicrotask(() => { Promise.all([ - import('../src/components'), - import('../src/directives'), - import('../src/widgets'), - import('../src/scripts/theme'), - import('../src/store'), - import('../src/os'), - ]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { defaultStore }, os]) => { + import('../src/directives/index.js'), + import('../src/components/index.js'), + import('../src/widgets/index.js'), + import('../src/scripts/theme.js'), + import('../src/store.js'), + import('../src/os.js'), + ]).then(([{ default: directives }, { default: components }, { default: widgets }, { applyTheme }, { defaultStore }, os]) => { setup((app) => { moduleInitialized = true; if (app[appInitialized]) { @@ -78,8 +78,8 @@ queueMicrotask(() => { } app[appInitialized] = true; loadTheme(applyTheme); - components(app); directives(app); + components(app); widgets(app); misskeyOS = os; if (isChromatic()) { diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index bfe5c4f5f7..04dac6e5f2 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -6,9 +6,9 @@ import { computed, watch, version as vueVersion, App } from 'vue'; import { compareVersions } from 'compare-versions'; import { version, lang, updateLocale, locale } from '@@/js/config.js'; -import widgets from '@/widgets/index.js'; import directives from '@/directives/index.js'; import components from '@/components/index.js'; +import widgets from '@/widgets/index.js'; import { applyTheme } from '@/scripts/theme.js'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js'; import { updateI18n, i18n } from '@/i18n.js'; @@ -243,9 +243,9 @@ export async function common(createVue: () => App) { app.config.performance = true; } - widgets(app); directives(app); components(app); + widgets(app); // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 // なぜか2回実行されることがあるため、mountするdivを1つに制限する diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index ba619f6063..f3cea758cd 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -54,7 +54,7 @@ import { defineAsyncComponent, ref } from 'vue'; import { v4 as uuid } from 'uuid'; import MkSelect from '@/components/MkSelect.vue'; import MkButton from '@/components/MkButton.vue'; -import { widgets as widgetDefs } from '@/widgets/index.js'; +import { widgetDefs } from '@/widgets/index.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { isLink } from '@@/js/is-link.js'; diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index b36625ed1b..f640754073 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { App } from 'vue'; +import type { App } from 'vue'; import Mfm from './global/MkMfm.js'; import MkA from './global/MkA.vue'; @@ -34,28 +34,28 @@ export default function(app: App) { } export const components = { - I18n: I18n, - RouterView: RouterView, - Mfm: Mfm, - MkA: MkA, - MkAcct: MkAcct, - MkAvatar: MkAvatar, - MkEmoji: MkEmoji, - MkCondensedLine: MkCondensedLine, - MkCustomEmoji: MkCustomEmoji, - MkUserName: MkUserName, - MkEllipsis: MkEllipsis, - MkTime: MkTime, - MkUrl: MkUrl, - MkLoading: MkLoading, - MkError: MkError, - MkAd: MkAd, - MkPageHeader: MkPageHeader, - MkSpacer: MkSpacer, - MkFooterSpacer: MkFooterSpacer, - MkStickyContainer: MkStickyContainer, - MkLazy: MkLazy, -}; + I18n, + RouterView, + Mfm, + MkA, + MkAcct, + MkAd, + MkAvatar, + MkCondensedLine, + MkCustomEmoji, + MkEllipsis, + MkEmoji, + MkError, + MkFooterSpacer, + MkLazy, + MkLoading, + MkPageHeader, + MkSpacer, + MkStickyContainer, + MkTime, + MkUrl, + MkUserName, +} as const; declare module '@vue/runtime-core' { export interface GlobalComponents { @@ -64,21 +64,21 @@ declare module '@vue/runtime-core' { Mfm: typeof Mfm; MkA: typeof MkA; MkAcct: typeof MkAcct; + MkAd: typeof MkAd; MkAvatar: typeof MkAvatar; - MkEmoji: typeof MkEmoji; MkCondensedLine: typeof MkCondensedLine; MkCustomEmoji: typeof MkCustomEmoji; - MkUserName: typeof MkUserName; MkEllipsis: typeof MkEllipsis; - MkTime: typeof MkTime; - MkUrl: typeof MkUrl; - MkLoading: typeof MkLoading; + MkEmoji: typeof MkEmoji; MkError: typeof MkError; - MkAd: typeof MkAd; + MkFooterSpacer: typeof MkFooterSpacer; + MkLazy: typeof MkLazy; + MkLoading: typeof MkLoading; MkPageHeader: typeof MkPageHeader; MkSpacer: typeof MkSpacer; - MkFooterSpacer: typeof MkFooterSpacer; MkStickyContainer: typeof MkStickyContainer; - MkLazy: typeof MkLazy; + MkTime: typeof MkTime; + MkUrl: typeof MkUrl; + MkUserName: typeof MkUserName; } } diff --git a/packages/frontend/src/directives/adaptive-bg.ts b/packages/frontend/src/directives/adaptive-bg.ts index f88996019f..f3c5921f8d 100644 --- a/packages/frontend/src/directives/adaptive-bg.ts +++ b/packages/frontend/src/directives/adaptive-bg.ts @@ -3,11 +3,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Directive } from 'vue'; -import { getBgColor } from '@/scripts/get-bg-color.js'; +import type { ObjectDirective } from 'vue'; + +type VAdaptiveBg = ObjectDirective; + +export const vAdaptiveBg = { + async mounted(src) { + const [ + { getBgColor }, + ] = await Promise.all([ + import('@/scripts/get-bg-color.js'), + ]); -export default { - mounted(src, binding, vn) { const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const myBg = window.getComputedStyle(src).backgroundColor; @@ -18,4 +25,4 @@ export default { src.style.backgroundColor = myBg; } }, -} as Directive; +} satisfies VAdaptiveBg as VAdaptiveBg; diff --git a/packages/frontend/src/directives/adaptive-border.ts b/packages/frontend/src/directives/adaptive-border.ts index 1305f312bd..28ed4edbb7 100644 --- a/packages/frontend/src/directives/adaptive-border.ts +++ b/packages/frontend/src/directives/adaptive-border.ts @@ -3,11 +3,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Directive } from 'vue'; -import { getBgColor } from '@/scripts/get-bg-color.js'; +import type { ObjectDirective } from 'vue'; + +type VAdaptiveBorder = ObjectDirective; + +export const vAdaptiveBorder = { + async mounted(src) { + const [ + { getBgColor }, + ] = await Promise.all([ + import('@/scripts/get-bg-color.js'), + ]); -export default { - mounted(src, binding, vn) { const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const myBg = window.getComputedStyle(src).backgroundColor; @@ -18,4 +25,4 @@ export default { src.style.borderColor = myBg; } }, -} as Directive; +} satisfies VAdaptiveBorder as VAdaptiveBorder; diff --git a/packages/frontend/src/directives/anim.ts b/packages/frontend/src/directives/anim.ts index d5b6ae4287..3e0b08b599 100644 --- a/packages/frontend/src/directives/anim.ts +++ b/packages/frontend/src/directives/anim.ts @@ -3,21 +3,25 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Directive } from 'vue'; +import type { ObjectDirective } from 'vue'; -export default { - beforeMount(src, binding, vn) { +type VAnim = ObjectDirective; + +export const vAnim = { + async beforeMount(src) { src.style.opacity = '0'; src.style.transform = 'scale(0.9)'; // ページネーションと相性が悪いので - //if (typeof binding.value === 'number') src.style.transitionDelay = `${binding.value * 30}ms`; + // if (typeof binding.value === 'number') { + // src.style.transitionDelay = `${binding.value * 30}ms`; + // } src.classList.add('_zoom'); }, - mounted(src, binding, vn) { + async mounted(src) { window.setTimeout(() => { src.style.opacity = '1'; src.style.transform = 'none'; }, 1); }, -} as Directive; +} satisfies VAnim as VAnim; diff --git a/packages/frontend/src/directives/appear.ts b/packages/frontend/src/directives/appear.ts index 706d4a9ee4..243d54a41e 100644 --- a/packages/frontend/src/directives/appear.ts +++ b/packages/frontend/src/directives/appear.ts @@ -3,25 +3,29 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Directive } from 'vue'; +import type { ObjectDirective } from 'vue'; -export default { - mounted(src, binding, vn) { +type VAppear = ObjectDirective unknown) | null | undefined>; + +export const vAppear = { + async mounted(src, binding) { const fn = binding.value; if (fn == null) return; - const observer = new IntersectionObserver(entries => { - if (entries.some(entry => entry.isIntersecting)) { + const observer = new IntersectionObserver((entries) => { + if (entries.some((entry) => entry.isIntersecting)) { fn(); } }); observer.observe(src); + //@ts-expect-error HTMLElementにプロパティを追加している src._observer_ = observer; }, - unmounted(src, binding, vn) { - if (src._observer_) src._observer_.disconnect(); + async unmounted(src) { + //@ts-expect-error HTMLElementにプロパティを追加している + src._observer_?.disconnect(); }, -} as Directive; +} satisfies VAppear as VAppear; diff --git a/packages/frontend/src/directives/click-anime.ts b/packages/frontend/src/directives/click-anime.ts index 5bb48bbcdd..00a0b42d0f 100644 --- a/packages/frontend/src/directives/click-anime.ts +++ b/packages/frontend/src/directives/click-anime.ts @@ -3,20 +3,27 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Directive } from 'vue'; -import { defaultStore } from '@/store.js'; +import type { ObjectDirective } from 'vue'; + +type VClickAnime = ObjectDirective; + +export const vClickAnime = { + async mounted(src) { + const [ + { defaultStore }, + ] = await Promise.all([ + import('@/store.js'), + ]); -export default { - mounted(el: HTMLElement, binding, vn) { if (!defaultStore.state.animation) return; - const target = el.children[0]; + const target = src.children[0]; if (target == null) return; target.classList.add('_anime_bounce_standBy'); - el.addEventListener('mousedown', () => { + src.addEventListener('mousedown', () => { target.classList.remove('_anime_bounce'); target.classList.add('_anime_bounce_standBy'); @@ -27,14 +34,14 @@ export default { }); }); - el.addEventListener('click', () => { + src.addEventListener('click', () => { target.classList.add('_anime_bounce'); target.classList.remove('_anime_bounce_ready'); }); - el.addEventListener('animationend', () => { + src.addEventListener('animationend', () => { target.classList.remove('_anime_bounce'); target.classList.add('_anime_bounce_standBy'); }); }, -} as Directive; +} satisfies VClickAnime as VClickAnime; diff --git a/packages/frontend/src/directives/follow-append.ts b/packages/frontend/src/directives/follow-append.ts deleted file mode 100644 index 615dd99fa8..0000000000 --- a/packages/frontend/src/directives/follow-append.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Directive } from 'vue'; -import { getScrollContainer, getScrollPosition } from '@@/js/scroll.js'; - -export default { - mounted(src, binding, vn) { - if (binding.value === false) return; - - let isBottom = true; - - const container = getScrollContainer(src)!; - container.addEventListener('scroll', () => { - const pos = getScrollPosition(container); - const viewHeight = container.clientHeight; - const height = container.scrollHeight; - isBottom = (pos + viewHeight > height - 32); - }, { passive: true }); - container.scrollTop = container.scrollHeight; - - const ro = new ResizeObserver((entries, observer) => { - if (isBottom) { - const height = container.scrollHeight; - container.scrollTop = height; - } - }); - - ro.observe(src); - - // TODO: 新たにプロパティを作るのをやめMapを使う - src._ro_ = ro; - }, - - unmounted(src, binding, vn) { - if (src._ro_) src._ro_.unobserve(src); - }, -} as Directive; diff --git a/packages/frontend/src/directives/get-size.ts b/packages/frontend/src/directives/get-size.ts index 2655c76c48..b6d298cf2c 100644 --- a/packages/frontend/src/directives/get-size.ts +++ b/packages/frontend/src/directives/get-size.ts @@ -3,15 +3,41 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Directive } from 'vue'; +import type { ObjectDirective } from 'vue'; -const mountings = new Map void; }>(); -function calc(src: Element) { +type VGetSize = ObjectDirective unknown) | null | undefined>; + +export const vGetSize = { + async mounted(src, binding) { + if (!binding.value) return; + + const resize = new ResizeObserver(() => { + calc(src); + }); + resize.observe(src); + + mountings.set(src, { resize, fn: binding.value }); + calc(src); + }, + + async unmounted(src, binding) { + if (!binding.value) return; + binding.value(0, 0); + const info = mountings.get(src); + if (!info) return; + info.resize.disconnect(); + if (info.intersection) info.intersection.disconnect(); + mountings.delete(src); + }, +} satisfies VGetSize as VGetSize; + +function calc(src: HTMLElement) { const info = mountings.get(src); const height = src.clientHeight; const width = src.clientWidth; @@ -22,8 +48,8 @@ function calc(src: Element) { if (!height) { // IntersectionObserverで表示検出する if (!info.intersection) { - info.intersection = new IntersectionObserver(entries => { - if (entries.some(entry => entry.isIntersecting)) calc(src); + info.intersection = new IntersectionObserver((entries) => { + if (entries.some((entry) => entry.isIntersecting)) calc(src); }); } info.intersection.observe(src); @@ -36,24 +62,3 @@ function calc(src: Element) { info.fn(width, height); } - -export default { - mounted(src, binding, vn) { - const resize = new ResizeObserver((entries, observer) => { - calc(src); - }); - resize.observe(src); - - mountings.set(src, { resize, fn: binding.value }); - calc(src); - }, - - unmounted(src, binding, vn) { - binding.value(0, 0); - const info = mountings.get(src); - if (!info) return; - info.resize.disconnect(); - if (info.intersection) info.intersection.disconnect(); - mountings.delete(src); - }, -} as Directive void>; diff --git a/packages/frontend/src/directives/hotkey.ts b/packages/frontend/src/directives/hotkey.ts index 0e5c7ede24..4cb16ad0f7 100644 --- a/packages/frontend/src/directives/hotkey.ts +++ b/packages/frontend/src/directives/hotkey.ts @@ -3,27 +3,43 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Directive } from 'vue'; -import { makeHotkey } from '@/scripts/hotkey.js'; +import type { ObjectDirective } from 'vue'; +import type { Keymap } from '@/scripts/hotkey.js'; -export default { - mounted(el, binding) { - el._hotkey_global = binding.modifiers.global === true; +type VHotkey = ObjectDirective; - el._keyHandler = makeHotkey(binding.value); +export const vHotkey = { + async mounted(src, binding) { + const [ + { makeHotkey }, + ] = await Promise.all([ + import('@/scripts/hotkey.js'), + ]); - if (el._hotkey_global) { - document.addEventListener('keydown', el._keyHandler, { passive: false }); + //@ts-expect-error HTMLElementにプロパティを追加している + src._hotkey_global = binding.modifiers.global === true; + + //@ts-expect-error HTMLElementにプロパティを追加している + src._keyHandler = makeHotkey(binding.value); + + //@ts-expect-error HTMLElementにプロパティを追加している + if (src._hotkey_global) { + //@ts-expect-error HTMLElementにプロパティを追加している + document.addEventListener('keydown', src._keyHandler, { passive: false }); } else { - el.addEventListener('keydown', el._keyHandler, { passive: false }); + //@ts-expect-error HTMLElementにプロパティを追加している + src.addEventListener('keydown', src._keyHandler, { passive: false }); } }, - unmounted(el) { - if (el._hotkey_global) { - document.removeEventListener('keydown', el._keyHandler); + async unmounted(src) { + //@ts-expect-error HTMLElementにプロパティを追加している + if (src._hotkey_global) { + //@ts-expect-error HTMLElementにプロパティを追加している + document.removeEventListener('keydown', src._keyHandler); } else { - el.removeEventListener('keydown', el._keyHandler); + //@ts-expect-error HTMLElementにプロパティを追加している + src.removeEventListener('keydown', src._keyHandler); } }, -} as Directive; +} satisfies VHotkey as VHotkey; diff --git a/packages/frontend/src/directives/index.ts b/packages/frontend/src/directives/index.ts index bda7738ccd..5a106cb637 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 { App } from 'vue'; +import type { App } 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 { vAdaptiveBg } from '@/directives/adaptive-bg.js'; +import { vAdaptiveBorder } from '@/directives/adaptive-border.js'; +import { vAnim } from '@/directives/anim.js'; +import { vAppear } from '@/directives/appear.js'; +import { vClickAnime } from '@/directives/click-anime.js'; +import { vGetSize } from '@/directives/get-size.js'; +import { vHotkey } from '@/directives/hotkey.js'; +import { vPanel } from '@/directives/panel.js'; +import { vRipple } from '@/directives/ripple.js'; +import { vTooltip } from '@/directives/tooltip.js'; +import { vUserPreview } from '@/directives/user-preview.js'; export default function(app: App) { for (const [key, value] of Object.entries(directives)) { @@ -24,16 +24,31 @@ 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, -}; + 'adaptive-bg': vAdaptiveBg, + 'adaptive-border': vAdaptiveBorder, + 'anim': vAnim, + 'appear': vAppear, + 'click-anime': vClickAnime, + 'get-size': vGetSize, + 'hotkey': vHotkey, + 'panel': vPanel, + 'ripple': vRipple, + 'tooltip': vTooltip, + 'user-preview': vUserPreview, +} as const; + +declare module '@vue/runtime-core' { + export interface GlobalDirectives { + vAdaptiveBg: typeof vAdaptiveBg; + vAdaptiveBorder: typeof vAdaptiveBorder; + vAnim: typeof vAnim; + vAppear: typeof vAppear; + vClickAnime: typeof vClickAnime; + vGetSize: typeof vGetSize; + vHotkey: typeof vHotkey; + vPanel: typeof vPanel; + vRipple: typeof vRipple; + vTooltip: typeof vTooltip; + vUserPreview: typeof vUserPreview; + } +} diff --git a/packages/frontend/src/directives/panel.ts b/packages/frontend/src/directives/panel.ts index aa26b94d0b..e461843863 100644 --- a/packages/frontend/src/directives/panel.ts +++ b/packages/frontend/src/directives/panel.ts @@ -3,11 +3,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Directive } from 'vue'; -import { getBgColor } from '@/scripts/get-bg-color.js'; +import type { ObjectDirective } from 'vue'; + +type VPanel = ObjectDirective; + +export const vPanel = { + async mounted(src) { + const [ + { getBgColor }, + ] = await Promise.all([ + import('@/scripts/get-bg-color.js'), + ]); -export default { - mounted(src, binding, vn) { const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'); @@ -18,4 +25,4 @@ export default { src.style.backgroundColor = 'var(--MI_THEME-panel)'; } }, -} as Directive; +} satisfies VPanel as VPanel; diff --git a/packages/frontend/src/directives/ripple.ts b/packages/frontend/src/directives/ripple.ts index a043ff212d..97c0c18705 100644 --- a/packages/frontend/src/directives/ripple.ts +++ b/packages/frontend/src/directives/ripple.ts @@ -3,23 +3,31 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import type { ObjectDirective } from 'vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; -import { popup } from '@/os.js'; -export default { - mounted(el, binding, vn) { +type VRipple = ObjectDirective; + +export const vRipple = { + async mounted(src, binding) { + const [ + { popup }, + ] = await Promise.all([ + import('@/os.js'), + ]); + // 明示的に false であればバインドしない if (binding.value === false) return; - el.addEventListener('click', () => { - const rect = el.getBoundingClientRect(); + src.addEventListener('click', () => { + const rect = src.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); + const x = rect.left + (src.offsetWidth / 2); + const y = rect.top + (src.offsetHeight / 2); const { dispose } = popup(MkRippleEffect, { x, y }, { end: () => dispose(), }); }); }, -}; +} satisfies VRipple as VRipple; diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts index 251ce5675f..6b546bb5d7 100644 --- a/packages/frontend/src/directives/tooltip.ts +++ b/packages/frontend/src/directives/tooltip.ts @@ -6,18 +6,27 @@ // TODO: useTooltip関数使うようにしたい // ただディレクティブ内でonUnmountedなどのcomposition api使えるのか不明 -import { defineAsyncComponent, Directive, ref } from 'vue'; -import { isTouchUsing } from '@/scripts/touch.js'; -import { popup, alert } from '@/os.js'; +import { type ObjectDirective, defineAsyncComponent, ref } from 'vue'; -const start = isTouchUsing ? 'touchstart' : 'mouseenter'; -const end = isTouchUsing ? 'touchend' : 'mouseleave'; +type VTooltip = ObjectDirective; + +export const vTooltip = { + async mounted(src, binding) { + const [ + { alert, popup }, + { isTouchUsing }, + ] = await Promise.all([ + import('@/os.js'), + import('@/scripts/touch.js'), + ]); + + const start = isTouchUsing ? 'touchstart' : 'mouseenter'; + const end = isTouchUsing ? 'touchend' : 'mouseleave'; -export default { - mounted(el: HTMLElement, binding, vn) { const delay = binding.modifiers.noDelay ? 0 : 100; - const self = (el as any)._tooltipDirective_ = {} as any; + //@ts-expect-error HTMLElementにプロパティを追加している + const self = src._tooltipDirective_ = {} as any; self.text = binding.value as string; self._close = null; @@ -34,19 +43,19 @@ export default { }; if (binding.arg === 'dialog') { - el.addEventListener('click', (ev) => { + src.addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation(); alert({ type: 'info', - text: binding.value, + text: binding.value ?? '', }); return false; }); } self.show = () => { - if (!document.body.contains(el)) return; + if (!document.body.contains(src)) return; if (self._close) return; if (self.text == null) return; @@ -56,7 +65,7 @@ export default { text: self.text, asMfm: binding.modifiers.mfm, direction: binding.modifiers.left ? 'left' : binding.modifiers.right ? 'right' : binding.modifiers.top ? 'top' : binding.modifiers.bottom ? 'bottom' : 'top', - targetElement: el, + targetElement: src, }, { closed: () => dispose(), }); @@ -66,13 +75,13 @@ export default { }; }; - el.addEventListener('selectstart', ev => { + src.addEventListener('selectstart', (ev) => { ev.preventDefault(); }); - el.addEventListener(start, (ev) => { - window.clearTimeout(self.showTimer); - window.clearTimeout(self.hideTimer); + src.addEventListener(start, () => { + if (self.showTimer != null) window.clearTimeout(self.showTimer); + if (self.hideTimer != null) window.clearTimeout(self.hideTimer); if (delay === 0) { self.show(); } else { @@ -80,9 +89,9 @@ export default { } }, { passive: true }); - el.addEventListener(end, () => { - window.clearTimeout(self.showTimer); - window.clearTimeout(self.hideTimer); + src.addEventListener(end, () => { + if (self.showTimer != null) window.clearTimeout(self.showTimer); + if (self.hideTimer != null) window.clearTimeout(self.hideTimer); if (delay === 0) { self.close(); } else { @@ -90,19 +99,21 @@ export default { } }, { passive: true }); - el.addEventListener('click', () => { - window.clearTimeout(self.showTimer); + src.addEventListener('click', () => { + if (self.showTimer != null) window.clearTimeout(self.showTimer); self.close(); }); }, - updated(el, binding) { - const self = el._tooltipDirective_; + async updated(src, binding) { + //@ts-expect-error HTMLElementにプロパティを追加している + const self = src._tooltipDirective_; self.text = binding.value as string; }, - unmounted(el, binding, vn) { - const self = el._tooltipDirective_; - window.clearInterval(self.checkTimer); + async unmounted(src) { + //@ts-expect-error HTMLElementにプロパティを追加している + const self = src._tooltipDirective_; + if (self.checkTimer != null) window.clearInterval(self.checkTimer); }, -} as Directive; +} satisfies VTooltip as VTooltip; diff --git a/packages/frontend/src/directives/user-preview.ts b/packages/frontend/src/directives/user-preview.ts index 278d842d09..b2006b178f 100644 --- a/packages/frontend/src/directives/user-preview.ts +++ b/packages/frontend/src/directives/user-preview.ts @@ -3,10 +3,32 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { defineAsyncComponent, Directive, ref } from 'vue'; +import { type ObjectDirective, defineAsyncComponent, ref } from 'vue'; import { popup } from '@/os.js'; -export class UserPreview { +type VUserPreview = ObjectDirective; + +export const vUserPreview = { + async mounted(src, binding) { + if (binding.value == null) return; + + // TODO: 新たにプロパティを作るのをやめMapを使う + // ただメモリ的には↓の方が省メモリかもしれないので検討中 + const self = (src as any)._userPreviewDirective_ = {} as any; + + self.preview = new UserPreview(src, binding.value); + }, + + async unmounted(src, binding) { + if (binding.value == null) return; + + //@ts-expect-error HTMLElementにプロパティを追加している + const self = src._userPreviewDirective_; + self.preview.detach(); + }, +} satisfies VUserPreview as VUserPreview; + +class UserPreview { private el; private user; private showTimer; @@ -41,10 +63,10 @@ export class UserPreview { source: this.el, }, { mouseover: () => { - window.clearTimeout(this.hideTimer); + if (this.hideTimer != null) window.clearTimeout(this.hideTimer); }, mouseleave: () => { - window.clearTimeout(this.showTimer); + if (this.showTimer != null) window.clearTimeout(this.showTimer); this.hideTimer = window.setTimeout(this.close, 500); }, closed: () => dispose(), @@ -58,8 +80,8 @@ export class UserPreview { this.checkTimer = window.setInterval(() => { if (!document.body.contains(this.el)) { - window.clearTimeout(this.showTimer); - window.clearTimeout(this.hideTimer); + if (this.showTimer != null) window.clearTimeout(this.showTimer); + if (this.hideTimer != null) window.clearTimeout(this.hideTimer); this.close(); } }, 1000); @@ -67,26 +89,26 @@ export class UserPreview { private close() { if (this.promise) { - window.clearInterval(this.checkTimer); + if (this.checkTimer != null) window.clearInterval(this.checkTimer); this.promise.cancel(); this.promise = null; } } private onMouseover() { - window.clearTimeout(this.showTimer); - window.clearTimeout(this.hideTimer); + if (this.showTimer != null) window.clearTimeout(this.showTimer); + if (this.hideTimer != null) window.clearTimeout(this.hideTimer); this.showTimer = window.setTimeout(this.show, 500); } private onMouseleave() { - window.clearTimeout(this.showTimer); - window.clearTimeout(this.hideTimer); + if (this.showTimer != null) window.clearTimeout(this.showTimer); + if (this.hideTimer != null) window.clearTimeout(this.hideTimer); this.hideTimer = window.setTimeout(this.close, 500); } private onClick() { - window.clearTimeout(this.showTimer); + if (this.showTimer != null) window.clearTimeout(this.showTimer); this.close(); } @@ -102,22 +124,3 @@ export class UserPreview { this.el.removeEventListener('click', this.onClick); } } - -export default { - mounted(el: HTMLElement, binding, vn) { - if (binding.value == null) return; - - // TODO: 新たにプロパティを作るのをやめMapを使う - // ただメモリ的には↓の方が省メモリかもしれないので検討中 - const self = (el as any)._userPreviewDirective_ = {} as any; - - self.preview = new UserPreview(el, binding.value); - }, - - unmounted(el, binding, vn) { - if (binding.value == null) return; - - const self = el._userPreviewDirective_; - self.preview.detach(); - }, -} as Directive; diff --git a/packages/frontend/src/widgets/index.ts b/packages/frontend/src/widgets/index.ts index e269fcf9eb..cf23818549 100644 --- a/packages/frontend/src/widgets/index.ts +++ b/packages/frontend/src/widgets/index.ts @@ -3,40 +3,75 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { App, defineAsyncComponent } from 'vue'; +import { type App, defineAsyncComponent } from 'vue'; + +const WidgetProfile = defineAsyncComponent(() => import('@/widgets/WidgetProfile.vue')); +const WidgetInstanceInfo = defineAsyncComponent(() => import('@/widgets/WidgetInstanceInfo.vue')); +const WidgetMemo = defineAsyncComponent(() => import('@/widgets/WidgetMemo.vue')); +const WidgetNotifications = defineAsyncComponent(() => import('@/widgets/WidgetNotifications.vue')); +const WidgetTimeline = defineAsyncComponent(() => import('@/widgets/WidgetTimeline.vue')); +const WidgetCalendar = defineAsyncComponent(() => import('@/widgets/WidgetCalendar.vue')); +const WidgetRss = defineAsyncComponent(() => import('@/widgets/WidgetRss.vue')); +const WidgetRssTicker = defineAsyncComponent(() => import('@/widgets/WidgetRssTicker.vue')); +const WidgetTrends = defineAsyncComponent(() => import('@/widgets/WidgetTrends.vue')); +const WidgetClock = defineAsyncComponent(() => import('@/widgets/WidgetClock.vue')); +const WidgetActivity = defineAsyncComponent(() => import('@/widgets/WidgetActivity.vue')); +const WidgetPhotos = defineAsyncComponent(() => import('@/widgets/WidgetPhotos.vue')); +const WidgetDigitalClock = defineAsyncComponent(() => import('@/widgets/WidgetDigitalClock.vue')); +const WidgetUnixClock = defineAsyncComponent(() => import('@/widgets/WidgetUnixClock.vue')); +const WidgetFederation = defineAsyncComponent(() => import('@/widgets/WidgetFederation.vue')); +const WidgetInstanceCloud = defineAsyncComponent(() => import('@/widgets/WidgetInstanceCloud.vue')); +const WidgetPostForm = defineAsyncComponent(() => import('@/widgets/WidgetPostForm.vue')); +const WidgetSlideshow = defineAsyncComponent(() => import('@/widgets/WidgetSlideshow.vue')); +const WidgetServerMetric = defineAsyncComponent(() => import('@/widgets/server-metric/index.vue')); +const WidgetOnlineUsers = defineAsyncComponent(() => import('@/widgets/WidgetOnlineUsers.vue')); +const WidgetJobQueue = defineAsyncComponent(() => import('@/widgets/WidgetJobQueue.vue')); +const WidgetButton = defineAsyncComponent(() => import('@/widgets/WidgetButton.vue')); +const WidgetAiscript = defineAsyncComponent(() => import('@/widgets/WidgetAiscript.vue')); +const WidgetAiscriptApp = defineAsyncComponent(() => import('@/widgets/WidgetAiscriptApp.vue')); +const WidgetAichan = defineAsyncComponent(() => import('@/widgets/WidgetAichan.vue')); +const WidgetUserList = defineAsyncComponent(() => import('@/widgets/WidgetUserList.vue')); +const WidgetClicker = defineAsyncComponent(() => import('@/widgets/WidgetClicker.vue')); +const WidgetBirthdayFollowings = defineAsyncComponent(() => import('@/widgets/WidgetBirthdayFollowings.vue')); export default function(app: App) { - app.component('WidgetProfile', defineAsyncComponent(() => import('./WidgetProfile.vue'))); - app.component('WidgetInstanceInfo', defineAsyncComponent(() => import('./WidgetInstanceInfo.vue'))); - app.component('WidgetMemo', defineAsyncComponent(() => import('./WidgetMemo.vue'))); - app.component('WidgetNotifications', defineAsyncComponent(() => import('./WidgetNotifications.vue'))); - app.component('WidgetTimeline', defineAsyncComponent(() => import('./WidgetTimeline.vue'))); - app.component('WidgetCalendar', defineAsyncComponent(() => import('./WidgetCalendar.vue'))); - app.component('WidgetRss', defineAsyncComponent(() => import('./WidgetRss.vue'))); - app.component('WidgetRssTicker', defineAsyncComponent(() => import('./WidgetRssTicker.vue'))); - app.component('WidgetTrends', defineAsyncComponent(() => import('./WidgetTrends.vue'))); - app.component('WidgetClock', defineAsyncComponent(() => import('./WidgetClock.vue'))); - app.component('WidgetActivity', defineAsyncComponent(() => import('./WidgetActivity.vue'))); - app.component('WidgetPhotos', defineAsyncComponent(() => import('./WidgetPhotos.vue'))); - app.component('WidgetDigitalClock', defineAsyncComponent(() => import('./WidgetDigitalClock.vue'))); - app.component('WidgetUnixClock', defineAsyncComponent(() => import('./WidgetUnixClock.vue'))); - app.component('WidgetFederation', defineAsyncComponent(() => import('./WidgetFederation.vue'))); - app.component('WidgetPostForm', defineAsyncComponent(() => import('./WidgetPostForm.vue'))); - app.component('WidgetSlideshow', defineAsyncComponent(() => import('./WidgetSlideshow.vue'))); - app.component('WidgetServerMetric', defineAsyncComponent(() => import('./server-metric/index.vue'))); - app.component('WidgetOnlineUsers', defineAsyncComponent(() => import('./WidgetOnlineUsers.vue'))); - app.component('WidgetJobQueue', defineAsyncComponent(() => import('./WidgetJobQueue.vue'))); - app.component('WidgetInstanceCloud', defineAsyncComponent(() => import('./WidgetInstanceCloud.vue'))); - app.component('WidgetButton', defineAsyncComponent(() => import('./WidgetButton.vue'))); - app.component('WidgetAiscript', defineAsyncComponent(() => import('./WidgetAiscript.vue'))); - app.component('WidgetAiscriptApp', defineAsyncComponent(() => import('./WidgetAiscriptApp.vue'))); - app.component('WidgetAichan', defineAsyncComponent(() => import('./WidgetAichan.vue'))); - app.component('WidgetUserList', defineAsyncComponent(() => import('./WidgetUserList.vue'))); - app.component('WidgetClicker', defineAsyncComponent(() => import('./WidgetClicker.vue'))); - app.component('WidgetBirthdayFollowings', defineAsyncComponent(() => import('./WidgetBirthdayFollowings.vue'))); + for (const [key, value] of Object.entries(widgets)) { + app.component(key, value); + } } -export const widgets = [ +const widgets = { + WidgetProfile, + WidgetInstanceInfo, + WidgetMemo, + WidgetNotifications, + WidgetTimeline, + WidgetCalendar, + WidgetRss, + WidgetRssTicker, + WidgetTrends, + WidgetClock, + WidgetActivity, + WidgetPhotos, + WidgetDigitalClock, + WidgetUnixClock, + WidgetFederation, + WidgetInstanceCloud, + WidgetPostForm, + WidgetSlideshow, + WidgetServerMetric, + WidgetOnlineUsers, + WidgetJobQueue, + WidgetButton, + WidgetAiscript, + WidgetAiscriptApp, + WidgetAichan, + WidgetUserList, + WidgetClicker, + WidgetBirthdayFollowings, +} as const; + +export const widgetDefs = [ 'profile', 'instanceInfo', 'memo', @@ -65,4 +100,37 @@ export const widgets = [ 'userList', 'clicker', 'birthdayFollowings', -]; +] as const; + +declare module '@vue/runtime-core' { + export interface GlobalComponents { + WidgetProfile: typeof WidgetProfile; + WidgetInstanceInfo: typeof WidgetInstanceInfo; + WidgetMemo: typeof WidgetMemo; + WidgetNotifications: typeof WidgetNotifications; + WidgetTimeline: typeof WidgetTimeline; + WidgetCalendar: typeof WidgetCalendar; + WidgetRss: typeof WidgetRss; + WidgetRssTicker: typeof WidgetRssTicker; + WidgetTrends: typeof WidgetTrends; + WidgetClock: typeof WidgetClock; + WidgetActivity: typeof WidgetActivity; + WidgetPhotos: typeof WidgetPhotos; + WidgetDigitalClock: typeof WidgetDigitalClock; + WidgetUnixClock: typeof WidgetUnixClock; + WidgetFederation: typeof WidgetFederation; + WidgetInstanceCloud: typeof WidgetInstanceCloud; + WidgetPostForm: typeof WidgetPostForm; + WidgetSlideshow: typeof WidgetSlideshow; + WidgetServerMetric: typeof WidgetServerMetric; + WidgetOnlineUsers: typeof WidgetOnlineUsers; + WidgetJobQueue: typeof WidgetJobQueue; + WidgetButton: typeof WidgetButton; + WidgetAiscript: typeof WidgetAiscript; + WidgetAiscriptApp: typeof WidgetAiscriptApp; + WidgetAichan: typeof WidgetAichan; + WidgetUserList: typeof WidgetUserList; + WidgetClicker: typeof WidgetClicker; + WidgetBirthdayFollowings: typeof WidgetBirthdayFollowings; + } +} diff --git a/packages/frontend/test/emoji.test.ts b/packages/frontend/test/emoji.test.ts index cf686efd0d..1130ea767a 100644 --- a/packages/frontend/test/emoji.test.ts +++ b/packages/frontend/test/emoji.test.ts @@ -6,9 +6,9 @@ import { describe, test, assert, afterEach } from 'vitest'; import { render, cleanup, type RenderResult } from '@testing-library/vue'; import { defaultStoreState } from './init.js'; -import { getEmojiName } from '@@/js/emojilist.js'; -import { components } from '@/components/index.js'; +import { getEmojiName } from '../../frontend-shared/js/emojilist.js'; import { directives } from '@/directives/index.js'; +import { components } from '@/components/index.js'; import MkEmoji from '@/components/global/MkEmoji.vue'; describe('Emoji', () => { diff --git a/packages/frontend/test/note.test.ts b/packages/frontend/test/note.test.ts index 7ce5f23e22..2af9a70f30 100644 --- a/packages/frontend/test/note.test.ts +++ b/packages/frontend/test/note.test.ts @@ -7,8 +7,8 @@ import { describe, test, assert, afterEach } from 'vitest'; import { render, cleanup, type RenderResult } from '@testing-library/vue'; import './init'; import type * as Misskey from 'misskey-js'; -import { components } from '@/components/index.js'; import { directives } from '@/directives/index.js'; +import { components } from '@/components/index.js'; import MkMediaImage from '@/components/MkMediaImage.vue'; describe('MkMediaImage', () => { diff --git a/packages/frontend/test/url-preview.test.ts b/packages/frontend/test/url-preview.test.ts index 4b79d33348..24375ba5b6 100644 --- a/packages/frontend/test/url-preview.test.ts +++ b/packages/frontend/test/url-preview.test.ts @@ -7,8 +7,8 @@ import { describe, test, assert, afterEach } from 'vitest'; import { render, cleanup, type RenderResult } from '@testing-library/vue'; import './init'; import type { summaly } from '@misskey-dev/summaly'; -import { components } from '@/components/index.js'; import { directives } from '@/directives/index.js'; +import { components } from '@/components/index.js'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; type SummalyResult = Awaited>;