18
packages/client/src/directives/anim.ts
Normal file
18
packages/client/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) {
|
||||
setTimeout(() => {
|
||||
src.style.opacity = '1';
|
||||
src.style.transform = 'none';
|
||||
}, 1);
|
||||
},
|
||||
} as Directive;
|
22
packages/client/src/directives/appear.ts
Normal file
22
packages/client/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;
|
29
packages/client/src/directives/click-anime.ts
Normal file
29
packages/client/src/directives/click-anime.ts
Normal file
@ -0,0 +1,29 @@
|
||||
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/client/src/directives/follow-append.ts
Normal file
35
packages/client/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;
|
34
packages/client/src/directives/get-size.ts
Normal file
34
packages/client/src/directives/get-size.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Directive } from 'vue';
|
||||
|
||||
export default {
|
||||
mounted(src, binding, vn) {
|
||||
const calc = () => {
|
||||
const height = src.clientHeight;
|
||||
const width = src.clientWidth;
|
||||
|
||||
// 要素が(一時的に)DOMに存在しないときは計算スキップ
|
||||
if (height === 0) return;
|
||||
|
||||
binding.value(width, height);
|
||||
};
|
||||
|
||||
calc();
|
||||
|
||||
// Vue3では使えなくなった
|
||||
// 無くても大丈夫か...?
|
||||
// TODO: ↑大丈夫じゃなかったので解決策を探す
|
||||
//vn.context.$on('hook:activated', calc);
|
||||
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
calc();
|
||||
});
|
||||
ro.observe(src);
|
||||
|
||||
src._get_size_ro_ = ro;
|
||||
},
|
||||
|
||||
unmounted(src, binding, vn) {
|
||||
binding.value(0, 0);
|
||||
src._get_size_ro_.unobserve(src);
|
||||
}
|
||||
} as Directive;
|
24
packages/client/src/directives/hotkey.ts
Normal file
24
packages/client/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;
|
26
packages/client/src/directives/index.ts
Normal file
26
packages/client/src/directives/index.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { App } from 'vue';
|
||||
|
||||
import userPreview from './user-preview';
|
||||
import size from './size';
|
||||
import getSize from './get-size';
|
||||
import particle from './particle';
|
||||
import tooltip from './tooltip';
|
||||
import hotkey from './hotkey';
|
||||
import appear from './appear';
|
||||
import anim from './anim';
|
||||
import stickyContainer from './sticky-container';
|
||||
import clickAnime from './click-anime';
|
||||
|
||||
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('particle', particle);
|
||||
app.directive('tooltip', tooltip);
|
||||
app.directive('hotkey', hotkey);
|
||||
app.directive('appear', appear);
|
||||
app.directive('anim', anim);
|
||||
app.directive('click-anime', clickAnime);
|
||||
app.directive('sticky-container', stickyContainer);
|
||||
}
|
18
packages/client/src/directives/particle.ts
Normal file
18
packages/client/src/directives/particle.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import Particle from '@/components/particle.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.clientWidth / 2);
|
||||
const y = rect.top + (el.clientHeight / 2);
|
||||
|
||||
popup(Particle, { x, y }, {}, 'end');
|
||||
});
|
||||
}
|
||||
};
|
68
packages/client/src/directives/size.ts
Normal file
68
packages/client/src/directives/size.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { Directive } from 'vue';
|
||||
|
||||
//const observers = new Map<Element, ResizeObserver>();
|
||||
|
||||
export default {
|
||||
mounted(src, binding, vn) {
|
||||
const query = binding.value;
|
||||
|
||||
const addClass = (el: Element, cls: string) => {
|
||||
el.classList.add(cls);
|
||||
};
|
||||
|
||||
const removeClass = (el: Element, cls: string) => {
|
||||
el.classList.remove(cls);
|
||||
};
|
||||
|
||||
const calc = () => {
|
||||
const width = src.clientWidth;
|
||||
|
||||
// 要素が(一時的に)DOMに存在しないときは計算スキップ
|
||||
if (width === 0) return;
|
||||
|
||||
if (query.max) {
|
||||
for (const v of query.max) {
|
||||
if (width <= v) {
|
||||
addClass(src, 'max-width_' + v + 'px');
|
||||
} else {
|
||||
removeClass(src, 'max-width_' + v + 'px');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (query.min) {
|
||||
for (const v of query.min) {
|
||||
if (width >= v) {
|
||||
addClass(src, 'min-width_' + v + 'px');
|
||||
} else {
|
||||
removeClass(src, 'min-width_' + v + 'px');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
calc();
|
||||
|
||||
window.addEventListener('resize', calc);
|
||||
|
||||
// Vue3では使えなくなった
|
||||
// 無くても大丈夫か...?
|
||||
// TODO: ↑大丈夫じゃなかったので解決策を探す
|
||||
//vn.context.$on('hook:activated', calc);
|
||||
|
||||
//const ro = new ResizeObserver((entries, observer) => {
|
||||
// calc();
|
||||
//});
|
||||
|
||||
//ro.observe(el);
|
||||
|
||||
// TODO: 新たにプロパティを作るのをやめMapを使う
|
||||
// ただメモリ的には↓の方が省メモリかもしれないので検討中
|
||||
//el._ro_ = ro;
|
||||
src._calc_ = calc;
|
||||
},
|
||||
|
||||
unmounted(src, binding, vn) {
|
||||
//el._ro_.unobserve(el);
|
||||
window.removeEventListener('resize', src._calc_);
|
||||
}
|
||||
} as Directive;
|
15
packages/client/src/directives/sticky-container.ts
Normal file
15
packages/client/src/directives/sticky-container.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Directive } from 'vue';
|
||||
|
||||
export default {
|
||||
mounted(src, binding, vn) {
|
||||
//const query = binding.value;
|
||||
|
||||
const header = src.children[0];
|
||||
const currentStickyTop = getComputedStyle(src).getPropertyValue('--stickyTop') || '0px';
|
||||
src.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
|
||||
header.style.setProperty('--stickyTop', currentStickyTop);
|
||||
header.style.position = 'sticky';
|
||||
header.style.top = 'var(--stickyTop)';
|
||||
header.style.zIndex = '1';
|
||||
},
|
||||
} as Directive;
|
87
packages/client/src/directives/tooltip.ts
Normal file
87
packages/client/src/directives/tooltip.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { Directive, ref } from 'vue';
|
||||
import { isDeviceTouch } from '@/scripts/is-device-touch';
|
||||
import { popup, dialog } from '@/os';
|
||||
|
||||
const start = isDeviceTouch ? 'touchstart' : 'mouseover';
|
||||
const end = isDeviceTouch ? 'touchend' : 'mouseleave';
|
||||
const delay = 100;
|
||||
|
||||
export default {
|
||||
mounted(el: HTMLElement, binding, vn) {
|
||||
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) {
|
||||
clearInterval(self.checkTimer);
|
||||
self._close();
|
||||
self._close = null;
|
||||
}
|
||||
};
|
||||
|
||||
if (binding.arg === 'dialog') {
|
||||
el.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
dialog({
|
||||
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(import('@/components/ui/tooltip.vue'), {
|
||||
showing,
|
||||
text: self.text,
|
||||
source: el
|
||||
}, {}, 'closed');
|
||||
|
||||
self._close = () => {
|
||||
showing.value = false;
|
||||
};
|
||||
};
|
||||
|
||||
el.addEventListener('selectstart', e => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
el.addEventListener(start, () => {
|
||||
clearTimeout(self.showTimer);
|
||||
clearTimeout(self.hideTimer);
|
||||
self.showTimer = setTimeout(self.show, delay);
|
||||
}, { passive: true });
|
||||
|
||||
el.addEventListener(end, () => {
|
||||
clearTimeout(self.showTimer);
|
||||
clearTimeout(self.hideTimer);
|
||||
self.hideTimer = setTimeout(self.close, delay);
|
||||
}, { passive: true });
|
||||
|
||||
el.addEventListener('click', () => {
|
||||
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_;
|
||||
clearInterval(self.checkTimer);
|
||||
},
|
||||
} as Directive;
|
118
packages/client/src/directives/user-preview.ts
Normal file
118
packages/client/src/directives/user-preview.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { 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(import('@/components/user-preview.vue'), {
|
||||
showing,
|
||||
q: this.user,
|
||||
source: this.el
|
||||
}, {
|
||||
mouseover: () => {
|
||||
clearTimeout(this.hideTimer);
|
||||
},
|
||||
mouseleave: () => {
|
||||
clearTimeout(this.showTimer);
|
||||
this.hideTimer = setTimeout(this.close, 500);
|
||||
},
|
||||
}, 'closed');
|
||||
|
||||
this.promise = {
|
||||
cancel: () => {
|
||||
showing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.checkTimer = setInterval(() => {
|
||||
if (!document.body.contains(this.el)) {
|
||||
clearTimeout(this.showTimer);
|
||||
clearTimeout(this.hideTimer);
|
||||
this.close();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
@autobind
|
||||
private close() {
|
||||
if (this.promise) {
|
||||
clearInterval(this.checkTimer);
|
||||
this.promise.cancel();
|
||||
this.promise = null;
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
private onMouseover() {
|
||||
clearTimeout(this.showTimer);
|
||||
clearTimeout(this.hideTimer);
|
||||
this.showTimer = setTimeout(this.show, 500);
|
||||
}
|
||||
|
||||
@autobind
|
||||
private onMouseleave() {
|
||||
clearTimeout(this.showTimer);
|
||||
clearTimeout(this.hideTimer);
|
||||
this.hideTimer = setTimeout(this.close, 500);
|
||||
}
|
||||
|
||||
@autobind
|
||||
private onClick() {
|
||||
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);
|
||||
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