From 49c9215b61a70e020c5b28a68756bfdc0df3cac7 Mon Sep 17 00:00:00 2001 From: taiyme <53635909+taiyme@users.noreply.github.com> Date: Fri, 1 Nov 2024 21:07:35 +0900 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20=E3=82=AB=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=83=A0=E3=83=87=E3=82=A3=E3=83=AC=E3=82=AF=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=83=96=E3=81=AE=E5=9E=8B=E4=BB=98=E3=81=91=20?= =?UTF-8?q?=E3=81=AA=E3=81=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/.storybook/preview.ts | 6 +- packages/frontend/src/boot/common.ts | 4 +- .../frontend/src/components/MkWidgets.vue | 2 +- packages/frontend/src/components/index.ts | 108 +++++++-------- .../frontend/src/directives/adaptive-bg.ts | 18 ++- .../src/directives/adaptive-border.ts | 18 ++- packages/frontend/src/directives/anim.ts | 17 ++- packages/frontend/src/directives/appear.ts | 19 +-- .../frontend/src/directives/click-anime.ts | 26 ++-- .../frontend/src/directives/follow-append.ts | 41 ------ packages/frontend/src/directives/get-size.ts | 55 ++++---- packages/frontend/src/directives/hotkey.ts | 37 +++-- packages/frontend/src/directives/index.ts | 65 +++++---- packages/frontend/src/directives/panel.ts | 18 ++- packages/frontend/src/directives/ripple.ts | 25 ++-- packages/frontend/src/directives/tooltip.ts | 49 ++++--- .../frontend/src/directives/user-preview.ts | 45 +++--- packages/frontend/src/widgets/index.ts | 130 +++++++++++++----- packages/frontend/test/emoji.test.ts | 2 +- packages/frontend/test/note.test.ts | 2 +- packages/frontend/test/url-preview.test.ts | 2 +- 21 files changed, 391 insertions(+), 298 deletions(-) delete mode 100644 packages/frontend/src/directives/follow-append.ts diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts index 1937ecd20e92..643c3a7128d5 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/index.js'), 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: components }, { default: directives }, { default: widgets }, { applyTheme }, { defaultStore }, os]) => { + ]).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 0b410c7bbb30..d28d1e4e2baf 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, commitHash } 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 } from '@/i18n.js'; @@ -257,9 +257,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 ef22d70cfb89..1e7590dfadc7 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -55,7 +55,7 @@ import { v4 as uuid } from 'uuid'; import { isLink } from '@@/js/is-link.js'; 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'; diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index 4492e38fdb3c..8e9fab40c16d 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -3,30 +3,30 @@ * 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'; -import MkAcct from './global/MkAcct.vue'; -import MkAvatar from './global/MkAvatar.vue'; -import MkEmoji from './global/MkEmoji.vue'; -import MkCondensedLine from './global/MkCondensedLine.vue'; -import MkCustomEmoji from './global/MkCustomEmoji.vue'; -import MkUserName from './global/MkUserName.vue'; -import MkEllipsis from './global/MkEllipsis.vue'; -import MkTime from './global/MkTime.vue'; -import MkUrl from './global/MkUrl.vue'; -import I18n from './global/I18n.vue'; -import RouterView from './global/RouterView.vue'; -import MkLoading from './global/MkLoading.vue'; -import MkError from './global/MkError.vue'; -import MkAd from './global/MkAd.vue'; -import MkPageHeader from './global/MkPageHeader.vue'; -import MkSpacer from './global/MkSpacer.vue'; -import MkFooterSpacer from './global/MkFooterSpacer.vue'; -import MkStickyContainer from './global/MkStickyContainer.vue'; -import MkLazy from './global/MkLazy.vue'; -import TmsNoCache from './global/TmsNoCache.vue'; +import I18n from '@/components/global/I18n.vue'; +import RouterView from '@/components/global/RouterView.vue'; +import Mfm from '@/components/global/MkMfm.js'; +import MkA from '@/components/global/MkA.vue'; +import MkAcct from '@/components/global/MkAcct.vue'; +import MkAd from '@/components/global/MkAd.vue'; +import MkAvatar from '@/components/global/MkAvatar.vue'; +import MkCondensedLine from '@/components/global/MkCondensedLine.vue'; +import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue'; +import MkEllipsis from '@/components/global/MkEllipsis.vue'; +import MkEmoji from '@/components/global/MkEmoji.vue'; +import MkError from '@/components/global/MkError.vue'; +import MkFooterSpacer from '@/components/global/MkFooterSpacer.vue'; +import MkLazy from '@/components/global/MkLazy.vue'; +import MkLoading from '@/components/global/MkLoading.vue'; +import MkPageHeader from '@/components/global/MkPageHeader.vue'; +import MkSpacer from '@/components/global/MkSpacer.vue'; +import MkStickyContainer from '@/components/global/MkStickyContainer.vue'; +import MkTime from '@/components/global/MkTime.vue'; +import MkUrl from '@/components/global/MkUrl.vue'; +import MkUserName from '@/components/global/MkUserName.vue'; +import TmsNoCache from '@/components/global/TmsNoCache.vue'; // eslint-disable-next-line import/no-default-export export default function(app: App) { @@ -36,29 +36,29 @@ 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, - TmsNoCache: TmsNoCache, -}; + I18n, + RouterView, + Mfm, + MkA, + MkAcct, + MkAd, + MkAvatar, + MkCondensedLine, + MkCustomEmoji, + MkEllipsis, + MkEmoji, + MkError, + MkFooterSpacer, + MkLazy, + MkLoading, + MkPageHeader, + MkSpacer, + MkStickyContainer, + MkTime, + MkUrl, + MkUserName, + TmsNoCache, +} as const; declare module '@vue/runtime-core' { export interface GlobalComponents { @@ -67,22 +67,22 @@ 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; TmsNoCache: typeof TmsNoCache; } } diff --git a/packages/frontend/src/directives/adaptive-bg.ts b/packages/frontend/src/directives/adaptive-bg.ts index dee0372d65a0..392ab95ae5b6 100644 --- a/packages/frontend/src/directives/adaptive-bg.ts +++ b/packages/frontend/src/directives/adaptive-bg.ts @@ -3,12 +3,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Directive } from 'vue'; -import { getBgColor } from '@/scripts/tms/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/tms/get-bg-color.js'), + ]); -// eslint-disable-next-line import/no-default-export -export default { - mounted(src, binding, vn) { const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const myBg = window.getComputedStyle(src).backgroundColor; @@ -19,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 a6280372af76..8a650fd194ad 100644 --- a/packages/frontend/src/directives/adaptive-border.ts +++ b/packages/frontend/src/directives/adaptive-border.ts @@ -3,12 +3,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Directive } from 'vue'; -import { getBgColor } from '@/scripts/tms/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/tms/get-bg-color.js'), + ]); -// eslint-disable-next-line import/no-default-export -export default { - mounted(src, binding, vn) { const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const myBg = window.getComputedStyle(src).backgroundColor; @@ -19,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 800f8712445a..3e0b08b59924 100644 --- a/packages/frontend/src/directives/anim.ts +++ b/packages/frontend/src/directives/anim.ts @@ -3,22 +3,25 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Directive } from 'vue'; +import type { ObjectDirective } from 'vue'; -// eslint-disable-next-line import/no-default-export -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 de0378d3f908..af1af491e5c5 100644 --- a/packages/frontend/src/directives/appear.ts +++ b/packages/frontend/src/directives/appear.ts @@ -3,16 +3,17 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Directive } from 'vue'; +import type { ObjectDirective } from 'vue'; -// eslint-disable-next-line import/no-default-export -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(); } }); @@ -22,7 +23,7 @@ export default { src._observer_ = observer; }, - unmounted(src, binding, vn) { - if (src._observer_) src._observer_.disconnect(); + async unmounted(src) { + 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 235d80b4bb62..c9dab53946b9 100644 --- a/packages/frontend/src/directives/click-anime.ts +++ b/packages/frontend/src/directives/click-anime.ts @@ -3,21 +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'), + ]); -// eslint-disable-next-line import/no-default-export -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'); @@ -28,14 +34,14 @@ export default { }, { passive: true }); }, { passive: true }); - el.addEventListener('click', () => { + src.addEventListener('click', () => { target.classList.add('_anime_bounce'); target.classList.remove('_anime_bounce_ready'); }, { passive: true }); - el.addEventListener('animationend', () => { + src.addEventListener('animationend', () => { target.classList.remove('_anime_bounce'); target.classList.add('_anime_bounce_standBy'); }, { passive: true }); }, -} 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 6d58373468f9..000000000000 --- a/packages/frontend/src/directives/follow-append.ts +++ /dev/null @@ -1,41 +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'; - -// eslint-disable-next-line import/no-default-export -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 55801b974bc6..f25e4cfeed68 100644 --- a/packages/frontend/src/directives/get-size.ts +++ b/packages/frontend/src/directives/get-size.ts @@ -3,15 +3,38 @@ * 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) { + const resize = new ResizeObserver(() => { + calc(src); + }); + resize.observe(src); + + mountings.set(src, { resize, fn: binding.value }); + calc(src); + }, + + async unmounted(src, binding) { + 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 +45,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,25 +59,3 @@ function calc(src: Element) { info.fn(width, height); } - -// eslint-disable-next-line import/no-default-export -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 9e81f76d2da8..16314742fdd4 100644 --- a/packages/frontend/src/directives/hotkey.ts +++ b/packages/frontend/src/directives/hotkey.ts @@ -3,28 +3,35 @@ * 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'; -// eslint-disable-next-line import/no-default-export -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 }); + src._hotkey_global = binding.modifiers.global === true; + + src._keyHandler = makeHotkey(binding.value); + + if (src._hotkey_global) { + document.addEventListener('keydown', src._keyHandler, { passive: false }); } else { - el.addEventListener('keydown', el._keyHandler, { passive: false }); + src.addEventListener('keydown', src._keyHandler, { passive: false }); } }, - unmounted(el) { - if (el._hotkey_global) { - document.removeEventListener('keydown', el._keyHandler); + async unmounted(src) { + if (src._hotkey_global) { + document.removeEventListener('keydown', src._keyHandler); } else { - el.removeEventListener('keydown', el._keyHandler); + 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 58ddfc0795e8..04ec508e452e 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'; // eslint-disable-next-line import/no-default-export export default function(app: App) { @@ -25,16 +25,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 8cddf3931c77..d75cc8b8f280 100644 --- a/packages/frontend/src/directives/panel.ts +++ b/packages/frontend/src/directives/panel.ts @@ -3,12 +3,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Directive } from 'vue'; -import { getBgColor } from '@/scripts/tms/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/tms/get-bg-color.js'), + ]); -// eslint-disable-next-line import/no-default-export -export default { - mounted(src, binding, vn) { const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'); @@ -19,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 2c3681e9b691..98f321846cda 100644 --- a/packages/frontend/src/directives/ripple.ts +++ b/packages/frontend/src/directives/ripple.ts @@ -3,24 +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'; -// eslint-disable-next-line import/no-default-export -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(), }); }, { passive: true }); }, -}; +} satisfies VRipple as VRipple; diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts index c4222c40d0fb..73dac32c9b95 100644 --- a/packages/frontend/src/directives/tooltip.ts +++ b/packages/frontend/src/directives/tooltip.ts @@ -6,19 +6,26 @@ // 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'; -// eslint-disable-next-line import/no-default-export -export default { - mounted(el: HTMLElement, binding, vn) { const delay = binding.modifiers.noDelay ? 0 : 100; - const self = (el as any)._tooltipDirective_ = {} as any; + const self = (src as any)._tooltipDirective_ = {} as any; self.text = binding.value as string; self._close = null; @@ -35,7 +42,7 @@ export default { }; if (binding.arg === 'dialog') { - el.addEventListener('click', (ev) => { + src.addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation(); alert({ @@ -46,7 +53,7 @@ export default { } 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 +63,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,11 +73,11 @@ export default { }; }; - el.addEventListener('selectstart', ev => { + src.addEventListener('selectstart', (ev) => { ev.preventDefault(); }, { passive: false }); - el.addEventListener(start, () => { + src.addEventListener(start, () => { window.clearTimeout(self.showTimer); window.clearTimeout(self.hideTimer); if (delay === 0) { @@ -80,7 +87,7 @@ export default { } }, { passive: true }); - el.addEventListener(end, () => { + src.addEventListener(end, () => { window.clearTimeout(self.showTimer); window.clearTimeout(self.hideTimer); if (delay === 0) { @@ -90,19 +97,19 @@ export default { } }, { passive: true }); - el.addEventListener('click', () => { + src.addEventListener('click', () => { window.clearTimeout(self.showTimer); self.close(); }, { passive: true }); }, - updated(el, binding) { - const self = el._tooltipDirective_; + async updated(src, binding) { + const self = (src as any)._tooltipDirective_; self.text = binding.value as string; }, - unmounted(el, binding, vn) { - const self = el._tooltipDirective_; + async unmounted(src) { + const self = (src as any)._tooltipDirective_; 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 3c7322fc91a8..f054adb1ca3b 100644 --- a/packages/frontend/src/directives/user-preview.ts +++ b/packages/frontend/src/directives/user-preview.ts @@ -3,10 +3,31 @@ * 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; + + const self = src._userPreviewDirective_; + self.preview.detach(); + }, +} satisfies VUserPreview as VUserPreview; + +class UserPreview { private el; private user; private showTimer; @@ -102,23 +123,3 @@ export class UserPreview { this.el.removeEventListener('click', this.onClick); } } - -// eslint-disable-next-line import/no-default-export -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 87d2b404c75b..770e493c78aa 100644 --- a/packages/frontend/src/widgets/index.ts +++ b/packages/frontend/src/widgets/index.ts @@ -3,41 +3,76 @@ * 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')); // eslint-disable-next-line import/no-default-export 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', @@ -66,4 +101,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 9b1e44333693..1130ea767aa1 100644 --- a/packages/frontend/test/emoji.test.ts +++ b/packages/frontend/test/emoji.test.ts @@ -7,8 +7,8 @@ import { describe, test, assert, afterEach } from 'vitest'; import { render, cleanup, type RenderResult } from '@testing-library/vue'; import { defaultStoreState } from './init.js'; import { getEmojiName } from '../../frontend-shared/js/emojilist.js'; -import { components } from '@/components/index.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 9cefdaab1dd6..2d9eb327d3fc 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.js'; 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 c1ab3f3f6ca4..446ed79acaf1 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.js'; 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>;