rename: client -> frontend
This commit is contained in:
24
packages/frontend/src/directives/adaptive-border.ts
Normal file
24
packages/frontend/src/directives/adaptive-border.ts
Normal file
@@ -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;
|
18
packages/frontend/src/directives/anim.ts
Normal file
18
packages/frontend/src/directives/anim.ts
Normal file
@@ -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;
|
22
packages/frontend/src/directives/appear.ts
Normal file
22
packages/frontend/src/directives/appear.ts
Normal file
@@ -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;
|
31
packages/frontend/src/directives/click-anime.ts
Normal file
31
packages/frontend/src/directives/click-anime.ts
Normal file
@@ -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;
|
35
packages/frontend/src/directives/follow-append.ts
Normal file
35
packages/frontend/src/directives/follow-append.ts
Normal file
@@ -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;
|
54
packages/frontend/src/directives/get-size.ts
Normal file
54
packages/frontend/src/directives/get-size.ts
Normal file
@@ -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>;
|
24
packages/frontend/src/directives/hotkey.ts
Normal file
24
packages/frontend/src/directives/hotkey.ts
Normal file
@@ -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;
|
28
packages/frontend/src/directives/index.ts
Normal file
28
packages/frontend/src/directives/index.ts
Normal file
@@ -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);
|
||||
}
|
24
packages/frontend/src/directives/panel.ts
Normal file
24
packages/frontend/src/directives/panel.ts
Normal file
@@ -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;
|
18
packages/frontend/src/directives/ripple.ts
Normal file
18
packages/frontend/src/directives/ripple.ts
Normal file
@@ -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');
|
||||
});
|
||||
},
|
||||
};
|
123
packages/frontend/src/directives/size.ts
Normal file
123
packages/frontend/src/directives/size.ts
Normal file
@@ -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>;
|
93
packages/frontend/src/directives/tooltip.ts
Normal file
93
packages/frontend/src/directives/tooltip.ts
Normal file
@@ -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;
|
118
packages/frontend/src/directives/user-preview.ts
Normal file
118
packages/frontend/src/directives/user-preview.ts
Normal file
@@ -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;
|
Reference in New Issue
Block a user