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

View File

@ -15,7 +15,14 @@
autofocus
>
</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">
<li
v-for="(item, index) in filteredSuggestion"
@ -35,11 +42,11 @@
</ul>
<!-- dummy object to open suggest popper -->
<template #reference>
<span></span>
<el-button type="text" ref="suggestButtonRef" class="dummy-button">dummy</el-button>
</template>
</el-popover>
<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
:data="emojiIndex"
set="twitter"
@ -63,11 +70,13 @@
<script lang="ts">
import 'emoji-mart-vue-fast/css/emoji-mart.css'
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 { ElButton } from 'element-plus'
import suggestText from '@/utils/suggestText'
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({
name: 'status',
@ -99,14 +108,12 @@ export default defineComponent({
const { modelValue } = toRefs(props)
const highlightedIndex = ref(0)
const statusRef = ref<HTMLTextAreaElement>()
const suggestButtonRef = ref<InstanceType<typeof ElButton>>()
const suggestRef = ref()
const filteredAccounts = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.filteredAccounts)
const filteredHashtags = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.filteredHashtags)
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 matchWord = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.matchWord)
const customEmojis = computed(() => store.getters[`${space}/pickerEmojis`])
@ -114,17 +121,47 @@ export default defineComponent({
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 = () => {
store.dispatch(`${space}/${ACTION_TYPES.CLOSE_SUGGEST}`)
if (openSuggest.value) {
highlightedIndex.value = 0
}
ctx.emit('suggestOpened', false)
highlightedIndex.value = 0
suggestButtonRef.value?.$el.blur()
statusRef.value?.focus()
}
const suggestAccount = async (start: number, word: string) => {
try {
await store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_ACCOUNT}`, { word: word, start: start })
ctx.emit('suggestOpened', true)
openSuggest()
return true
} catch (err) {
console.log(err)
@ -134,7 +171,7 @@ export default defineComponent({
const suggestHashtag = async (start: number, word: string) => {
try {
await store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_HASHTAG}`, { word: word, start: start })
ctx.emit('suggestOpened', true)
openSuggest()
return true
} catch (err) {
console.log(err)
@ -144,7 +181,7 @@ export default defineComponent({
const suggestEmoji = async (start: number, word: string) => {
try {
store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_EMOJI}`, { word: word, start: start })
ctx.emit('suggestOpened', true)
openSuggest()
return true
} catch (err) {
console.log(err)
@ -193,20 +230,25 @@ export default defineComponent({
highlightedIndex.value = index
}
}
const selectCurrentItem = () => {
const item = filteredSuggestion.value[highlightedIndex.value]
insertItem(item)
}
const insertItem = item => {
console.log('inserted', item.name)
if (item.code) {
const str = `${modelValue.value.slice(0, startIndex.value - 1)}${item.code} ${modelValue.value.slice(
startIndex.value + matchWord.value.length
)}`
ctx.emit('update:modelValue', str)
} else {
const str = `${modelValue.value.slice(0, startIndex.value - 1)}${item.name} ${modelValue.value.slice(
startIndex.value + matchWord.value.length
)}`
console.log(str)
ctx.emit('update:modelValue', str)
if (item) {
if (item.code) {
const str = `${modelValue.value.slice(0, startIndex.value - 1)}${item.code} ${modelValue.value.slice(
startIndex.value + matchWord.value.length
)}`
ctx.emit('update:modelValue', str)
} else {
const str = `${modelValue.value.slice(0, startIndex.value - 1)}${item.name} ${modelValue.value.slice(
startIndex.value + matchWord.value.length
)}`
ctx.emit('update:modelValue', str)
}
}
closeSuggest()
}
const selectEmoji = emoji => {
@ -217,19 +259,38 @@ export default defineComponent({
// Custom emoji don't have natvie code
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 {
statusRef,
suggestButtonRef,
suggestRef,
emojiIndex,
highlightedIndex,
filteredAccounts,
filteredHashtags,
filteredSuggestion,
openSuggest,
startSuggest,
suggestHighlight,
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 {
position: absolute;
top: 4px;

View File

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