refactor: カスタムディレクティブの型付け (taiyme#298)

This commit is contained in:
taiyme 2024-10-22 22:55:33 +09:00 committed by kakkokari-gtyih
parent 15ae1605ec
commit 1106af8d2d
12 changed files with 163 additions and 142 deletions

View file

@ -3,11 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Directive } from 'vue'; import type { ObjectDirective } from 'vue';
import { getBgColor } from '@/scripts/get-bg-color.js'; import { getBgColor } from '@/scripts/get-bg-color.js';
export default { export const vAdaptiveBg: ObjectDirective<HTMLElement, null | undefined> = {
mounted(src, binding, vn) { mounted(src) {
const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const myBg = window.getComputedStyle(src).backgroundColor; const myBg = window.getComputedStyle(src).backgroundColor;
@ -18,4 +18,4 @@ export default {
src.style.backgroundColor = myBg; src.style.backgroundColor = myBg;
} }
}, },
} as Directive; };

View file

@ -3,11 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Directive } from 'vue'; import type { ObjectDirective } from 'vue';
import { getBgColor } from '@/scripts/get-bg-color.js'; import { getBgColor } from '@/scripts/get-bg-color.js';
export default { export const vAdaptiveBorder: ObjectDirective<HTMLElement, null | undefined> = {
mounted(src, binding, vn) { mounted(src) {
const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const myBg = window.getComputedStyle(src).backgroundColor; const myBg = window.getComputedStyle(src).backgroundColor;
@ -18,4 +18,4 @@ export default {
src.style.borderColor = myBg; src.style.borderColor = myBg;
} }
}, },
} as Directive; };

View file

@ -3,21 +3,23 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Directive } from 'vue'; import type { ObjectDirective } from 'vue';
export default { export const vAnim: ObjectDirective<HTMLElement, number | null | undefined> = {
beforeMount(src, binding, vn) { beforeMount(src) {
src.style.opacity = '0'; src.style.opacity = '0';
src.style.transform = 'scale(0.9)'; 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'); src.classList.add('_zoom');
}, },
mounted(src, binding, vn) { mounted(src) {
window.setTimeout(() => { window.setTimeout(() => {
src.style.opacity = '1'; src.style.opacity = '1';
src.style.transform = 'none'; src.style.transform = 'none';
}, 1); }, 1);
}, },
} as Directive; };

View file

@ -3,15 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Directive } from 'vue'; import type { ObjectDirective } from 'vue';
export default { export const vAppear: ObjectDirective<HTMLElement, (() => unknown) | null | undefined> = {
mounted(src, binding, vn) { mounted(src, binding) {
const fn = binding.value; const fn = binding.value;
if (fn == null) return; if (fn == null) return;
const observer = new IntersectionObserver(entries => { const observer = new IntersectionObserver((entries) => {
if (entries.some(entry => entry.isIntersecting)) { if (entries.some((entry) => entry.isIntersecting)) {
fn(); fn();
} }
}); });
@ -21,7 +21,7 @@ export default {
src._observer_ = observer; src._observer_ = observer;
}, },
unmounted(src, binding, vn) { unmounted(src) {
if (src._observer_) src._observer_.disconnect(); src._observer_?.disconnect();
}, },
} as Directive; };

View file

@ -3,20 +3,20 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Directive } from 'vue'; import type { ObjectDirective } from 'vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
export default { export const vClickAnime: ObjectDirective<HTMLElement, null | undefined> = {
mounted(el: HTMLElement, binding, vn) { mounted(src) {
if (!defaultStore.state.animation) return; if (!defaultStore.state.animation) return;
const target = el.children[0]; const target = src.children[0];
if (target == null) return; if (target == null) return;
target.classList.add('_anime_bounce_standBy'); target.classList.add('_anime_bounce_standBy');
el.addEventListener('mousedown', () => { src.addEventListener('mousedown', () => {
target.classList.remove('_anime_bounce'); target.classList.remove('_anime_bounce');
target.classList.add('_anime_bounce_standBy'); target.classList.add('_anime_bounce_standBy');
@ -27,14 +27,14 @@ export default {
}); });
}); });
el.addEventListener('click', () => { src.addEventListener('click', () => {
target.classList.add('_anime_bounce'); target.classList.add('_anime_bounce');
target.classList.remove('_anime_bounce_ready'); target.classList.remove('_anime_bounce_ready');
}); });
el.addEventListener('animationend', () => { src.addEventListener('animationend', () => {
target.classList.remove('_anime_bounce'); target.classList.remove('_anime_bounce');
target.classList.add('_anime_bounce_standBy'); target.classList.add('_anime_bounce_standBy');
}); });
}, },
} as Directive; };

