summaryrefslogtreecommitdiff
path: root/packages/frontend/src/directives
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-12-27 14:36:33 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2022-12-27 14:36:33 +0900
commit9384f5399da39e53855beb8e7f8ded1aa56bf72e (patch)
treece5959571a981b9c4047da3c7b3fd080aa44222c /packages/frontend/src/directives
parentwip: retention for dashboard (diff)
downloadsharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.gz
sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.bz2
sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.zip
rename: client -> frontend
Diffstat (limited to 'packages/frontend/src/directives')
-rw-r--r--packages/frontend/src/directives/adaptive-border.ts24
-rw-r--r--packages/frontend/src/directives/anim.ts18
-rw-r--r--packages/frontend/src/directives/appear.ts22
-rw-r--r--packages/frontend/src/directives/click-anime.ts31
-rw-r--r--packages/frontend/src/directives/follow-append.ts35
-rw-r--r--packages/frontend/src/directives/get-size.ts54
-rw-r--r--packages/frontend/src/directives/hotkey.ts24
-rw-r--r--packages/frontend/src/directives/index.ts28
-rw-r--r--packages/frontend/src/directives/panel.ts24
-rw-r--r--packages/frontend/src/directives/ripple.ts18
-rw-r--r--packages/frontend/src/directives/size.ts123
-rw-r--r--packages/frontend/src/directives/tooltip.ts93
-rw-r--r--packages/frontend/src/directives/user-preview.ts118
13 files changed, 612 insertions, 0 deletions
diff --git a/packages/frontend/src/directives/adaptive-border.ts b/packages/frontend/src/directives/adaptive-border.ts
new file mode 100644
index 0000000000..619c9f0b6d
--- /dev/null
+++ b/packages/frontend/src/directives/adaptive-border.ts
@@ -0,0 +1,24 @@
+import { Directive } from 'vue';
+
+export default {
+ mounted(src, binding, vn) {
+ const getBgColor = (el: HTMLElement) => {
+ const style = window.getComputedStyle(el);
+ if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
+ return style.backgroundColor;
+ } else {
+ return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
+ }
+ };
+
+ const parentBg = getBgColor(src.parentElement);
+
+ const myBg = window.getComputedStyle(src).backgroundColor;
+
+ if (parentBg === myBg) {
+ src.style.borderColor = 'var(--divider)';
+ } else {
+ src.style.borderColor = myBg;
+ }
+ },
+} as Directive;
diff --git a/packages/frontend/src/directives/anim.ts b/packages/frontend/src/directives/anim.ts
new file mode 100644
index 0000000000..04e1c6a404
--- /dev/null
+++ b/packages/frontend/src/directives/anim.ts
@@ -0,0 +1,18 @@
+import { Directive } from 'vue';
+
+export default {
+ beforeMount(src, binding, vn) {
+ src.style.opacity = '0';
+ src.style.transform = 'scale(0.9)';
+ // ページネーションと相性が悪いので
+ //if (typeof binding.value === 'number') src.style.transitionDelay = `${binding.value * 30}ms`;
+ src.classList.add('_zoom');
+ },
+
+ mounted(src, binding, vn) {
+ window.setTimeout(() => {
+ src.style.opacity = '1';
+ src.style.transform = 'none';
+ }, 1);
+ },
+} as Directive;
diff --git a/packages/frontend/src/directives/appear.ts b/packages/frontend/src/directives/appear.ts
new file mode 100644
index 0000000000..7fa43fc34a
--- /dev/null
+++ b/packages/frontend/src/directives/appear.ts
@@ -0,0 +1,22 @@
+import { Directive } from 'vue';
+
+export default {
+ mounted(src, binding, vn) {
+ const fn = binding.value;
+ if (fn == null) return;
+
+ const observer = new IntersectionObserver(entries => {
+ if (entries.some(entry => entry.isIntersecting)) {
+ fn();
+ }
+ });
+
+ observer.observe(src);
+
+ src._observer_ = observer;
+ },
+
+ unmounted(src, binding, vn) {
+ if (src._observer_) src._observer_.disconnect();
+ },
+} as Directive;
diff --git a/packages/frontend/src/directives/click-anime.ts b/packages/frontend/src/directives/click-anime.ts
new file mode 100644
index 0000000000..e2f514b7ca
--- /dev/null
+++ b/packages/frontend/src/directives/click-anime.ts
@@ -0,0 +1,31 @@
+import { Directive } from 'vue';
+import { defaultStore } from '@/store';
+
+export default {
+ mounted(el, binding, vn) {
+ /*
+ if (!defaultStore.state.animation) return;
+
+ el.classList.add('_anime_bounce_standBy');
+
+ el.addEventListener('mousedown', () => {
+ el.classList.add('_anime_bounce_standBy');
+ el.classList.add('_anime_bounce_ready');
+
+ el.addEventListener('mouseleave', () => {
+ el.classList.remove('_anime_bounce_ready');
+ });
+ });
+
+ el.addEventListener('click', () => {
+ el.classList.add('_anime_bounce');
+ });
+
+ el.addEventListener('animationend', () => {
+ el.classList.remove('_anime_bounce_ready');
+ el.classList.remove('_anime_bounce');
+ el.classList.add('_anime_bounce_standBy');
+ });
+ */
+ },
+} as Directive;
diff --git a/packages/frontend/src/directives/follow-append.ts b/packages/frontend/src/directives/follow-append.ts
new file mode 100644
index 0000000000..62e0ac3b94
--- /dev/null
+++ b/packages/frontend/src/directives/follow-append.ts
@@ -0,0 +1,35 @@
+import { Directive } from 'vue';
+import { getScrollContainer, getScrollPosition } from '@/scripts/scroll';
+
+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
new file mode 100644
index 0000000000..ff3bdd78ac
--- /dev/null
+++ b/packages/frontend/src/directives/get-size.ts
@@ -0,0 +1,54 @@
+import { Directive } from 'vue';
+
+const mountings = new Map<Element, {
+ resize: ResizeObserver;
+ intersection?: IntersectionObserver;
+ fn: (w: number, h: number) => void;
+}>();
+
+function calc(src: Element) {
+ const info = mountings.get(src);
+ const height = src.clientHeight;
+ const width = src.clientWidth;
+
+ if (!info) return;
+
+ // アクティベート前などでsrcが描画されていない場合
+ if (!height) {
+ // IntersectionObserverで表示検出する
+ if (!info.intersection) {
+ info.intersection = new IntersectionObserver(entries => {
+ if (entries.some(entry => entry.isIntersecting)) calc(src);
+ });
+ }
+ info.intersection.observe(src);
+ return;
+ }
+ if (info.intersection) {
+ info.intersection.disconnect();
+ delete info.intersection;
+ }
+
+ 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>;
diff --git a/packages/frontend/src/directives/hotkey.ts b/packages/frontend/src/directives/hotkey.ts
new file mode 100644
index 0000000000..dfc5f646a4
--- /dev/null
+++ b/packages/frontend/src/directives/hotkey.ts
@@ -0,0 +1,24 @@
+import { Directive } from 'vue';
+import { makeHotkey } from '../scripts/hotkey';
+
+export default {
+ mounted(el, binding) {
+ el._hotkey_global = binding.modifiers.global === true;
+
+ el._keyHandler = makeHotkey(binding.value);
+
+ if (el._hotkey_global) {
+ document.addEventListener('keydown', el._keyHandler);
+ } else {
+ el.addEventListener('keydown', el._keyHandler);
+ }
+ },
+
+ unmounted(el) {
+ if (el._hotkey_global) {
+ document.removeEventListener('keydown', el._keyHandler);
+ } else {
+ el.removeEventListener('keydown', el._keyHandler);
+ }
+ },
+} as Directive;
diff --git a/packages/frontend/src/directives/index.ts b/packages/frontend/src/directives/index.ts
new file mode 100644
index 0000000000..401a917cba
--- /dev/null
+++ b/packages/frontend/src/directives/index.ts
@@ -0,0 +1,28 @@
+import { App } from 'vue';
+
+import userPreview from './user-preview';
+import size from './size';
+import getSize from './get-size';
+import ripple from './ripple';
+import tooltip from './tooltip';
+import hotkey from './hotkey';
+import appear from './appear';
+import anim from './anim';
+import clickAnime from './click-anime';
+import panel from './panel';
+import adaptiveBorder from './adaptive-border';
+
+export default function(app: App) {
+ app.directive('userPreview', userPreview);
+ app.directive('user-preview', userPreview);
+ app.directive('size', size);
+ app.directive('get-size', getSize);
+ app.directive('ripple', ripple);
+ app.directive('tooltip', tooltip);
+ app.directive('hotkey', hotkey);
+ app.directive('appear', appear);
+ app.directive('anim', anim);
+ app.directive('click-anime', clickAnime);
+ app.directive('panel', panel);
+ app.directive('adaptive-border', adaptiveBorder);
+}
diff --git a/packages/frontend/src/directives/panel.ts b/packages/frontend/src/directives/panel.ts
new file mode 100644
index 0000000000..d31dc41ed4
--- /dev/null
+++ b/packages/frontend/src/directives/panel.ts
@@ -0,0 +1,24 @@
+import { Directive } from 'vue';
+
+export default {
+ mounted(src, binding, vn) {
+ const getBgColor = (el: HTMLElement) => {
+ const style = window.getComputedStyle(el);
+ if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
+ return style.backgroundColor;
+ } else {
+ return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
+ }
+ };
+
+ const parentBg = getBgColor(src.parentElement);
+
+ const myBg = getComputedStyle(document.documentElement).getPropertyValue('--panel');
+
+ if (parentBg === myBg) {
+ src.style.backgroundColor = 'var(--bg)';
+ } else {
+ src.style.backgroundColor = 'var(--panel)';
+ }
+ },
+} as Directive;
diff --git a/packages/frontend/src/directives/ripple.ts b/packages/frontend/src/directives/ripple.ts
new file mode 100644
index 0000000000..d32f7ab441
--- /dev/null
+++ b/packages/frontend/src/directives/ripple.ts
@@ -0,0 +1,18 @@
+import Ripple from '@/components/MkRipple.vue';
+import { popup } from '@/os';
+
+export default {
+ mounted(el, binding, vn) {
+ // 明示的に false であればバインドしない
+ if (binding.value === false) return;
+
+ el.addEventListener('click', () => {
+ const rect = el.getBoundingClientRect();
+
+ const x = rect.left + (el.offsetWidth / 2);
+ const y = rect.top + (el.offsetHeight / 2);
+
+ popup(Ripple, { x, y }, {}, 'end');
+ });
+ },
+};
diff --git a/packages/frontend/src/directives/size.ts b/packages/frontend/src/directives/size.ts
new file mode 100644
index 0000000000..da8bd78ea1
--- /dev/null
+++ b/packages/frontend/src/directives/size.ts
@@ -0,0 +1,123 @@
+import { Directive } from 'vue';
+
+type Value = { max?: number[]; min?: number[]; };
+
+//const observers = new Map<Element, ResizeObserver>();
+const mountings = new Map<Element, {
+ value: Value;
+ resize: ResizeObserver;
+ intersection?: IntersectionObserver;
+ previousWidth: number;
+ twoPreviousWidth: number;
+}>();
+
+type ClassOrder = {
+ add: string[];
+ remove: string[];
+};
+
+const isContainerQueriesSupported = ('container' in document.documentElement.style);
+
+const cache = new Map<string, ClassOrder>();
+
+function getClassOrder(width: number, queue: Value): ClassOrder {
+ const getMaxClass = (v: number) => `max-width_${v}px`;
+ const getMinClass = (v: number) => `min-width_${v}px`;
+
+ return {
+ add: [
+ ...(queue.max ? queue.max.filter(v => width <= v).map(getMaxClass) : []),
+ ...(queue.min ? queue.min.filter(v => width >= v).map(getMinClass) : []),
+ ],
+ remove: [
+ ...(queue.max ? queue.max.filter(v => width > v).map(getMaxClass) : []),
+ ...(queue.min ? queue.min.filter(v => width < v).map(getMinClass) : []),
+ ],
+ };
+}
+
+function applyClassOrder(el: Element, order: ClassOrder) {
+ el.classList.add(...order.add);
+ el.classList.remove(...order.remove);
+}
+
+function getOrderName(width: number, queue: Value): string {
+ return `${width}|${queue.max ? queue.max.join(',') : ''}|${queue.min ? queue.min.join(',') : ''}`;
+}
+
+function calc(el: Element) {
+ const info = mountings.get(el);
+ const width = el.clientWidth;
+
+ if (!info || info.previousWidth === width) return;
+
+ // アクティベート前などでsrcが描画されていない場合
+ if (!width) {
+ // IntersectionObserverで表示検出する
+ if (!info.intersection) {
+ info.intersection = new IntersectionObserver(entries => {
+ if (entries.some(entry => entry.isIntersecting)) calc(el);
+ });
+ }
+ info.intersection.observe(el);
+ return;
+ }
+ if (info.intersection) {
+ info.intersection.disconnect();
+ delete info.intersection;
+ }
+
+ mountings.set(el, { ...info, ...{ previousWidth: width, twoPreviousWidth: info.previousWidth }});
+
+ // Prevent infinite resizing
+ // https://github.com/misskey-dev/misskey/issues/9076
+ if (info.twoPreviousWidth === width) {
+ return;
+ }
+
+ const cached = cache.get(getOrderName(width, info.value));
+ if (cached) {
+ applyClassOrder(el, cached);
+ } else {
+ const order = getClassOrder(width, info.value);
+ cache.set(getOrderName(width, info.value), order);
+ applyClassOrder(el, order);
+ }
+}
+
+export default {
+ mounted(src, binding, vn) {
+ if (isContainerQueriesSupported) return;
+
+ const resize = new ResizeObserver((entries, observer) => {
+ calc(src);
+ });
+
+ mountings.set(src, {
+ value: binding.value,
+ resize,
+ previousWidth: 0,
+ twoPreviousWidth: 0,
+ });
+
+ calc(src);
+ resize.observe(src);
+ },
+
+ updated(src, binding, vn) {
+ if (isContainerQueriesSupported) return;
+
+ mountings.set(src, Object.assign({}, mountings.get(src), { value: binding.value }));
+ calc(src);
+ },
+
+ unmounted(src, binding, vn) {
+ if (isContainerQueriesSupported) return;
+
+ const info = mountings.get(src);
+ if (!info) return;
+ info.resize.disconnect();
+ if (info.intersection) info.intersection.disconnect();
+ mountings.delete(src);
+ },
+} as Directive<Element, Value>;
diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts
new file mode 100644
index 0000000000..5d13497b5f
--- /dev/null
+++ b/packages/frontend/src/directives/tooltip.ts
@@ -0,0 +1,93 @@
+// TODO: useTooltip関数使うようにしたい
+// ただディレクティブ内でonUnmountedなどのcomposition api使えるのか不明
+
+import { defineAsyncComponent, Directive, ref } from 'vue';
+import { isTouchUsing } from '@/scripts/touch';
+import { popup, alert } from '@/os';
+
+const start = isTouchUsing ? 'touchstart' : 'mouseover';
+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;
+
+ self.text = binding.value as string;
+ self._close = null;
+ self.showTimer = null;
+ self.hideTimer = null;
+ self.checkTimer = null;
+
+ self.close = () => {
+ if (self._close) {
+ window.clearInterval(self.checkTimer);
+ self._close();
+ self._close = null;
+ }
+ };
+
+ if (binding.arg === 'dialog') {
+ el.addEventListener('click', (ev) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ alert({
+ type: 'info',
+ text: binding.value,
+ });
+ return false;
+ });
+ }
+
+ self.show = () => {
+ if (!document.body.contains(el)) return;
+ if (self._close) return;
+ if (self.text == null) return;
+
+ const showing = ref(true);
+ popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), {
+ showing,
+ 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,
+ }, {}, 'closed');
+
+ self._close = () => {
+ showing.value = false;
+ };
+ };
+
+ el.addEventListener('selectstart', ev => {
+ ev.preventDefault();
+ });
+
+ el.addEventListener(start, () => {
+ window.clearTimeout(self.showTimer);
+ window.clearTimeout(self.hideTimer);
+ self.showTimer = window.setTimeout(self.show, delay);
+ }, { passive: true });
+
+ el.addEventListener(end, () => {
+ window.clearTimeout(self.showTimer);
+ window.clearTimeout(self.hideTimer);
+ self.hideTimer = window.setTimeout(self.close, delay);
+ }, { passive: true });
+
+ el.addEventListener('click', () => {
+ window.clearTimeout(self.showTimer);
+ self.close();
+ });
+ },
+
+ updated(el, binding) {
+ const self = el._tooltipDirective_;
+ self.text = binding.value as string;
+ },
+
+ unmounted(el, binding, vn) {
+ const self = el._tooltipDirective_;
+ window.clearInterval(self.checkTimer);
+ },
+} as Directive;
diff --git a/packages/frontend/src/directives/user-preview.ts b/packages/frontend/src/directives/user-preview.ts
new file mode 100644
index 0000000000..ed5f00ca65
--- /dev/null
+++ b/packages/frontend/src/directives/user-preview.ts
@@ -0,0 +1,118 @@
+import { defineAsyncComponent, Directive, ref } from 'vue';
+import autobind from 'autobind-decorator';
+import { popup } from '@/os';
+
+export class UserPreview {
+ private el;
+ private user;
+ private showTimer;
+ private hideTimer;
+ private checkTimer;
+ private promise;
+
+ constructor(el, user) {
+ this.el = el;
+ this.user = user;
+
+ this.attach();
+ }
+
+ @autobind
+ private show() {
+ if (!document.body.contains(this.el)) return;
+ if (this.promise) return;
+
+ const showing = ref(true);
+
+ popup(defineAsyncComponent(() => import('@/components/MkUserPreview.vue')), {
+ showing,
+ q: this.user,
+ source: this.el,
+ }, {
+ mouseover: () => {
+ window.clearTimeout(this.hideTimer);
+ },
+ mouseleave: () => {
+ window.clearTimeout(this.showTimer);
+ this.hideTimer = window.setTimeout(this.close, 500);
+ },
+ }, 'closed');
+
+ this.promise = {
+ cancel: () => {
+ showing.value = false;
+ },
+ };
+
+ this.checkTimer = window.setInterval(() => {
+ if (!document.body.contains(this.el)) {
+ window.clearTimeout(this.showTimer);
+ window.clearTimeout(this.hideTimer);
+ this.close();
+ }
+ }, 1000);
+ }
+
+ @autobind
+ private close() {
+ if (this.promise) {
+ window.clearInterval(this.checkTimer);
+ this.promise.cancel();
+ this.promise = null;
+ }
+ }
+
+ @autobind
+ private onMouseover() {
+ window.clearTimeout(this.showTimer);
+ window.clearTimeout(this.hideTimer);
+ this.showTimer = window.setTimeout(this.show, 500);
+ }
+
+ @autobind
+ private onMouseleave() {
+ window.clearTimeout(this.showTimer);
+ window.clearTimeout(this.hideTimer);
+ this.hideTimer = window.setTimeout(this.close, 500);
+ }
+
+ @autobind
+ private onClick() {
+ window.clearTimeout(this.showTimer);
+ this.close();
+ }
+
+ @autobind
+ public attach() {
+ this.el.addEventListener('mouseover', this.onMouseover);
+ this.el.addEventListener('mouseleave', this.onMouseleave);
+ this.el.addEventListener('click', this.onClick);
+ }
+
+ @autobind
+ public detach() {
+ this.el.removeEventListener('mouseover', this.onMouseover);
+ this.el.removeEventListener('mouseleave', this.onMouseleave);
+ this.el.removeEventListener('click', this.onClick);
+ window.clearInterval(this.checkTimer);
+ }
+}
+
+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;