Migrate to Vue3 (#6587)
* Update reaction.vue
* fix bug
* wip
* wip
* wjio
* wip
* Revert "wip"
This reverts commit e427f2160a
.
* wip
* wip
* wip
* Update init.ts
* Update drive-window.vue
* wip
* wip
* Use PascalCase for components
* Use PascalCase for components
* update dep
* wip
* wip
* wip
* Update init.ts
* wip
* Update paging.ts
* Update test.vue
* watch deep
* wip
* lint
* wip
* wip
* wip
* wip
* wiop
* wip
* Update webpack.config.ts
* alllow null poll
* wip
* wip
* wip
* wiop
* UI redesign & refactor (#6714)
* wip
* wip
* wip
* wip
* wip
* Update drive.vue
* Update word-mute.vue
* wip
* wip
* wip
* clean up
* wip
* Update default.vue
* wip
* Update notes.vue
* Update mfm.ts
* Update index.home.vue
* Update post-form.vue
* Update post-form-attaches.vue
* wip
* Update post-form.vue
* Update sidebar.vue
* wip
* wip
* Update index.vue
* wip
* Update default.vue
* Update index.vue
* Update index.vue
* wip
* Update post-form-attaches.vue
* Update note.vue
* wip
* clean up
* Update notes.vue
* wip
* wip
* Update ja-JP.yml
* wip
* wip
* Update index.vue
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update default.vue
* wip
* Update _dark.json5
* wip
* wip
* wip
* clean up
* wip
* wip
* Update index.vue
* Update test.vue
* wip
* wip
* fix
* wip
* wip
* wip
* wip
* clena yop
* wip
* wip
* Update store.ts
* Update messaging-room.vue
* Update default.widgets.vue
* fix
* wip
* wip
* Update modal.vue
* wip
* Update os.ts
* Update os.ts
* Update deck.vue
* Update init.ts
* wip
* Update ja-JP.yml
* v-sizeは単にwindowのresizeを監視するだけで良いかもしれない
* Update modal.vue
* wip
* Update tooltip.ts
* wip
* wip
* wip
* wip
* wip
* Update image-viewer.vue
* wip
* wip
* Update style.scss
* Update style.scss
* Update visitor.vue
* wip
* Update init.ts
* Update init.ts
* wip
* wip
* Update visitor.vue
* Update visitor.vue
* Update visitor.vue
* Update visitor.vue
* wip
* wip
* Update modal.vue
* Update header.vue
* Update menu.vue
* Update about.vue
* Update about-misskey.vue
* wip
* wip
* Update visitor.vue
* Update tooltip.ts
* wip
* Update drive.vue
* wip
* Update style.scss
* Update header.vue
* wip
* wip
* Update users.user.vue
* Update announcements.vue
* wip
* wip
* wip
* Update emojis.vue
* wip
* Update emojis.vue
* Update style.scss
* Update users.vue
* wip
* Update style.scss
* wip
* Update welcome.entrance.vue
* Update radio.vue
* Update size.ts
* Update emoji-edit-dialog.vue
* wip
* Update emojis.vue
* wip
* Update emojis.vue
* Update emojis.vue
* Update emojis.vue
* wip
* wip
* wip
* wip
* Update file-dialog.vue
* wip
* wip
* Update token-generate-window.vue
* Update notification-setting-window.vue
* wip
* wip
* Update _error_.vue
* Update ja-JP.yml
* wip
* wip
* Update store.ts
* Update emojis.vue
* Update emojis.vue
* Update emojis.vue
* Update announcements.vue
* Update store.ts
* wip
* Update page-editor.vue
* wip
* wip
* Update modal.vue
* wip
* Update select-file.ts
* Update timeline.vue
* Update emojis.vue
* Update os.ts
* wip
* Update user-select.vue
* Update mfm.ts
* Update get-file-info.ts
* Update drive.vue
* Update init.ts
* Update mfm.ts
* wip
* wip
* Update window.vue
* Update note.vue
* wip
* wip
* Update user-info.vue
* wip
* wip
* wip
* wip
* wip
* Update header.vue
* Update header.vue
* wip
* Update explore.vue
* wip
* wip
* wip
* Update webpack.config.ts
* wip
* wip
* wip
* wip
* wip
* wip
* Update autocomplete.ts
* wip
* wip
* wip
* Update toast.vue
* wip
* Update post-form-dialog.vue
* wip
* wip
* wip
* wip
* wip
* Update users.vue
* wip
* Update explore.vue
* wip
* wip
* wip
* Update package.json
* wip
* Update icon-dialog.vue
* wip
* wip
* Update user-preview.ts
* wip
* wip
* wip
* wip
* wip
* Update instance.vue
* Update user-name.vue
* Update federation.vue
* Update instance.vue
* wip
* wip
* Update tag.vue
* wip
* wip
* wip
* wip
* wip
* Update instance.vue
* wip
* Update os.ts
* Update os.ts
* wip
* wip
* wip
* Update router.ts
* wip
* Update init.ts
* Update note.vue
* Update messages.vue
* wip
* wip
* wip
* wip
* wip
* google
* wip
* wip
* wip
* wip
* Update theme-editor.vue
* wip
* wip
* Update room.vue
* Update channel-editor.vue
* wip
* Update window.vue
* Update window.vue
* wip
* Update window.vue
* Update window.vue
* wip
* Update menu.vue
* wip
* wip
* wip
* wip
* Update messaging-room.vue
* wip
* Update post-form.vue
* Update default.widgets.vue
* Update window.vue
* wip
This commit is contained in:
22
src/client/directives/appear.ts
Normal file
22
src/client/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;
|
@@ -1,252 +0,0 @@
|
||||
import * as getCaretCoordinates from 'textarea-caret';
|
||||
import { toASCII } from 'punycode';
|
||||
|
||||
export default {
|
||||
bind(el, binding, vn) {
|
||||
const self = el._autoCompleteDirective_ = {} as any;
|
||||
self.x = new Autocomplete(el, vn.context, binding.value);
|
||||
self.x.attach();
|
||||
},
|
||||
|
||||
unbind(el, binding, vn) {
|
||||
const self = el._autoCompleteDirective_;
|
||||
self.x.detach();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* オートコンプリートを管理するクラス。
|
||||
*/
|
||||
class Autocomplete {
|
||||
private suggestion: any;
|
||||
private textarea: any;
|
||||
private vm: any;
|
||||
private currentType: string;
|
||||
private opts: {
|
||||
model: string;
|
||||
};
|
||||
private opening: boolean;
|
||||
|
||||
private get text(): string {
|
||||
return this.vm[this.opts.model];
|
||||
}
|
||||
|
||||
private set text(text: string) {
|
||||
this.vm[this.opts.model] = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 対象のテキストエリアを与えてインスタンスを初期化します。
|
||||
*/
|
||||
constructor(textarea, vm, opts) {
|
||||
//#region BIND
|
||||
this.onInput = this.onInput.bind(this);
|
||||
this.complete = this.complete.bind(this);
|
||||
this.close = this.close.bind(this);
|
||||
//#endregion
|
||||
|
||||
this.suggestion = null;
|
||||
this.textarea = textarea;
|
||||
this.vm = vm;
|
||||
this.opts = opts;
|
||||
this.opening = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* このインスタンスにあるテキストエリアの入力のキャプチャを開始します。
|
||||
*/
|
||||
public attach() {
|
||||
this.textarea.addEventListener('input', this.onInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* このインスタンスにあるテキストエリアの入力のキャプチャを解除します。
|
||||
*/
|
||||
public detach() {
|
||||
this.textarea.removeEventListener('input', this.onInput);
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* テキスト入力時
|
||||
*/
|
||||
private onInput() {
|
||||
const caretPos = this.textarea.selectionStart;
|
||||
const text = this.text.substr(0, caretPos).split('\n').pop();
|
||||
|
||||
const mentionIndex = text.lastIndexOf('@');
|
||||
const hashtagIndex = text.lastIndexOf('#');
|
||||
const emojiIndex = text.lastIndexOf(':');
|
||||
|
||||
const max = Math.max(
|
||||
mentionIndex,
|
||||
hashtagIndex,
|
||||
emojiIndex);
|
||||
|
||||
if (max == -1) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const isMention = mentionIndex != -1;
|
||||
const isHashtag = hashtagIndex != -1;
|
||||
const isEmoji = emojiIndex != -1;
|
||||
|
||||
let opened = false;
|
||||
|
||||
if (isMention) {
|
||||
const username = text.substr(mentionIndex + 1);
|
||||
if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) {
|
||||
this.open('user', username);
|
||||
opened = true;
|
||||
} else if (username === '') {
|
||||
this.open('user', null);
|
||||
opened = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHashtag && !opened) {
|
||||
const hashtag = text.substr(hashtagIndex + 1);
|
||||
if (!hashtag.includes(' ')) {
|
||||
this.open('hashtag', hashtag);
|
||||
opened = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmoji && !opened) {
|
||||
const emoji = text.substr(emojiIndex + 1);
|
||||
if (!emoji.includes(' ')) {
|
||||
this.open('emoji', emoji);
|
||||
opened = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!opened) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* サジェストを提示します。
|
||||
*/
|
||||
private async open(type: string, q: string) {
|
||||
if (type != this.currentType) {
|
||||
this.close();
|
||||
}
|
||||
if (this.opening) return;
|
||||
this.opening = true;
|
||||
this.currentType = type;
|
||||
|
||||
//#region サジェストを表示すべき位置を計算
|
||||
const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
|
||||
|
||||
const rect = this.textarea.getBoundingClientRect();
|
||||
|
||||
const x = rect.left + caretPosition.left - this.textarea.scrollLeft;
|
||||
const y = rect.top + caretPosition.top - this.textarea.scrollTop;
|
||||
//#endregion
|
||||
|
||||
if (this.suggestion) {
|
||||
// TODO: Vueの警告が出るのでなんとかする
|
||||
this.suggestion.x = x;
|
||||
this.suggestion.y = y;
|
||||
this.suggestion.q = q;
|
||||
|
||||
this.opening = false;
|
||||
} else {
|
||||
const MkAutocomplete = await import('../components/autocomplete.vue').then(m => m.default);
|
||||
|
||||
// サジェスト要素作成
|
||||
this.suggestion = new MkAutocomplete({
|
||||
parent: this.vm,
|
||||
propsData: {
|
||||
textarea: this.textarea,
|
||||
complete: this.complete,
|
||||
close: this.close,
|
||||
type: type,
|
||||
q: q,
|
||||
x,
|
||||
y
|
||||
}
|
||||
}).$mount();
|
||||
|
||||
// 要素追加
|
||||
document.body.appendChild(this.suggestion.$el);
|
||||
|
||||
this.opening = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* サジェストを閉じます。
|
||||
*/
|
||||
private close() {
|
||||
if (this.suggestion == null) return;
|
||||
|
||||
this.suggestion.destroyDom();
|
||||
this.suggestion = null;
|
||||
|
||||
this.textarea.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* オートコンプリートする
|
||||
*/
|
||||
private complete(type, value) {
|
||||
this.close();
|
||||
|
||||
const caret = this.textarea.selectionStart;
|
||||
|
||||
if (type == 'user') {
|
||||
const source = this.text;
|
||||
|
||||
const before = source.substr(0, caret);
|
||||
const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
|
||||
const after = source.substr(caret);
|
||||
|
||||
const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
|
||||
|
||||
// 挿入
|
||||
this.text = `${trimmedBefore}@${acct} ${after}`;
|
||||
|
||||
// キャレットを戻す
|
||||
this.vm.$nextTick(() => {
|
||||
this.textarea.focus();
|
||||
const pos = trimmedBefore.length + (acct.length + 2);
|
||||
this.textarea.setSelectionRange(pos, pos);
|
||||
});
|
||||
} else if (type == 'hashtag') {
|
||||
const source = this.text;
|
||||
|
||||
const before = source.substr(0, caret);
|
||||
const trimmedBefore = before.substring(0, before.lastIndexOf('#'));
|
||||
const after = source.substr(caret);
|
||||
|
||||
// 挿入
|
||||
this.text = `${trimmedBefore}#${value} ${after}`;
|
||||
|
||||
// キャレットを戻す
|
||||
this.vm.$nextTick(() => {
|
||||
this.textarea.focus();
|
||||
const pos = trimmedBefore.length + (value.length + 2);
|
||||
this.textarea.setSelectionRange(pos, pos);
|
||||
});
|
||||
} else if (type == 'emoji') {
|
||||
const source = this.text;
|
||||
|
||||
const before = source.substr(0, caret);
|
||||
const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
|
||||
const after = source.substr(caret);
|
||||
|
||||
// 挿入
|
||||
this.text = trimmedBefore + value + after;
|
||||
|
||||
// キャレットを戻す
|
||||
this.vm.$nextTick(() => {
|
||||
this.textarea.focus();
|
||||
const pos = trimmedBefore.length + value.length;
|
||||
this.textarea.setSelectionRange(pos, pos);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
113
src/client/directives/hotkey.ts
Normal file
113
src/client/directives/hotkey.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Directive } from 'vue';
|
||||
import keyCode from '../scripts/keycode';
|
||||
import { concat } from '../../prelude/array';
|
||||
|
||||
type pattern = {
|
||||
which: string[];
|
||||
ctrl?: boolean;
|
||||
shift?: boolean;
|
||||
alt?: boolean;
|
||||
};
|
||||
|
||||
type action = {
|
||||
patterns: pattern[];
|
||||
|
||||
callback: Function;
|
||||
|
||||
allowRepeat: boolean;
|
||||
};
|
||||
|
||||
const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => {
|
||||
const result = {
|
||||
patterns: [],
|
||||
callback: callback,
|
||||
allowRepeat: true
|
||||
} as action;
|
||||
|
||||
if (patterns.match(/^\(.*\)$/) !== null) {
|
||||
result.allowRepeat = false;
|
||||
patterns = patterns.slice(1, -1);
|
||||
}
|
||||
|
||||
result.patterns = patterns.split('|').map(part => {
|
||||
const pattern = {
|
||||
which: [],
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false
|
||||
} as pattern;
|
||||
|
||||
const keys = part.trim().split('+').map(x => x.trim().toLowerCase());
|
||||
for (const key of keys) {
|
||||
switch (key) {
|
||||
case 'ctrl': pattern.ctrl = true; break;
|
||||
case 'alt': pattern.alt = true; break;
|
||||
case 'shift': pattern.shift = true; break;
|
||||
default: pattern.which = keyCode(key).map(k => k.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
return pattern;
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const ignoreElemens = ['input', 'textarea'];
|
||||
|
||||
function match(e: KeyboardEvent, patterns: action['patterns']): boolean {
|
||||
const key = e.code.toLowerCase();
|
||||
return patterns.some(pattern => pattern.which.includes(key) &&
|
||||
pattern.ctrl === e.ctrlKey &&
|
||||
pattern.shift === e.shiftKey &&
|
||||
pattern.alt === e.altKey &&
|
||||
!e.metaKey
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
mounted(el, binding) {
|
||||
el._hotkey_global = binding.modifiers.global === true;
|
||||
|
||||
const actions = getKeyMap(binding.value);
|
||||
|
||||
// flatten
|
||||
const reservedKeys = concat(actions.map(a => a.patterns));
|
||||
|
||||
el._misskey_reservedKeys = reservedKeys;
|
||||
|
||||
el._keyHandler = (e: KeyboardEvent) => {
|
||||
const targetReservedKeys = document.activeElement ? ((document.activeElement as any)._misskey_reservedKeys || []) : [];
|
||||
if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
|
||||
if (document.activeElement && document.activeElement.attributes['contenteditable']) return;
|
||||
|
||||
for (const action of actions) {
|
||||
const matched = match(e, action.patterns);
|
||||
|
||||
if (matched) {
|
||||
if (!action.allowRepeat && e.repeat) return;
|
||||
if (el._hotkey_global && match(e, targetReservedKeys)) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
action.callback(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
@@ -1,14 +1,18 @@
|
||||
import Vue from 'vue';
|
||||
import { App } from 'vue';
|
||||
|
||||
import userPreview from './user-preview';
|
||||
import autocomplete from './autocomplete';
|
||||
import size from './size';
|
||||
import particle from './particle';
|
||||
import tooltip from './tooltip';
|
||||
import hotkey from './hotkey';
|
||||
import appear from './appear';
|
||||
|
||||
Vue.directive('autocomplete', autocomplete);
|
||||
Vue.directive('userPreview', userPreview);
|
||||
Vue.directive('user-preview', userPreview);
|
||||
Vue.directive('size', size);
|
||||
Vue.directive('particle', particle);
|
||||
Vue.directive('tooltip', tooltip);
|
||||
export default function(app: App) {
|
||||
app.directive('userPreview', userPreview);
|
||||
app.directive('user-preview', userPreview);
|
||||
app.directive('size', size);
|
||||
app.directive('particle', particle);
|
||||
app.directive('tooltip', tooltip);
|
||||
app.directive('hotkey', hotkey);
|
||||
app.directive('appear', appear);
|
||||
}
|
||||
|
@@ -1,24 +1,18 @@
|
||||
import Particle from '../components/particle.vue';
|
||||
import Particle from '@/components/particle.vue';
|
||||
import { popup } from '@/os';
|
||||
|
||||
export default {
|
||||
bind(el, binding, vn) {
|
||||
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);
|
||||
|
||||
const particle = new Particle({
|
||||
parent: vn.context,
|
||||
propsData: {
|
||||
x,
|
||||
y
|
||||
}
|
||||
}).$mount();
|
||||
|
||||
document.body.appendChild(particle.$el);
|
||||
popup(Particle, { x, y }, {}, 'end');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -1,10 +1,10 @@
|
||||
export default {
|
||||
inserted(src, binding, vn) {
|
||||
const query = binding.value;
|
||||
import { Directive } from 'vue';
|
||||
|
||||
// TODO: 要素をもらうというよりはカスタム幅算出関数をもらうようにしてcalcで都度呼び出して計算するようにした方が柔軟そう
|
||||
// その場合はunbindの方も改修することを忘れずに
|
||||
const el = query.el ? query.el() : src;
|
||||
//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);
|
||||
@@ -15,7 +15,10 @@ export default {
|
||||
};
|
||||
|
||||
const calc = () => {
|
||||
const width = el.clientWidth;
|
||||
const width = src.clientWidth;
|
||||
|
||||
// 要素が(一時的に)DOMに存在しないときは計算スキップ
|
||||
if (width === 0) return;
|
||||
|
||||
if (query.max) {
|
||||
for (const v of query.max) {
|
||||
@@ -39,22 +42,27 @@ export default {
|
||||
|
||||
calc();
|
||||
|
||||
vn.context.$on('hook:activated', calc);
|
||||
window.addEventListener('resize', calc);
|
||||
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
calc();
|
||||
});
|
||||
// Vue3では使えなくなった
|
||||
// 無くても大丈夫か...?
|
||||
// TODO: ↑大丈夫じゃなかったので解決策を探す
|
||||
//vn.context.$on('hook:activated', calc);
|
||||
|
||||
ro.observe(el);
|
||||
//const ro = new ResizeObserver((entries, observer) => {
|
||||
// calc();
|
||||
//});
|
||||
|
||||
el._ro_ = ro;
|
||||
//ro.observe(el);
|
||||
|
||||
// TODO: 新たにプロパティを作るのをやめMapを使う
|
||||
// ただメモリ的には↓の方が省メモリかもしれないので検討中
|
||||
//el._ro_ = ro;
|
||||
src._calc_ = calc;
|
||||
},
|
||||
|
||||
unbind(src, binding, vn) {
|
||||
const query = binding.value;
|
||||
|
||||
const el = query.el ? query.el() : src;
|
||||
|
||||
el._ro_.unobserve(el);
|
||||
unmounted(src, binding, vn) {
|
||||
//el._ro_.unobserve(el);
|
||||
window.removeEventListener('resize', src._calc_);
|
||||
}
|
||||
};
|
||||
} as Directive;
|
||||
|
@@ -1,53 +1,60 @@
|
||||
import MkTooltip from '../components/ui/tooltip.vue';
|
||||
import { isDeviceTouch } from '../scripts/is-device-touch';
|
||||
import { Directive, ref } from 'vue';
|
||||
import { isDeviceTouch } from '@/scripts/is-device-touch';
|
||||
import { popup } from '@/os';
|
||||
|
||||
const start = isDeviceTouch ? 'touchstart' : 'mouseover';
|
||||
const end = isDeviceTouch ? 'touchend' : 'mouseleave';
|
||||
|
||||
export default {
|
||||
bind(el: HTMLElement, binding, vn) {
|
||||
mounted(el: HTMLElement, binding, vn) {
|
||||
const self = (el as any)._tooltipDirective_ = {} as any;
|
||||
|
||||
self.text = binding.value as string;
|
||||
self.tag = null;
|
||||
self._close = null;
|
||||
self.showTimer = null;
|
||||
self.hideTimer = null;
|
||||
self.checkTimer = null;
|
||||
|
||||
self.close = () => {
|
||||
if (self.tag) {
|
||||
if (self._close) {
|
||||
clearInterval(self.checkTimer);
|
||||
self.tag.close();
|
||||
self.tag = null;
|
||||
self._close();
|
||||
self._close = null;
|
||||
}
|
||||
};
|
||||
|
||||
const show = e => {
|
||||
const show = async e => {
|
||||
if (!document.body.contains(el)) return;
|
||||
if (self.tag) return;
|
||||
if (self._close) return;
|
||||
if (self.text == null) return;
|
||||
|
||||
self.tag = new MkTooltip({
|
||||
parent: vn.context,
|
||||
propsData: {
|
||||
text: self.text,
|
||||
source: el
|
||||
}
|
||||
}).$mount();
|
||||
const showing = ref(true);
|
||||
popup(await import('@/components/ui/tooltip.vue'), {
|
||||
showing,
|
||||
text: self.text,
|
||||
source: el
|
||||
}, {}, 'closed');
|
||||
|
||||
document.body.appendChild(self.tag.$el);
|
||||
self._close = () => {
|
||||
showing.value = false;
|
||||
};
|
||||
};
|
||||
|
||||
el.addEventListener('selectstart', e => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
el.addEventListener(start, () => {
|
||||
clearTimeout(self.showTimer);
|
||||
clearTimeout(self.hideTimer);
|
||||
self.showTimer = setTimeout(show, 300);
|
||||
});
|
||||
}, { passive: true });
|
||||
|
||||
el.addEventListener(end, () => {
|
||||
clearTimeout(self.showTimer);
|
||||
clearTimeout(self.hideTimer);
|
||||
self.hideTimer = setTimeout(self.close, 300);
|
||||
});
|
||||
}, { passive: true });
|
||||
|
||||
el.addEventListener('click', () => {
|
||||
clearTimeout(self.showTimer);
|
||||
@@ -55,8 +62,8 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
unbind(el, binding, vn) {
|
||||
unmounted(el, binding, vn) {
|
||||
const self = el._tooltipDirective_;
|
||||
clearInterval(self.checkTimer);
|
||||
},
|
||||
};
|
||||
} as Directive;
|
||||
|
@@ -1,75 +1,118 @@
|
||||
import MkUserPreview from '../components/user-preview.vue';
|
||||
import { Directive, ref } from 'vue';
|
||||
import autobind from 'autobind-decorator';
|
||||
import { popup } from '@/os';
|
||||
|
||||
export default {
|
||||
bind(el: HTMLElement, binding, vn) {
|
||||
const self = (el as any)._userPreviewDirective_ = {} as any;
|
||||
export class UserPreview {
|
||||
private el;
|
||||
private user;
|
||||
private showTimer;
|
||||
private hideTimer;
|
||||
private checkTimer;
|
||||
private promise;
|
||||
|
||||
self.user = binding.value;
|
||||
self.tag = null;
|
||||
self.showTimer = null;
|
||||
self.hideTimer = null;
|
||||
self.checkTimer = null;
|
||||
constructor(el, user) {
|
||||
this.el = el;
|
||||
this.user = user;
|
||||
|
||||
self.close = () => {
|
||||
if (self.tag) {
|
||||
clearInterval(self.checkTimer);
|
||||
self.tag.close();
|
||||
self.tag = null;
|
||||
this.attach();
|
||||
}
|
||||
|
||||
@autobind
|
||||
private async show() {
|
||||
if (!document.body.contains(this.el)) return;
|
||||
if (this.promise) return;
|
||||
|
||||
const showing = ref(true);
|
||||
|
||||
popup(await 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;
|
||||
}
|
||||
};
|
||||
|
||||
const show = () => {
|
||||
if (!document.body.contains(el)) return;
|
||||
if (self.tag) return;
|
||||
this.checkTimer = setInterval(() => {
|
||||
if (!document.body.contains(this.el)) {
|
||||
clearTimeout(this.showTimer);
|
||||
clearTimeout(this.hideTimer);
|
||||
this.close();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
self.tag = new MkUserPreview({
|
||||
parent: vn.context,
|
||||
propsData: {
|
||||
user: self.user,
|
||||
source: el
|
||||
}
|
||||
}).$mount();
|
||||
@autobind
|
||||
private close() {
|
||||
if (this.promise) {
|
||||
clearInterval(this.checkTimer);
|
||||
this.promise.cancel();
|
||||
this.promise = null;
|
||||
}
|
||||
}
|
||||
|
||||
self.tag.$on('mouseover', () => {
|
||||
clearTimeout(self.hideTimer);
|
||||
});
|
||||
@autobind
|
||||
private onMouseover() {
|
||||
clearTimeout(this.showTimer);
|
||||
clearTimeout(this.hideTimer);
|
||||
this.showTimer = setTimeout(this.show, 500);
|
||||
}
|
||||
|
||||
self.tag.$on('mouseleave', () => {
|
||||
clearTimeout(self.showTimer);
|
||||
self.hideTimer = setTimeout(self.close, 500);
|
||||
});
|
||||
@autobind
|
||||
private onMouseleave() {
|
||||
clearTimeout(this.showTimer);
|
||||
clearTimeout(this.hideTimer);
|
||||
this.hideTimer = setTimeout(this.close, 500);
|
||||
}
|
||||
|
||||
document.body.appendChild(self.tag.$el);
|
||||
@autobind
|
||||
private onClick() {
|
||||
clearTimeout(this.showTimer);
|
||||
this.close();
|
||||
}
|
||||
|
||||
self.checkTimer = setInterval(() => {
|
||||
if (!document.body.contains(el)) {
|
||||
clearTimeout(self.showTimer);
|
||||
clearTimeout(self.hideTimer);
|
||||
self.close();
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
@autobind
|
||||
public attach() {
|
||||
this.el.addEventListener('mouseover', this.onMouseover);
|
||||
this.el.addEventListener('mouseleave', this.onMouseleave);
|
||||
this.el.addEventListener('click', this.onClick);
|
||||
}
|
||||
|
||||
el.addEventListener('mouseover', () => {
|
||||
clearTimeout(self.showTimer);
|
||||
clearTimeout(self.hideTimer);
|
||||
self.showTimer = setTimeout(show, 500);
|
||||
});
|
||||
@autobind
|
||||
public detach() {
|
||||
this.el.removeEventListener('mouseover', this.onMouseover);
|
||||
this.el.removeEventListener('mouseleave', this.onMouseleave);
|
||||
this.el.removeEventListener('click', this.onClick);
|
||||
clearInterval(this.checkTimer);
|
||||
}
|
||||
}
|
||||
|
||||
el.addEventListener('mouseleave', () => {
|
||||
clearTimeout(self.showTimer);
|
||||
clearTimeout(self.hideTimer);
|
||||
self.hideTimer = setTimeout(self.close, 500);
|
||||
});
|
||||
export default {
|
||||
mounted(el: HTMLElement, binding, vn) {
|
||||
if (binding.value == null) return;
|
||||
|
||||
el.addEventListener('click', () => {
|
||||
clearTimeout(self.showTimer);
|
||||
self.close();
|
||||
});
|
||||
// TODO: 新たにプロパティを作るのをやめMapを使う
|
||||
// ただメモリ的には↓の方が省メモリかもしれないので検討中
|
||||
const self = (el as any)._userPreviewDirective_ = {} as any;
|
||||
|
||||
self.preview = new UserPreview(el, binding.value);
|
||||
},
|
||||
|
||||
unbind(el, binding, vn) {
|
||||
unmounted(el, binding, vn) {
|
||||
if (binding.value == null) return;
|
||||
|
||||
const self = el._userPreviewDirective_;
|
||||
clearInterval(self.checkTimer);
|
||||
self.preview.detach();
|
||||
}
|
||||
};
|
||||
} as Directive;
|
||||
|
Reference in New Issue
Block a user