refs #3300 Handle shortcut keys in NewToot/Status

This commit is contained in:
AkiraFukushima 2022-05-01 23:27:25 +09:00
parent bd6e546765
commit c204a53d96
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
3 changed files with 104 additions and 46 deletions

View File

@ -7,7 +7,7 @@
:before-close="closeConfirm" :before-close="closeConfirm"
width="600px" width="600px"
custom-class="new-toot-modal" custom-class="new-toot-modal"
ref="dialogRef" v-if="newTootModal"
> >
<el-form v-on:submit.prevent="toot" role="form"> <el-form v-on:submit.prevent="toot" role="form">
<Quote :message="quoteToMessage" :displayNameStyle="displayNameStyle" v-if="quoteToMessage !== null" ref="quoteRef"></Quote> <Quote :message="quoteToMessage" :displayNameStyle="displayNameStyle" v-if="quoteToMessage !== null" ref="quoteRef"></Quote>
@ -145,7 +145,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, reactive, computed, onMounted, ComponentPublicInstance, nextTick } from 'vue' import { defineComponent, ref, reactive, computed, onMounted, ComponentPublicInstance, nextTick } from 'vue'
import { useI18next } from 'vue3-i18next' import { useI18next } from 'vue3-i18next'
import { ElMessage, ElMessageBox, ElDialog } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Entity } from 'megalodon' import { Entity } from 'megalodon'
import { useStore } from '@/store' import { useStore } from '@/store'
import Visibility from '~/src/constants/visibility' import Visibility from '~/src/constants/visibility'
@ -184,7 +184,6 @@ export default defineComponent({
const imageRef = ref<HTMLInputElement>() const imageRef = ref<HTMLInputElement>()
const pollRef = ref<ComponentPublicInstance>() const pollRef = ref<ComponentPublicInstance>()
const spoilerRef = ref<HTMLElement>() const spoilerRef = ref<HTMLElement>()
const dialogRef = ref<InstanceType<typeof ElDialog>>()
const quoteRef = ref<ComponentPublicInstance>() const quoteRef = ref<ComponentPublicInstance>()
const quoteToMessage = computed(() => store.state.TimelineSpace.Modals.NewToot.quoteToMessage) const quoteToMessage = computed(() => store.state.TimelineSpace.Modals.NewToot.quoteToMessage)
@ -230,6 +229,7 @@ export default defineComponent({
store.dispatch(`${space}/${ACTION_TYPES.SETUP_LOADING}`) store.dispatch(`${space}/${ACTION_TYPES.SETUP_LOADING}`)
onMounted(() => { onMounted(() => {
console.log('new toot mounted')
EventEmitter.on('image-uploaded', () => { EventEmitter.on('image-uploaded', () => {
if (previewRef.value) { if (previewRef.value) {
statusHeight.value = statusHeight.value - previewRef.value.offsetHeight statusHeight.value = statusHeight.value - previewRef.value.offsetHeight
@ -366,6 +366,7 @@ export default defineComponent({
store.commit(`${space}/${MUTATION_TYPES.CHANGE_SENSITIVE}`, !sensitive.value) store.commit(`${space}/${MUTATION_TYPES.CHANGE_SENSITIVE}`, !sensitive.value)
} }
const closeConfirm = (done: Function) => { const closeConfirm = (done: Function) => {
if (!newTootModal.value) return
if (statusText.value.length === 0) { if (statusText.value.length === 0) {
done() done()
} else { } else {
@ -467,7 +468,6 @@ export default defineComponent({
imageRef, imageRef,
pollRef, pollRef,
spoilerRef, spoilerRef,
dialogRef,
quoteRef, quoteRef,
// computed // computed
quoteToMessage, quoteToMessage,

View File

@ -15,7 +15,14 @@
autofocus autofocus
> >
</textarea> </textarea>
<el-popover placement="bottom-start" width="300" trigger="manual" v-model:visible="openSuggest" popper-class="suggest-popper"> <el-popover
placement="bottom-start"
width="300"
trigger="focus"
popper-class="suggest-popper"
:popper-options="popperOptions()"
ref="suggestRef"
>
<ul class="suggest-list"> <ul class="suggest-list">
<li <li
v-for="(item, index) in filteredSuggestion" v-for="(item, index) in filteredSuggestion"
@ -35,11 +42,11 @@
</ul> </ul>
<!-- dummy object to open suggest popper --> <!-- dummy object to open suggest popper -->
<template #reference> <template #reference>
<span></span> <el-button type="text" ref="suggestButtonRef" class="dummy-button">dummy</el-button>
</template> </template>
</el-popover> </el-popover>
<div> <div>
<el-popover placement="bottom" width="281" trigger="click" popper-class="new-toot-emoji-picker" ref="new_toot_emoji_picker"> <el-popover placement="bottom" width="281" trigger="click" popper-class="new-toot-emoji-picker">
<picker <picker
:data="emojiIndex" :data="emojiIndex"
set="twitter" set="twitter"
@ -63,11 +70,13 @@
<script lang="ts"> <script lang="ts">
import 'emoji-mart-vue-fast/css/emoji-mart.css' import 'emoji-mart-vue-fast/css/emoji-mart.css'
import data from 'emoji-mart-vue-fast/data/all.json' import data from 'emoji-mart-vue-fast/data/all.json'
import { defineComponent, computed, toRefs, ref } from 'vue' import { defineComponent, computed, toRefs, ref, onBeforeUnmount, onMounted } from 'vue'
import { Picker, EmojiIndex } from 'emoji-mart-vue-fast/src' import { Picker, EmojiIndex } from 'emoji-mart-vue-fast/src'
import { ElButton } from 'element-plus'
import suggestText from '@/utils/suggestText' import suggestText from '@/utils/suggestText'
import { useStore } from '@/store' import { useStore } from '@/store'
import { MUTATION_TYPES, ACTION_TYPES } from '@/store/TimelineSpace/Modals/NewToot/Status' import { ACTION_TYPES } from '@/store/TimelineSpace/Modals/NewToot/Status'
export default defineComponent({ export default defineComponent({
name: 'status', name: 'status',
@ -99,14 +108,12 @@ export default defineComponent({
const { modelValue } = toRefs(props) const { modelValue } = toRefs(props)
const highlightedIndex = ref(0) const highlightedIndex = ref(0)
const statusRef = ref<HTMLTextAreaElement>() const statusRef = ref<HTMLTextAreaElement>()
const suggestButtonRef = ref<InstanceType<typeof ElButton>>()
const suggestRef = ref()
const filteredAccounts = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.filteredAccounts) const filteredAccounts = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.filteredAccounts)
const filteredHashtags = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.filteredHashtags) const filteredHashtags = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.filteredHashtags)
const filteredSuggestion = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.filteredSuggestion) const filteredSuggestion = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.filteredSuggestion)
const openSuggest = computed({
get: () => store.state.TimelineSpace.Modals.NewToot.Status.openSuggest,
set: (value: boolean) => store.commit(`${space}/${MUTATION_TYPES.CHANGE_OPEN_SUGGEST}`, value)
})
const startIndex = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.startIndex) const startIndex = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.startIndex)
const matchWord = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.matchWord) const matchWord = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.matchWord)
const customEmojis = computed(() => store.getters[`${space}/pickerEmojis`]) const customEmojis = computed(() => store.getters[`${space}/pickerEmojis`])
@ -114,17 +121,47 @@ export default defineComponent({
custom: customEmojis.value custom: customEmojis.value
}) })
const onKeyUp = (event: KeyboardEvent) => {
if (event.key === 'ArrowUp') {
suggestHighlight(highlightedIndex.value - 1)
event.preventDefault()
}
if (event.key === 'ArrowDown') {
suggestHighlight(highlightedIndex.value + 1)
event.preventDefault()
}
if (event.key === 'Enter') {
selectCurrentItem()
event.preventDefault()
}
if (event.key === 'Escape') {
closeSuggest()
event.preventDefault()
}
}
onBeforeUnmount(() => {
document.removeEventListener('keyup', onKeyUp)
closeSuggest()
})
onMounted(() => {
document.addEventListener('keyup', onKeyUp)
})
const openSuggest = () => {
suggestButtonRef.value?.$el.focus()
statusRef.value?.click()
}
const closeSuggest = () => { const closeSuggest = () => {
store.dispatch(`${space}/${ACTION_TYPES.CLOSE_SUGGEST}`) store.dispatch(`${space}/${ACTION_TYPES.CLOSE_SUGGEST}`)
if (openSuggest.value) { highlightedIndex.value = 0
highlightedIndex.value = 0 suggestButtonRef.value?.$el.blur()
} statusRef.value?.focus()
ctx.emit('suggestOpened', false)
} }
const suggestAccount = async (start: number, word: string) => { const suggestAccount = async (start: number, word: string) => {
try { try {
await store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_ACCOUNT}`, { word: word, start: start }) await store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_ACCOUNT}`, { word: word, start: start })
ctx.emit('suggestOpened', true) openSuggest()
return true return true
} catch (err) { } catch (err) {
console.log(err) console.log(err)
@ -134,7 +171,7 @@ export default defineComponent({
const suggestHashtag = async (start: number, word: string) => { const suggestHashtag = async (start: number, word: string) => {
try { try {
await store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_HASHTAG}`, { word: word, start: start }) await store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_HASHTAG}`, { word: word, start: start })
ctx.emit('suggestOpened', true) openSuggest()
return true return true
} catch (err) { } catch (err) {
console.log(err) console.log(err)
@ -144,7 +181,7 @@ export default defineComponent({
const suggestEmoji = async (start: number, word: string) => { const suggestEmoji = async (start: number, word: string) => {
try { try {
store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_EMOJI}`, { word: word, start: start }) store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_EMOJI}`, { word: word, start: start })
ctx.emit('suggestOpened', true) openSuggest()
return true return true
} catch (err) { } catch (err) {
console.log(err) console.log(err)
@ -193,20 +230,25 @@ export default defineComponent({
highlightedIndex.value = index highlightedIndex.value = index
} }
} }
const selectCurrentItem = () => {
const item = filteredSuggestion.value[highlightedIndex.value]
insertItem(item)
}
const insertItem = item => { const insertItem = item => {
console.log('inserted', item.name) if (item) {
if (item.code) { if (item.code) {
const str = `${modelValue.value.slice(0, startIndex.value - 1)}${item.code} ${modelValue.value.slice( const str = `${modelValue.value.slice(0, startIndex.value - 1)}${item.code} ${modelValue.value.slice(
startIndex.value + matchWord.value.length startIndex.value + matchWord.value.length
)}` )}`
ctx.emit('update:modelValue', str) ctx.emit('update:modelValue', str)
} else { } else {
const str = `${modelValue.value.slice(0, startIndex.value - 1)}${item.name} ${modelValue.value.slice( const str = `${modelValue.value.slice(0, startIndex.value - 1)}${item.name} ${modelValue.value.slice(
startIndex.value + matchWord.value.length startIndex.value + matchWord.value.length
)}` )}`
console.log(str) ctx.emit('update:modelValue', str)
ctx.emit('update:modelValue', str) }
} }
closeSuggest() closeSuggest()
} }
const selectEmoji = emoji => { const selectEmoji = emoji => {
@ -217,19 +259,38 @@ export default defineComponent({
// Custom emoji don't have natvie code // Custom emoji don't have natvie code
ctx.emit('update:modelValue', `${modelValue.value.slice(0, current)}${emoji.name} ${modelValue.value.slice(current)}`) ctx.emit('update:modelValue', `${modelValue.value.slice(0, current)}${emoji.name} ${modelValue.value.slice(current)}`)
} }
closeSuggest()
}
const popperOptions = () => {
const element = document.querySelector('#status_textarea')
return {
modifiers: [
{
name: 'preventOverflow',
options: {
boundary: element,
rootBoundary: 'viewport',
altBoundary: true
}
}
]
}
} }
return { return {
statusRef,
suggestButtonRef,
suggestRef,
emojiIndex, emojiIndex,
highlightedIndex, highlightedIndex,
filteredAccounts, filteredAccounts,
filteredHashtags, filteredHashtags,
filteredSuggestion, filteredSuggestion,
openSuggest,
startSuggest, startSuggest,
suggestHighlight, suggestHighlight,
insertItem, insertItem,
selectEmoji selectEmoji,
popperOptions
} }
} }
}) })
@ -307,6 +368,15 @@ export default defineComponent({
} }
} }
.dummy-button {
height: 0;
font-size: 0;
padding: 0;
margin: 0;
border: none;
display: block;
}
.emoji-selector { .emoji-selector {
position: absolute; position: absolute;
top: 4px; top: 4px;

View File

@ -39,7 +39,6 @@ export type StatusState = {
filteredAccounts: Array<SuggestAccount> filteredAccounts: Array<SuggestAccount>
filteredHashtags: Array<SuggestHashtag> filteredHashtags: Array<SuggestHashtag>
filteredEmojis: Array<SuggestEmoji> filteredEmojis: Array<SuggestEmoji>
openSuggest: boolean
startIndex: number startIndex: number
matchWord: string matchWord: string
client: MegalodonInterface | null client: MegalodonInterface | null
@ -50,7 +49,6 @@ const state = (): StatusState => ({
filteredAccounts: [], filteredAccounts: [],
filteredHashtags: [], filteredHashtags: [],
filteredEmojis: [], filteredEmojis: [],
openSuggest: false,
startIndex: 0, startIndex: 0,
matchWord: '', matchWord: '',
client: null client: null
@ -63,7 +61,6 @@ export const MUTATION_TYPES = {
CLEAR_FILTERED_HASHTAGS: 'clearFilteredHashtags', CLEAR_FILTERED_HASHTAGS: 'clearFilteredHashtags',
UPDATE_FILTERED_EMOJIS: 'updateFilteredEmojis', UPDATE_FILTERED_EMOJIS: 'updateFilteredEmojis',
CLEAR_FILTERED_EMOJIS: 'clearFilteredEmojis', CLEAR_FILTERED_EMOJIS: 'clearFilteredEmojis',
CHANGE_OPEN_SUGGEST: 'changeOpenSuggest',
CHANGE_START_INDEX: 'changeStartIndex', CHANGE_START_INDEX: 'changeStartIndex',
CHANGE_MATCH_WORD: 'changeMatchWord', CHANGE_MATCH_WORD: 'changeMatchWord',
FILTERED_SUGGESTION_FROM_HASHTAGS: 'filteredSuggestionFromHashtags', FILTERED_SUGGESTION_FROM_HASHTAGS: 'filteredSuggestionFromHashtags',
@ -123,9 +120,6 @@ const mutations: MutationTree<StatusState> = {
[MUTATION_TYPES.UPDATE_FILTERED_EMOJIS]: (state, emojis: Array<SuggestEmoji>) => { [MUTATION_TYPES.UPDATE_FILTERED_EMOJIS]: (state, emojis: Array<SuggestEmoji>) => {
state.filteredEmojis = emojis state.filteredEmojis = emojis
}, },
[MUTATION_TYPES.CHANGE_OPEN_SUGGEST]: (state, value: boolean) => {
state.openSuggest = value
},
[MUTATION_TYPES.CHANGE_START_INDEX]: (state, index: number) => { [MUTATION_TYPES.CHANGE_START_INDEX]: (state, index: number) => {
state.startIndex = index state.startIndex = index
}, },
@ -177,7 +171,6 @@ const actions: ActionTree<StatusState, RootState> = {
const matched = accounts.map(account => account.acct).filter(acct => acct.includes(target)) const matched = accounts.map(account => account.acct).filter(acct => acct.includes(target))
if (matched.length === 0) throw new Error('Empty') if (matched.length === 0) throw new Error('Empty')
commit(MUTATION_TYPES.APPEND_FILTERED_ACCOUNTS, matched) commit(MUTATION_TYPES.APPEND_FILTERED_ACCOUNTS, matched)
commit(MUTATION_TYPES.CHANGE_OPEN_SUGGEST, true)
commit(MUTATION_TYPES.CHANGE_START_INDEX, start) commit(MUTATION_TYPES.CHANGE_START_INDEX, start)
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word) commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word)
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_ACCOUNTS) commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_ACCOUNTS)
@ -201,7 +194,6 @@ const actions: ActionTree<StatusState, RootState> = {
ownerID: rootState.TimelineSpace.account._id!, ownerID: rootState.TimelineSpace.account._id!,
accts: res.data.map(a => a.acct) accts: res.data.map(a => a.acct)
} as InsertAccountCache) } as InsertAccountCache)
commit(MUTATION_TYPES.CHANGE_OPEN_SUGGEST, true)
commit(MUTATION_TYPES.CHANGE_START_INDEX, start) commit(MUTATION_TYPES.CHANGE_START_INDEX, start)
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word) commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word)
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_ACCOUNTS) commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_ACCOUNTS)
@ -220,7 +212,6 @@ const actions: ActionTree<StatusState, RootState> = {
const matched = tags.map(tag => tag.tagName).filter(tag => tag.includes(target)) const matched = tags.map(tag => tag.tagName).filter(tag => tag.includes(target))
if (matched.length === 0) throw new Error('Empty') if (matched.length === 0) throw new Error('Empty')
commit(MUTATION_TYPES.APPEND_FILTERED_HASHTAGS, matched) commit(MUTATION_TYPES.APPEND_FILTERED_HASHTAGS, matched)
commit(MUTATION_TYPES.CHANGE_OPEN_SUGGEST, true)
commit(MUTATION_TYPES.CHANGE_START_INDEX, start) commit(MUTATION_TYPES.CHANGE_START_INDEX, start)
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word) commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word)
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_HASHTAGS) commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_HASHTAGS)
@ -244,7 +235,6 @@ const actions: ActionTree<StatusState, RootState> = {
'insert-cache-hashtags', 'insert-cache-hashtags',
res.data.hashtags.map(tag => tag.name) res.data.hashtags.map(tag => tag.name)
) )
commit(MUTATION_TYPES.CHANGE_OPEN_SUGGEST, true)
commit(MUTATION_TYPES.CHANGE_START_INDEX, start) commit(MUTATION_TYPES.CHANGE_START_INDEX, start)
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word) commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word)
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_HASHTAGS) commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_HASHTAGS)
@ -292,7 +282,6 @@ const actions: ActionTree<StatusState, RootState> = {
return array.findIndex(ar => e.name === ar.name) === i return array.findIndex(ar => e.name === ar.name) === i
}) })
) )
commit(MUTATION_TYPES.CHANGE_OPEN_SUGGEST, true)
commit(MUTATION_TYPES.CHANGE_START_INDEX, start) commit(MUTATION_TYPES.CHANGE_START_INDEX, start)
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word) commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word)
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_EMOJIS) commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_EMOJIS)
@ -305,7 +294,6 @@ const actions: ActionTree<StatusState, RootState> = {
}, },
[ACTION_TYPES.CLOSE_SUGGEST]: ({ commit, dispatch }) => { [ACTION_TYPES.CLOSE_SUGGEST]: ({ commit, dispatch }) => {
dispatch('cancelRequest') dispatch('cancelRequest')
commit(MUTATION_TYPES.CHANGE_OPEN_SUGGEST, false)
commit(MUTATION_TYPES.CHANGE_START_INDEX, 0) commit(MUTATION_TYPES.CHANGE_START_INDEX, 0)
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, '') commit(MUTATION_TYPES.CHANGE_MATCH_WORD, '')
commit(MUTATION_TYPES.CLEAR_FILTERED_SUGGESTION) commit(MUTATION_TYPES.CLEAR_FILTERED_SUGGESTION)