View file

@ -3,15 +3,36 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Directive } from 'vue'; import type { ObjectDirective } from 'vue';
const mountings = new Map<Element, { const mountings = new Map<HTMLElement, {
resize: ResizeObserver; resize: ResizeObserver;
intersection?: IntersectionObserver; intersection?: IntersectionObserver;
fn: (w: number, h: number) => void; fn: (w: number, h: number) => void;
}>(); }>();
function calc(src: Element) { export const vGetSize: ObjectDirective<HTMLElement, ((w: number, h: number) => unknown) | null | undefined> = {
mounted(src, binding) {
const resize = new ResizeObserver(() => {
calc(src);
});
resize.observe(src);
mountings.set(src, { resize, fn: binding.value });
calc(src);
},
unmounted(src, binding) {
if (binding.value != null) binding.value(0, 0);
const info = mountings.get(src);
if (!info) return;
info.resize.disconnect();
if (info.intersection) info.intersection.disconnect();
mountings.delete(src);
},
};
function calc(src: HTMLElement) {
const info = mountings.get(src); const info = mountings.get(src);
const height = src.clientHeight; const height = src.clientHeight;
const width = src.clientWidth; const width = src.clientWidth;
@ -22,8 +43,8 @@ function calc(src: Element) {
if (!height) { if (!height) {
// IntersectionObserverで表示検出する // IntersectionObserverで表示検出する
if (!info.intersection) { if (!info.intersection) {
info.intersection = new IntersectionObserver(entries => { info.intersection = new IntersectionObserver((entries) => {
if (entries.some(entry => entry.isIntersecting)) calc(src); if (entries.some((entry) => entry.isIntersecting)) calc(src);
}); });
} }
info.intersection.observe(src); info.intersection.observe(src);
@ -36,24 +57,3 @@ function calc(src: Element) {
info.fn(width, height); 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<Element, (w: number, h: number) => void>;

View file

@ -3,27 +3,28 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Directive } from 'vue'; import type { ObjectDirective } from 'vue';
import type { Keymap } from '@/scripts/hotkey.js';
import { makeHotkey } from '@/scripts/hotkey.js'; import { makeHotkey } from '@/scripts/hotkey.js';
export default { export const vHotkey: ObjectDirective<HTMLElement, Keymap | null | undefined, 'global'> = {
mounted(el, binding) { mounted(src, binding) {
el._hotkey_global = binding.modifiers.global === true; src._hotkey_global = binding.modifiers.global === true;
el._keyHandler = makeHotkey(binding.value); src._keyHandler = makeHotkey(binding.value);
if (el._hotkey_global) { if (src._hotkey_global) {
document.addEventListener('keydown', el._keyHandler, { passive: false }); document.addEventListener('keydown', src._keyHandler, { passive: false });
} else { } else {
el.addEventListener('keydown', el._keyHandler, { passive: false }); src.addEventListener('keydown', src._keyHandler, { passive: false });
} }
}, },
unmounted(el) { unmounted(src) {
if (el._hotkey_global) { if (src._hotkey_global) {
document.removeEventListener('keydown', el._keyHandler); document.removeEventListener('keydown', src._keyHandler);
} else { } else {
el.removeEventListener('keydown', el._keyHandler); src.removeEventListener('keydown', src._keyHandler);
} }
}, },
} as Directive; };

View file

@ -3,19 +3,19 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { App } from 'vue'; import type { App } from 'vue';
import userPreview from './user-preview.js'; import { vAdaptiveBg } from '@/directives/adaptive-bg.js';
import getSize from './get-size.js'; import { vAdaptiveBorder } from '@/directives/adaptive-border.js';
import ripple from './ripple.js'; import { vAnim } from '@/directives/anim.js';
import tooltip from './tooltip.js'; import { vAppear } from '@/directives/appear.js';
import hotkey from './hotkey.js'; import { vClickAnime } from '@/directives/click-anime.js';
import appear from './appear.js'; import { vGetSize } from '@/directives/get-size.js';
import anim from './anim.js'; import { vHotkey } from '@/directives/hotkey.js';
import clickAnime from './click-anime.js'; import { vPanel } from '@/directives/panel.js';
import panel from './panel.js'; import { vRipple } from '@/directives/ripple.js';
import adaptiveBorder from './adaptive-border.js'; import { vTooltip } from '@/directives/tooltip.js';
import adaptiveBg from './adaptive-bg.js'; import { vUserPreview } from '@/directives/user-preview.js';
export default function(app: App) { export default function(app: App) {
for (const [key, value] of Object.entries(directives)) { for (const [key, value] of Object.entries(directives)) {
@ -24,16 +24,31 @@ export default function(app: App) {
} }
export const directives = { export const directives = {
'userPreview': userPreview, 'adaptive-bg': vAdaptiveBg,
'user-preview': userPreview, 'adaptive-border': vAdaptiveBorder,
'get-size': getSize, 'anim': vAnim,
'ripple': ripple, 'appear': vAppear,
'tooltip': tooltip, 'click-anime': vClickAnime,
'hotkey': hotkey, 'get-size': vGetSize,
'appear': appear, 'hotkey': vHotkey,
'anim': anim, 'panel': vPanel,
'click-anime': clickAnime, 'ripple': vRipple,
'panel': panel, 'tooltip': vTooltip,
'adaptive-border': adaptiveBorder, 'user-preview': vUserPreview,
'adaptive-bg': adaptiveBg, } 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;
}
}

View file

@ -3,11 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Directive } from 'vue'; import type { ObjectDirective } from 'vue';
import { getBgColor } from '@/scripts/get-bg-color.js'; import { getBgColor } from '@/scripts/get-bg-color.js';
export default { export const vPanel: ObjectDirective<HTMLElement, null | undefined> = {
mounted(src, binding, vn) { mounted(src) {
const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'); const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel');
@ -18,4 +18,4 @@ export default {
src.style.backgroundColor = 'var(--MI_THEME-panel)'; src.style.backgroundColor = 'var(--MI_THEME-panel)';
} }
}, },
} as Directive; };

View file

@ -3,19 +3,20 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import type { ObjectDirective } from 'vue';
import { popup } from '@/os.js'; import { popup } from '@/os.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
export default { export const vRipple: ObjectDirective<HTMLElement, boolean | null | undefined> = {
mounted(el, binding, vn) { mounted(src, binding) {
// 明示的に false であればバインドしない // 明示的に false であればバインドしない
if (binding.value === false) return; if (binding.value === false) return;
el.addEventListener('click', () => { src.addEventListener('click', () => {
const rect = el.getBoundingClientRect(); const rect = src.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (src.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); const y = rect.top + (src.offsetHeight / 2);
const { dispose } = popup(MkRippleEffect, { x, y }, { const { dispose } = popup(MkRippleEffect, { x, y }, {
end: () => dispose(), end: () => dispose(),

View file

@ -6,18 +6,19 @@
// TODO: useTooltip関数使うようにしたい // TODO: useTooltip関数使うようにしたい
// ただディレクティブ内でonUnmountedなどのcomposition api使えるのか不明 // ただディレクティブ内でonUnmountedなどのcomposition api使えるのか不明
import { defineAsyncComponent, Directive, ref } from 'vue'; import { defineAsyncComponent, ref } from 'vue';
import type { ObjectDirective } from 'vue';
import { isTouchUsing } from '@/scripts/touch.js'; import { isTouchUsing } from '@/scripts/touch.js';
import { popup, alert } from '@/os.js'; import { popup, alert } from '@/os.js';
const start = isTouchUsing ? 'touchstart' : 'mouseenter'; const start = isTouchUsing ? 'touchstart' : 'mouseenter';
const end = isTouchUsing ? 'touchend' : 'mouseleave'; const end = isTouchUsing ? 'touchend' : 'mouseleave';
export default { export const vTooltip: ObjectDirective<HTMLElement, string | null | undefined, 'noDelay' | 'mfm' | 'top' | 'right' | 'bottom' | 'left', 'dialog'> = {
mounted(el: HTMLElement, binding, vn) { mounted(src, binding) {
const delay = binding.modifiers.noDelay ? 0 : 100; 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.text = binding.value as string;
self._close = null; self._close = null;
@ -34,7 +35,7 @@ export default {
}; };
if (binding.arg === 'dialog') { if (binding.arg === 'dialog') {
el.addEventListener('click', (ev) => { src.addEventListener('click', (ev) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
alert({ alert({
@ -46,7 +47,7 @@ export default {
} }
self.show = () => { self.show = () => {
if (!document.body.contains(el)) return; if (!document.body.contains(src)) return;
if (self._close) return; if (self._close) return;
if (self.text == null) return; if (self.text == null) return;
@ -56,7 +57,7 @@ export default {
text: self.text, text: self.text,
asMfm: binding.modifiers.mfm, asMfm: binding.modifiers.mfm,
direction: binding.modifiers.left ? 'left' : binding.modifiers.right ? 'right' : binding.modifiers.top ? 'top' : binding.modifiers.bottom ? 'bottom' : 'top', direction: binding.modifiers.left ? 'left' : binding.modifiers.right ? 'right' : binding.modifiers.top ? 'top' : binding.modifiers.bottom ? 'bottom' : 'top',
targetElement: el, targetElement: src,
}, { }, {
closed: () => dispose(), closed: () => dispose(),
}); });
@ -66,11 +67,11 @@ export default {
}; };
}; };
el.addEventListener('selectstart', ev => { src.addEventListener('selectstart', (ev) => {
ev.preventDefault(); ev.preventDefault();
}); });
el.addEventListener(start, (ev) => { src.addEventListener(start, () => {
window.clearTimeout(self.showTimer); window.clearTimeout(self.showTimer);
window.clearTimeout(self.hideTimer); window.clearTimeout(self.hideTimer);
if (delay === 0) { if (delay === 0) {
@ -80,7 +81,7 @@ export default {
} }
}, { passive: true }); }, { passive: true });
el.addEventListener(end, () => { src.addEventListener(end, () => {
window.clearTimeout(self.showTimer); window.clearTimeout(self.showTimer);
window.clearTimeout(self.hideTimer); window.clearTimeout(self.hideTimer);
if (delay === 0) { if (delay === 0) {
@ -90,19 +91,19 @@ export default {
} }
}, { passive: true }); }, { passive: true });
el.addEventListener('click', () => { src.addEventListener('click', () => {
window.clearTimeout(self.showTimer); window.clearTimeout(self.showTimer);
self.close(); self.close();
}); });
}, },
updated(el, binding) { updated(src, binding) {
const self = el._tooltipDirective_; const self = (src as any)._tooltipDirective_;
self.text = binding.value as string; self.text = binding.value as string;
}, },
unmounted(el, binding, vn) { unmounted(src) {
const self = el._tooltipDirective_; const self = (src as any)._tooltipDirective_;
window.clearInterval(self.checkTimer); window.clearInterval(self.checkTimer);
}, },
} as Directive; };

View file

@ -3,10 +3,30 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { defineAsyncComponent, Directive, ref } from 'vue'; import { defineAsyncComponent, ref } from 'vue';
import type { ObjectDirective } from 'vue';
import { popup } from '@/os.js'; import { popup } from '@/os.js';
export class UserPreview { export const vUserPreview: ObjectDirective<HTMLElement, string | null | undefined> = {
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);
},
unmounted(src, binding) {
if (binding.value == null) return;
const self = src._userPreviewDirective_;
self.preview.detach();
},
};
class UserPreview {
private el; private el;
private user; private user;
private showTimer; private showTimer;
@ -102,22 +122,3 @@ export class UserPreview {
this.el.removeEventListener('click', this.onClick); 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;