diff --git a/src/client/app/common/hotkey.ts b/src/client/app/common/hotkey.ts new file mode 100644 index 000000000..10cbeea54 --- /dev/null +++ b/src/client/app/common/hotkey.ts @@ -0,0 +1,79 @@ +import keyCode from './keycode'; + +const getKeyMap = keymap => Object.keys(keymap).map(input => { + const result = {} as any; + + const { keyup, keydown } = keymap[input]; + + input.split('+').forEach(keyName => { + switch (keyName.toLowerCase()) { + case 'ctrl': + case 'alt': + case 'shift': + case 'meta': + result[keyName] = true; + break; + default: + result.keyCode = keyCode(keyName); + } + }); + + result.callback = { + keydown: keydown || keymap[input], + keyup + }; + + return result; +}); + +const ignoreElemens = ['input', 'textarea']; + +export default { + install(Vue) { + Vue.directive('hotkey', { + bind(el, binding) { + el._hotkey_global = binding.modifiers.global === true; + + el._keymap = getKeyMap(binding.value); + + el.dataset.reservedKeyCodes = el._keymap.map(key => `'${key.keyCode}'`).join(' '); + + el._keyHandler = e => { + const reservedKeyCodes = document.activeElement ? ((document.activeElement as any).dataset || {}).reservedKeyCodes || '' : ''; + if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return; + + for (const hotkey of el._keymap) { + if (el._hotkey_global && reservedKeyCodes.includes(`'${e.keyCode}'`)) break; + + const callback = hotkey.keyCode === e.keyCode && + !!hotkey.ctrl === e.ctrlKey && + !!hotkey.alt === e.altKey && + !!hotkey.shift === e.shiftKey && + !!hotkey.meta === e.metaKey && + hotkey.callback[e.type]; + + if (callback) { + e.preventDefault(); + e.stopPropagation(); + callback(e); + } + } + }; + + if (el._hotkey_global) { + document.addEventListener('keydown', el._keyHandler); + } else { + el.addEventListener('keydown', el._keyHandler); + } + }, + + unbind(el) { + if (el._hotkey_global) { + document.removeEventListener('keydown', el._keyHandler); + } else { + el.removeEventListener('keydown', el._keyHandler); + } + } + }); + } +}; diff --git a/src/client/app/common/keycode.ts b/src/client/app/common/keycode.ts new file mode 100644 index 000000000..c5ea6cb48 --- /dev/null +++ b/src/client/app/common/keycode.ts @@ -0,0 +1,139 @@ +export default searchInput => { + // Keyboard Events + if (searchInput && typeof searchInput === 'object') { + const hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode; + if (hasKeyCode) { + searchInput = hasKeyCode; + } + } + + // Numbers + // if (typeof searchInput === 'number') { + // return names[searchInput] + // } + + // Everything else (cast to string) + const search = String(searchInput); + + // check codes + const foundNamedKeyCodes = codes[search.toLowerCase()]; + if (foundNamedKeyCodes) { + return foundNamedKeyCodes; + } + + // check aliases + const foundNamedKeyAliases = aliases[search.toLowerCase()]; + if (foundNamedKeyAliases) { + return foundNamedKeyAliases; + } + + // weird character? + if (search.length === 1) { + return search.charCodeAt(0); + } + + return undefined; +}; + +/** + * Get by name + * + * exports.code['enter'] // => 13 + */ + +export const codes = { + 'backspace': 8, + 'tab': 9, + 'enter': 13, + 'shift': 16, + 'ctrl': 17, + 'alt': 18, + 'pause/break': 19, + 'caps lock': 20, + 'esc': 27, + 'space': 32, + 'page up': 33, + 'page down': 34, + 'end': 35, + 'home': 36, + 'left': 37, + 'up': 38, + 'right': 39, + 'down': 40, + // 'add': 43, + 'insert': 45, + 'delete': 46, + 'command': 91, + 'left command': 91, + 'right command': 93, + 'numpad *': 106, + // 'numpad +': 107, + 'numpad +': 43, + 'numpad add': 43, // as a trick + 'numpad -': 109, + 'numpad .': 110, + 'numpad /': 111, + 'num lock': 144, + 'scroll lock': 145, + 'my computer': 182, + 'my calculator': 183, + ';': 186, + '=': 187, + ',': 188, + '-': 189, + '.': 190, + '/': 191, + '`': 192, + '[': 219, + '\\': 220, + ']': 221, + "'": 222 +}; + +// Helper aliases + +export const aliases = { + 'windows': 91, + '⇧': 16, + '⌥': 18, + '⌃': 17, + '⌘': 91, + 'ctl': 17, + 'control': 17, + 'option': 18, + 'pause': 19, + 'break': 19, + 'caps': 20, + 'return': 13, + 'escape': 27, + 'spc': 32, + 'pgup': 33, + 'pgdn': 34, + 'ins': 45, + 'del': 46, + 'cmd': 91 +}; + +/*! +* Programatically add the following +*/ + +// lower case chars +for (let i = 97; i < 123; i++) { + codes[String.fromCharCode(i)] = i - 32; +} + +// numbers +for (let i = 48; i < 58; i++) { + codes[i - 48] = i; +} + +// function keys +for (let i = 1; i < 13; i++) { + codes['f' + i] = i + 111; +} + +// numpad keys +for (let i = 0; i < 10; i++) { + codes['numpad ' + i] = i + 96; +} diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue index a4828c987..58985658c 100644 --- a/src/client/app/common/views/components/reaction-picker.vue +++ b/src/client/app/common/views/components/reaction-picker.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-reaction-picker"> +<div class="mk-reaction-picker" v-hotkey.global="keymap"> <div class="backdrop" ref="backdrop" @click="close"></div> <div class="popover" :class="{ compact, big }" ref="popover"> <p v-if="!compact">{{ title }}</p> @@ -31,28 +31,51 @@ export default Vue.extend({ type: Object, required: true }, + source: { required: true }, + compact: { type: Boolean, required: false, default: false }, + cb: { required: false }, + big: { type: Boolean, required: false, default: false } }, + data() { return { title: placeholder }; }, + + computed: { + keymap(): any { + return { + '1': () => this.react('like'), + '2': () => this.react('love'), + '3': () => this.react('laugh'), + '4': () => this.react('hmm'), + '5': () => this.react('surprise'), + '6': () => this.react('congrats'), + '7': () => this.react('angry'), + '8': () => this.react('confused'), + '9': () => this.react('rip'), + '0': () => this.react('pudding'), + }; + } + }, + mounted() { this.$nextTick(() => { const popover = this.$refs.popover as any; @@ -88,6 +111,7 @@ export default Vue.extend({ }); }); }, + methods: { react(reaction) { (this as any).api('notes/reactions/create', { @@ -95,15 +119,19 @@ export default Vue.extend({ reaction: reaction }).then(() => { if (this.cb) this.cb(); + this.$emit('closed'); this.destroyDom(); }); }, + onMouseover(e) { this.title = e.target.title; }, + onMouseout(e) { this.title = placeholder; }, + close() { (this.$refs.backdrop as any).style.pointerEvents = 'none'; anime({ @@ -120,7 +148,10 @@ export default Vue.extend({ scale: 0.5, duration: 200, easing: 'easeInBack', - complete: () => this.destroyDom() + complete: () => { + this.$emit('closed'); + this.destroyDom(); + } }); } } diff --git a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue index b894f0e10..933d31f29 100644 --- a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue +++ b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue @@ -1,5 +1,5 @@ <template> -<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy"> +<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom"> <span slot="header"> <span v-html="title" :class="$style.title"></span> <span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}%i18n:@choose-file%)</span> diff --git a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue index 0c4643fdc..03d6fd163 100644 --- a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue +++ b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue @@ -1,5 +1,5 @@ <template> -<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy"> +<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom"> <span slot="header"> <span v-html="title" :class="$style.title"></span> </span> diff --git a/src/client/app/desktop/views/components/drive-window.vue b/src/client/app/desktop/views/components/drive-window.vue index 1f45b6432..191579538 100644 --- a/src/client/app/desktop/views/components/drive-window.vue +++ b/src/client/app/desktop/views/components/drive-window.vue @@ -1,5 +1,5 @@ <template> -<mk-window ref="window" @closed="$destroy" width="800px" height="500px" :popout-url="popout"> +<mk-window ref="window" @closed="destroyDom" width="800px" height="500px" :popout-url="popout"> <template slot="header"> <p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:@used%</p> <span :class="$style.title">%fa:cloud%%i18n:@drive%</span> diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue index fdab7bc1c..d5214adb2 100644 --- a/src/client/app/desktop/views/components/followers-window.vue +++ b/src/client/app/desktop/views/components/followers-window.vue @@ -1,5 +1,5 @@ <template> -<mk-window width="400px" height="550px" @closed="$destroy"> +<mk-window width="400px" height="550px" @closed="destroyDom"> <span slot="header" :class="$style.header"> <img :src="user.avatarUrl" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }} </span> diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue index 7cca833a8..aa9f2bde7 100644 --- a/src/client/app/desktop/views/components/following-window.vue +++ b/src/client/app/desktop/views/components/following-window.vue @@ -1,5 +1,5 @@ <template> -<mk-window width="400px" height="550px" @closed="$destroy"> +<mk-window width="400px" height="550px" @closed="destroyDom"> <span slot="header" :class="$style.header"> <img :src="user.avatarUrl" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }} </span> diff --git a/src/client/app/desktop/views/components/game-window.vue b/src/client/app/desktop/views/components/game-window.vue index 7c6cb9cd4..594eae58f 100644 --- a/src/client/app/desktop/views/components/game-window.vue +++ b/src/client/app/desktop/views/components/game-window.vue @@ -1,5 +1,5 @@ <template> -<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy"> +<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom"> <span slot="header" :class="$style.header">%fa:gamepad%%i18n:@game%</span> <mk-reversi :class="$style.content" @gamed="g => game = g"/> </mk-window> diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue index d45cc82e1..79c9a9a51 100644 --- a/src/client/app/desktop/views/components/home.vue +++ b/src/client/app/desktop/views/components/home.vue @@ -237,6 +237,10 @@ export default Vue.extend({ warp(date) { (this.$refs.tl as any).warp(date); + }, + + focus() { + (this.$refs.tl as any).focus(); } } }); diff --git a/src/client/app/desktop/views/components/input-dialog.vue b/src/client/app/desktop/views/components/input-dialog.vue index e2cf4e48f..cf7c09ea5 100644 --- a/src/client/app/desktop/views/components/input-dialog.vue +++ b/src/client/app/desktop/views/components/input-dialog.vue @@ -1,5 +1,5 @@ <template> -<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="$destroy"> +<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="destroyDom"> <span slot="header" :class="$style.header"> %fa:i-cursor%{{ title }} </span> diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue index 41b421b0e..370637760 100644 --- a/src/client/app/desktop/views/components/messaging-room-window.vue +++ b/src/client/app/desktop/views/components/messaging-room-window.vue @@ -1,5 +1,5 @@ <template> -<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy"> +<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom"> <span slot="header" :class="$style.header">%fa:comments%%i18n:@title% {{ user | userName }}</span> <mk-messaging-room :user="user" :class="$style.content"/> </mk-window> diff --git a/src/client/app/desktop/views/components/messaging-window.vue b/src/client/app/desktop/views/components/messaging-window.vue index 9580c5061..a8f0fc68b 100644 --- a/src/client/app/desktop/views/components/messaging-window.vue +++ b/src/client/app/desktop/views/components/messaging-window.vue @@ -1,5 +1,5 @@ <template> -<mk-window ref="window" width="500px" height="560px" @closed="$destroy"> +<mk-window ref="window" width="500px" height="560px" @closed="destroyDom"> <span slot="header" :class="$style.header">%fa:comments%%i18n:@title%</span> <mk-messaging :class="$style.content" @navigate="navigate"/> </mk-window> diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue index 46a866f9a..fadf47e62 100644 --- a/src/client/app/desktop/views/components/notes.note.vue +++ b/src/client/app/desktop/views/components/notes.note.vue @@ -1,5 +1,5 @@ <template> -<div class="note" tabindex="-1" :title="title" @keydown="onKeydown"> +<div class="note" tabindex="-1" v-hotkey="keymap" :title="title"> <div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> <x-sub :note="p.reply"/> </div> @@ -111,6 +111,18 @@ export default Vue.extend({ }, computed: { + keymap(): any { + return { + 'r': this.reply, + 'a': this.react, + 'n': this.renote, + 'up': this.focusBefore, + 'shift+tab': this.focusBefore, + 'down': this.focusAfter, + 'tab': this.focusAfter, + }; + }, + isRenote(): boolean { return (this.note.renote && this.note.text == null && @@ -223,64 +235,39 @@ export default Vue.extend({ reply() { (this as any).os.new(MkPostFormWindow, { reply: this.p - }); + }).$once('closed', this.focus); }, renote() { (this as any).os.new(MkRenoteFormWindow, { note: this.p - }); + }).$once('closed', this.focus); }, react() { (this as any).os.new(MkReactionPicker, { source: this.$refs.reactButton, note: this.p - }); + }).$once('closed', this.focus); }, menu() { (this as any).os.new(MkNoteMenu, { source: this.$refs.menuButton, note: this.p - }); + }).$once('closed', this.focus); }, - onKeydown(e) { - let shouldBeCancel = true; + focus() { + this.$el.focus(); + }, - switch (true) { - case e.which == 38: // [↑] - case e.which == 74: // [j] - case e.which == 9 && e.shiftKey: // [Shift] + [Tab] - focus(this.$el, e => e.previousElementSibling); - break; + focusBefore() { + focus(this.$el, e => e.previousElementSibling); + }, - case e.which == 40: // [↓] - case e.which == 75: // [k] - case e.which == 9: // [Tab] - focus(this.$el, e => e.nextElementSibling); - break; - - case e.which == 81: // [q] - case e.which == 69: // [e] - this.renote(); - break; - - case e.which == 70: // [f] - case e.which == 76: // [l] - //this.like(); - break; - - case e.which == 82: // [r] - this.reply(); - break; - - default: - shouldBeCancel = false; - } - - if (shouldBeCancel) e.preventDefault(); + focusAfter() { + focus(this.$el, e => e.nextElementSibling); } } }); diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue index ec9aa285d..469f62c08 100644 --- a/src/client/app/desktop/views/components/notes.vue +++ b/src/client/app/desktop/views/components/notes.vue @@ -12,7 +12,7 @@ <!-- トランジションを有効にするとなぜかメモリリークする --> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div"> <template v-for="(note, i) in _notes"> - <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> + <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" ref="note"/> <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> <span>%fa:angle-up%{{ note._datetext }}</span> <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> @@ -89,7 +89,7 @@ export default Vue.extend({ }, focus() { - (this.$el as any).children[0].focus(); + (this.$refs.note as any)[0].focus(); }, onNoteUpdated(i, note) { diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue index a88c96d1b..ade84f6bb 100644 --- a/src/client/app/desktop/views/components/post-form-window.vue +++ b/src/client/app/desktop/views/components/post-form-window.vue @@ -1,5 +1,5 @@ <template> -<mk-window class="mk-post-form-window" ref="window" is-modal @closed="$destroy"> +<mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed"> <span slot="header" class="mk-post-form-window--header"> <span class="icon" v-if="geo">%fa:map-marker-alt%</span> <span v-if="!reply">%i18n:@note%</span> @@ -53,6 +53,10 @@ export default Vue.extend({ }, onPosted() { (this.$refs.window as any).close(); + }, + onWindowClosed() { + this.$emit('closed'); + this.destroyDom(); } } }); diff --git a/src/client/app/desktop/views/components/progress-dialog.vue b/src/client/app/desktop/views/components/progress-dialog.vue index 2f59733d9..cc25ba8e3 100644 --- a/src/client/app/desktop/views/components/progress-dialog.vue +++ b/src/client/app/desktop/views/components/progress-dialog.vue @@ -1,5 +1,5 @@ <template> -<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="$destroy"> +<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="destroyDom"> <span slot="header">{{ title }}<mk-ellipsis/></span> <div :class="$style.body"> <p :class="$style.init" v-if="isNaN(value)">%i18n:@waiting%<mk-ellipsis/></p> diff --git a/src/client/app/desktop/views/components/received-follow-requests-window.vue b/src/client/app/desktop/views/components/received-follow-requests-window.vue index 26b7ec259..d8a94f6cb 100644 --- a/src/client/app/desktop/views/components/received-follow-requests-window.vue +++ b/src/client/app/desktop/views/components/received-follow-requests-window.vue @@ -1,5 +1,5 @@ <template> -<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy"> +<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom"> <span slot="header">%fa:envelope R% %i18n:@title%</span> <div class="slpqaxdoxhvglersgjukmvizkqbmbokc" :data-darkmode="$store.state.device.darkmode"> diff --git a/src/client/app/desktop/views/components/renote-form-window.vue b/src/client/app/desktop/views/components/renote-form-window.vue index df9d2f7fc..6c9cb59d4 100644 --- a/src/client/app/desktop/views/components/renote-form-window.vue +++ b/src/client/app/desktop/views/components/renote-form-window.vue @@ -1,7 +1,7 @@ <template> -<mk-window ref="window" is-modal @closed="$destroy"> +<mk-window ref="window" is-modal @closed="onWindowClosed"> <span slot="header" :class="$style.header">%fa:retweet%%i18n:@title%</span> - <mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled"/> + <mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/> </mk-window> </template> @@ -10,25 +10,32 @@ import Vue from 'vue'; export default Vue.extend({ props: ['note'], - mounted() { - document.addEventListener('keydown', this.onDocumentKeydown); - }, - beforeDestroy() { - document.removeEventListener('keydown', this.onDocumentKeydown); + + computed: { + keymap(): any { + return { + 'esc': this.close, + 'ctrl+enter': this.post + }; + } }, + methods: { - onDocumentKeydown(e) { - if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { - if (e.which == 27) { // Esc - (this.$refs.window as any).close(); - } - } + post() { + (this.$refs.form as any).ok(); + }, + close() { + (this.$refs.window as any).close(); }, onPosted() { (this.$refs.window as any).close(); }, onCanceled() { (this.$refs.window as any).close(); + }, + onWindowClosed() { + this.$emit('closed'); + this.destroyDom(); } } }); diff --git a/src/client/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue index b4cc57028..424771774 100644 --- a/src/client/app/desktop/views/components/settings-window.vue +++ b/src/client/app/desktop/views/components/settings-window.vue @@ -1,5 +1,5 @@ <template> -<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy"> +<mk-window ref="window" is-modal width="700px" height="550px" @closed="destroyDom"> <span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span> <mk-settings :initial-page="initialPage" @done="close"/> </mk-window> diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue index c8aa36f17..ff73bde95 100644 --- a/src/client/app/desktop/views/components/timeline.core.vue +++ b/src/client/app/desktop/views/components/timeline.core.vue @@ -152,14 +152,11 @@ export default Vue.extend({ }); } - document.addEventListener('keydown', this.onKeydown); - this.fetch(); }, beforeDestroy() { this.$emit('beforeDestroy'); - document.removeEventListener('keydown', this.onKeydown); }, methods: { @@ -212,14 +209,6 @@ export default Vue.extend({ warp(date) { this.date = date; this.fetch(); - }, - - onKeydown(e) { - if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { - if (e.which == 84) { // t - this.focus(); - } - } } } }); diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue index ccc35f95f..9f421a68e 100644 --- a/src/client/app/desktop/views/components/timeline.vue +++ b/src/client/app/desktop/views/components/timeline.vue @@ -92,6 +92,10 @@ export default Vue.extend({ }); }, + focus() { + (this.$refs.tl as any).focus(); + }, + warp(date) { (this.$refs.tl as any).warp(date); }, diff --git a/src/client/app/desktop/views/components/ui.vue b/src/client/app/desktop/views/components/ui.vue index d410c3d98..a28cb3029 100644 --- a/src/client/app/desktop/views/components/ui.vue +++ b/src/client/app/desktop/views/components/ui.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-ui" :style="style"> +<div class="mk-ui" :style="style" v-hotkey.global="keymap"> <x-header class="header" v-show="!zenMode"/> <div class="content"> <slot></slot> @@ -16,11 +16,13 @@ export default Vue.extend({ components: { XHeader }, + data() { return { zenMode: false }; }, + computed: { style(): any { if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {}; @@ -28,27 +30,24 @@ export default Vue.extend({ backgroundColor: this.$store.state.i.wallpaperColor && this.$store.state.i.wallpaperColor.length == 3 ? `rgb(${ this.$store.state.i.wallpaperColor.join(',') })` : null, backgroundImage: `url(${ this.$store.state.i.wallpaperUrl })` }; + }, + + keymap(): any { + return { + 'p': this.post, + 'n': this.post, + 'z': this.toggleZenMode + }; } }, - mounted() { - document.addEventListener('keydown', this.onKeydown); - }, - beforeDestroy() { - document.removeEventListener('keydown', this.onKeydown); - }, + methods: { - onKeydown(e) { - if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return; + post() { + (this as any).apis.post(); + }, - if (e.which == 80 || e.which == 78) { // p or n - e.preventDefault(); - (this as any).apis.post(); - } - - if (e.which == 90) { // z - e.preventDefault(); - this.zenMode = !this.zenMode; - } + toggleZenMode() { + this.zenMode = !this.zenMode; } } }); diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue index 72ae9cf4e..75253e078 100644 --- a/src/client/app/desktop/views/components/user-lists-window.vue +++ b/src/client/app/desktop/views/components/user-lists-window.vue @@ -1,5 +1,5 @@ <template> -<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy"> +<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom"> <span slot="header">%fa:list% %i18n:@title%</span> <div class="xkxvokkjlptzyewouewmceqcxhpgzprp" :data-darkmode="$store.state.device.darkmode"> diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue index 30f0ec558..e6886956e 100644 --- a/src/client/app/desktop/views/components/window.vue +++ b/src/client/app/desktop/views/components/window.vue @@ -190,8 +190,8 @@ export default Vue.extend({ }); setTimeout(() => { - this.destroyDom(); this.$emit('closed'); + this.destroyDom(); }, 300); }, diff --git a/src/client/app/desktop/views/pages/home.vue b/src/client/app/desktop/views/pages/home.vue index c7ff0904e..e595ef4c3 100644 --- a/src/client/app/desktop/views/pages/home.vue +++ b/src/client/app/desktop/views/pages/home.vue @@ -1,6 +1,6 @@ <template> <mk-ui> - <mk-home :mode="mode" @loaded="loaded"/> + <mk-home :mode="mode" @loaded="loaded" ref="home" v-hotkey.global="keymap"/> </mk-ui> </template> @@ -15,6 +15,13 @@ export default Vue.extend({ default: 'timeline' } }, + computed: { + keymap(): any { + return { + 't': this.focus + }; + } + }, mounted() { document.title = (this as any).os.instanceName; @@ -23,6 +30,9 @@ export default Vue.extend({ methods: { loaded() { Progress.done(); + }, + focus() { + this.$refs.home.focus(); } } }); diff --git a/src/client/app/init.ts b/src/client/app/init.ts index db3852da6..3a03f8492 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -8,6 +8,7 @@ import VueRouter from 'vue-router'; import * as TreeView from 'vue-json-tree-view'; import VAnimateCss from 'v-animate-css'; import VModal from 'vue-js-modal'; +import VueHotkey from './common/hotkey'; import App from './app.vue'; import checkForUpdate from './common/scripts/check-for-update'; @@ -19,6 +20,7 @@ Vue.use(VueRouter); Vue.use(TreeView); Vue.use(VAnimateCss); Vue.use(VModal); +Vue.use(VueHotkey); // Register global directives require('./common/views/directives');