2018-08-06 18:09:00 +02:00
|
|
|
<template>
|
|
|
|
<div class="status">
|
|
|
|
<textarea
|
|
|
|
v-model="status"
|
|
|
|
ref="status"
|
2018-08-16 16:43:02 +02:00
|
|
|
v-shortkey="openSuggest ? {up: ['arrowup'], down: ['arrowdown'], enter: ['enter'], esc: ['esc']} : {linux: ['ctrl', 'enter'], mac: ['meta', 'enter'], left: ['arrowleft'], right: ['arrowright']}"
|
2018-08-06 18:09:00 +02:00
|
|
|
@shortkey="handleKey"
|
2018-08-20 05:22:35 +02:00
|
|
|
@paste="onPaste"
|
2018-08-07 18:05:14 +02:00
|
|
|
v-on:input="startSuggest"
|
2018-08-12 14:26:44 +02:00
|
|
|
:placeholder="$t('modals.new_toot.status')"
|
2018-10-21 04:17:59 +02:00
|
|
|
role="textbox"
|
2018-08-06 18:09:00 +02:00
|
|
|
autofocus>
|
|
|
|
</textarea>
|
|
|
|
<el-popover
|
|
|
|
placement="bottom-start"
|
|
|
|
width="300"
|
|
|
|
trigger="manual"
|
|
|
|
v-model="openSuggest">
|
|
|
|
<ul class="suggest-list">
|
|
|
|
<li
|
2018-08-19 05:45:22 +02:00
|
|
|
v-for="(item, index) in filteredSuggestion"
|
2018-08-06 18:09:00 +02:00
|
|
|
:key="index"
|
2018-08-19 05:45:22 +02:00
|
|
|
@click="insertItem(item)"
|
|
|
|
@shortkey="insertItem(item)"
|
2018-08-06 18:09:00 +02:00
|
|
|
@mouseover="highlightedIndex = index"
|
|
|
|
:class="{'highlighted': highlightedIndex === index}">
|
2018-08-19 07:54:16 +02:00
|
|
|
<span v-if="item.image">
|
|
|
|
<img :src="item.image" class="icon" />
|
|
|
|
</span>
|
2018-08-29 02:26:54 +02:00
|
|
|
<span v-if="item.code">
|
|
|
|
{{ item.code }}
|
|
|
|
</span>
|
2018-08-19 07:42:20 +02:00
|
|
|
{{ item.name }}
|
2018-08-06 18:09:00 +02:00
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
</el-popover>
|
2018-11-02 17:13:57 +01:00
|
|
|
<div v-click-outside="hideEmojiPicker">
|
|
|
|
<el-button type="text" class="emoji-selector" @click="toggleEmojiPicker">
|
|
|
|
<icon name="regular/smile" scale="1.2"></icon>
|
|
|
|
</el-button>
|
|
|
|
<div v-show="openEmojiPicker" class="emoji-picker">
|
|
|
|
<picker
|
|
|
|
set="emojione"
|
|
|
|
:autoFocus="true"
|
|
|
|
@select="selectEmoji"
|
|
|
|
/>
|
|
|
|
</div>
|
2018-11-02 15:58:34 +01:00
|
|
|
</div>
|
2018-08-06 18:09:00 +02:00
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
2018-08-07 16:20:58 +02:00
|
|
|
import { mapState } from 'vuex'
|
2018-08-29 02:26:54 +02:00
|
|
|
import emojilib from 'emojilib'
|
2018-11-02 15:58:34 +01:00
|
|
|
import { Picker } from 'emoji-mart-vue'
|
|
|
|
import ClickOutside from 'vue-click-outside'
|
2018-08-06 18:09:00 +02:00
|
|
|
import suggestText from '../../../../utils/suggestText'
|
|
|
|
|
|
|
|
export default {
|
|
|
|
name: 'status',
|
2018-11-02 15:58:34 +01:00
|
|
|
directives: {
|
|
|
|
ClickOutside
|
|
|
|
},
|
|
|
|
components: {
|
|
|
|
Picker
|
|
|
|
},
|
2018-08-06 18:09:00 +02:00
|
|
|
props: {
|
|
|
|
value: {
|
|
|
|
type: String
|
|
|
|
},
|
|
|
|
opened: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false
|
2018-09-19 14:24:06 +02:00
|
|
|
},
|
|
|
|
fixCursorPos: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false
|
2018-08-06 18:09:00 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
data () {
|
|
|
|
return {
|
|
|
|
openSuggest: false,
|
2018-08-07 01:41:27 +02:00
|
|
|
highlightedIndex: 0,
|
|
|
|
startIndex: null,
|
2018-08-19 05:45:22 +02:00
|
|
|
matchWord: null,
|
2018-11-02 15:58:34 +01:00
|
|
|
filteredSuggestion: [],
|
|
|
|
openEmojiPicker: false
|
2018-08-06 18:09:00 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
computed: {
|
2018-08-07 16:20:58 +02:00
|
|
|
...mapState({
|
2018-08-29 02:26:54 +02:00
|
|
|
customEmojis: state => state.TimelineSpace.emojis
|
2018-08-07 16:20:58 +02:00
|
|
|
}),
|
2018-09-18 08:49:29 +02:00
|
|
|
...mapState('TimelineSpace/Modals/NewToot/Status', {
|
|
|
|
filteredAccounts: state => state.filteredAccounts,
|
|
|
|
filteredHashtags: state => state.filteredHashtags
|
|
|
|
}),
|
2018-08-06 18:09:00 +02:00
|
|
|
status: {
|
|
|
|
get: function () {
|
|
|
|
return this.value
|
|
|
|
},
|
|
|
|
set: function (value) {
|
|
|
|
this.$emit('input', value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2018-08-20 14:38:38 +02:00
|
|
|
mounted () {
|
|
|
|
// When change account, the new toot modal is recreated.
|
|
|
|
// So can not catch open event in watch.
|
|
|
|
this.$refs.status.focus()
|
2018-09-19 14:24:06 +02:00
|
|
|
if (this.fixCursorPos) {
|
|
|
|
this.$refs.status.setSelectionRange(0, 0)
|
|
|
|
}
|
2018-08-20 14:38:38 +02:00
|
|
|
},
|
2018-08-06 18:09:00 +02:00
|
|
|
watch: {
|
|
|
|
opened: function (newState, oldState) {
|
|
|
|
if (!oldState && newState) {
|
|
|
|
this.$nextTick(function () {
|
|
|
|
this.$refs.status.focus()
|
2018-09-19 14:24:06 +02:00
|
|
|
if (this.fixCursorPos) {
|
|
|
|
this.$refs.status.setSelectionRange(0, 0)
|
|
|
|
}
|
2018-08-06 18:09:00 +02:00
|
|
|
})
|
2018-08-07 16:20:58 +02:00
|
|
|
} else if (oldState && !newState) {
|
|
|
|
this.closeSuggest()
|
2018-11-02 15:58:34 +01:00
|
|
|
this.openEmojiPicker = false
|
2018-08-06 18:09:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
2018-08-07 18:05:14 +02:00
|
|
|
startSuggest (e) {
|
|
|
|
const currentValue = e.target.value
|
|
|
|
// Start suggest after user stop writing
|
|
|
|
setTimeout(() => {
|
|
|
|
if (currentValue === this.status) {
|
2018-08-19 05:45:22 +02:00
|
|
|
this.suggest(e)
|
2018-08-07 18:05:14 +02:00
|
|
|
}
|
|
|
|
}, 500)
|
|
|
|
},
|
2018-08-19 05:45:22 +02:00
|
|
|
async suggest (e) {
|
2018-09-18 08:49:29 +02:00
|
|
|
const emoji = this.suggestEmoji(e)
|
|
|
|
if (emoji) {
|
|
|
|
return true
|
|
|
|
}
|
2018-08-19 05:45:22 +02:00
|
|
|
const ac = await this.suggestAccount(e)
|
2018-09-18 08:49:29 +02:00
|
|
|
if (ac) {
|
|
|
|
return true
|
2018-08-19 05:45:22 +02:00
|
|
|
}
|
2018-09-18 08:49:29 +02:00
|
|
|
const tag = await this.suggestHashtag(e)
|
|
|
|
return tag
|
2018-08-19 05:45:22 +02:00
|
|
|
},
|
2018-08-07 16:20:58 +02:00
|
|
|
async suggestAccount (e) {
|
2018-08-06 18:09:00 +02:00
|
|
|
// e.target.sectionStart: Cursor position
|
|
|
|
// e.target.value: current value of the textarea
|
|
|
|
const [start, word] = suggestText(e.target.value, e.target.selectionStart, '@')
|
|
|
|
if (!start || !word) {
|
2018-08-07 14:40:29 +02:00
|
|
|
this.closeSuggest()
|
2018-08-06 18:09:00 +02:00
|
|
|
return false
|
|
|
|
}
|
2018-08-07 16:20:58 +02:00
|
|
|
try {
|
|
|
|
await this.$store.dispatch('TimelineSpace/Modals/NewToot/Status/searchAccount', word)
|
|
|
|
this.openSuggest = true
|
|
|
|
this.startIndex = start
|
|
|
|
this.matchWord = word
|
2018-08-19 05:45:22 +02:00
|
|
|
this.filteredSuggestion = this.filteredAccounts
|
|
|
|
return true
|
2018-08-07 16:20:58 +02:00
|
|
|
} catch (err) {
|
|
|
|
console.log(err)
|
2018-08-19 05:45:22 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
},
|
2018-09-18 08:49:29 +02:00
|
|
|
async suggestHashtag (e) {
|
|
|
|
const [start, word] = suggestText(e.target.value, e.target.selectionStart, '#')
|
|
|
|
if (!start || !word) {
|
|
|
|
this.closeSuggest()
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
await this.$store.dispatch('TimelineSpace/Modals/NewToot/Status/searchHashtag', word)
|
|
|
|
this.openSuggest = true
|
|
|
|
this.startIndex = start
|
|
|
|
this.matchWord = word
|
|
|
|
this.filteredSuggestion = this.filteredHashtags
|
|
|
|
return true
|
|
|
|
} catch (err) {
|
|
|
|
console.log(err)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
},
|
2018-08-19 05:45:22 +02:00
|
|
|
suggestEmoji (e) {
|
|
|
|
// e.target.sectionStart: Cursor position
|
|
|
|
// e.target.value: current value of the textarea
|
|
|
|
const [start, word] = suggestText(e.target.value, e.target.selectionStart, ':')
|
|
|
|
if (!start || !word) {
|
|
|
|
this.closeSuggest()
|
|
|
|
return false
|
|
|
|
}
|
2018-08-29 02:26:54 +02:00
|
|
|
// Find native emojis
|
|
|
|
const filteredEmojiName = emojilib.ordered.filter(emoji => `:${emoji}`.includes(word))
|
|
|
|
const filteredNativeEmoji = filteredEmojiName.map((name) => {
|
|
|
|
return {
|
|
|
|
name: `:${name}:`,
|
|
|
|
code: emojilib.lib[name].char
|
|
|
|
}
|
|
|
|
})
|
|
|
|
// Find custom emojis
|
|
|
|
const filteredCustomEmoji = this.customEmojis.filter(emoji => emoji.name.includes(word))
|
|
|
|
const filtered = filteredNativeEmoji.concat(filteredCustomEmoji)
|
2018-08-19 05:45:22 +02:00
|
|
|
if (filtered.length > 0) {
|
|
|
|
this.openSuggest = true
|
|
|
|
this.startIndex = start
|
|
|
|
this.matchWord = word
|
2018-10-29 15:35:34 +01:00
|
|
|
this.filteredSuggestion = filtered.filter((e, i, array) => {
|
|
|
|
return (array.findIndex(ar => e.code === ar.code) === i)
|
|
|
|
})
|
2018-08-19 05:45:22 +02:00
|
|
|
} else {
|
|
|
|
this.openSuggest = false
|
2018-08-06 18:09:00 +02:00
|
|
|
}
|
2018-08-19 05:45:22 +02:00
|
|
|
return true
|
2018-08-06 18:09:00 +02:00
|
|
|
},
|
2018-08-07 14:40:29 +02:00
|
|
|
closeSuggest () {
|
|
|
|
this.openSuggest = false
|
|
|
|
this.startIndex = null
|
|
|
|
this.matchWord = null
|
2018-08-07 16:20:58 +02:00
|
|
|
this.highlightedIndex = 0
|
2018-08-19 05:45:22 +02:00
|
|
|
this.filteredSuggestion = []
|
2018-08-07 16:20:58 +02:00
|
|
|
this.$store.commit('TimelineSpace/Modals/NewToot/Status/clearFilteredAccounts')
|
2018-09-18 08:49:29 +02:00
|
|
|
this.$store.commit('TimelineSpace/Modals/NewToot/Status/clearFilteredHashtags')
|
2018-08-07 14:40:29 +02:00
|
|
|
},
|
2018-08-06 18:09:00 +02:00
|
|
|
suggestHighlight (index) {
|
|
|
|
if (index < 0) {
|
|
|
|
this.highlightedIndex = 0
|
2018-08-19 05:45:22 +02:00
|
|
|
} else if (index >= this.filteredSuggestion.length) {
|
|
|
|
this.highlightedIndex = this.filteredSuggestion.length - 1
|
2018-08-06 18:09:00 +02:00
|
|
|
} else {
|
|
|
|
this.highlightedIndex = index
|
|
|
|
}
|
|
|
|
},
|
2018-08-19 05:45:22 +02:00
|
|
|
insertItem (item) {
|
2018-08-29 02:26:54 +02:00
|
|
|
if (item.code) {
|
|
|
|
const str = `${this.status.slice(0, this.startIndex - 1)}${item.code} ${this.status.slice(this.startIndex + this.matchWord.length)}`
|
|
|
|
this.status = str
|
|
|
|
} else {
|
|
|
|
const str = `${this.status.slice(0, this.startIndex - 1)}${item.name} ${this.status.slice(this.startIndex + this.matchWord.length)}`
|
|
|
|
this.status = str
|
|
|
|
}
|
2018-08-07 16:20:58 +02:00
|
|
|
this.closeSuggest()
|
2018-08-07 01:41:27 +02:00
|
|
|
},
|
2018-08-19 05:45:22 +02:00
|
|
|
selectCurrentItem () {
|
|
|
|
const item = this.filteredSuggestion[this.highlightedIndex]
|
|
|
|
this.insertItem(item)
|
2018-08-06 18:09:00 +02:00
|
|
|
},
|
2018-08-20 05:22:35 +02:00
|
|
|
onPaste (e) {
|
|
|
|
this.$emit('paste', e)
|
|
|
|
},
|
2018-08-06 18:09:00 +02:00
|
|
|
handleKey (event) {
|
2018-08-15 02:34:42 +02:00
|
|
|
const current = event.target.selectionStart
|
2018-08-06 18:09:00 +02:00
|
|
|
switch (event.srcKey) {
|
|
|
|
case 'up':
|
|
|
|
this.suggestHighlight(this.highlightedIndex - 1)
|
|
|
|
break
|
|
|
|
case 'down':
|
|
|
|
this.suggestHighlight(this.highlightedIndex + 1)
|
|
|
|
break
|
|
|
|
case 'enter':
|
2018-08-19 05:45:22 +02:00
|
|
|
this.selectCurrentItem()
|
2018-08-07 01:41:27 +02:00
|
|
|
break
|
2018-08-16 16:43:02 +02:00
|
|
|
case 'esc':
|
|
|
|
this.closeSuggest()
|
|
|
|
break
|
2018-08-15 02:34:42 +02:00
|
|
|
case 'left':
|
|
|
|
event.target.setSelectionRange(current - 1, current - 1)
|
|
|
|
break
|
|
|
|
case 'right':
|
|
|
|
event.target.setSelectionRange(current + 1, current + 1)
|
|
|
|
break
|
2018-08-07 18:09:46 +02:00
|
|
|
case 'linux':
|
|
|
|
case 'mac':
|
|
|
|
this.$emit('toot')
|
|
|
|
break
|
2018-08-06 18:09:00 +02:00
|
|
|
default:
|
|
|
|
return true
|
|
|
|
}
|
2018-11-02 15:58:34 +01:00
|
|
|
},
|
|
|
|
toggleEmojiPicker () {
|
|
|
|
this.openEmojiPicker = !this.openEmojiPicker
|
|
|
|
},
|
|
|
|
hideEmojiPicker () {
|
|
|
|
this.openEmojiPicker = false
|
2018-11-02 17:13:57 +01:00
|
|
|
},
|
|
|
|
selectEmoji (emoji) {
|
|
|
|
const current = this.$refs.status.selectionStart
|
|
|
|
this.status = `${this.status.slice(0, current)}${emoji.native} ${this.status.slice(current)}`
|
|
|
|
this.hideEmojiPicker()
|
2018-08-06 18:09:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
.status {
|
2018-11-02 15:58:34 +01:00
|
|
|
position: relative;
|
|
|
|
|
2018-08-06 18:09:00 +02:00
|
|
|
textarea {
|
|
|
|
display: block;
|
2018-11-02 15:58:34 +01:00
|
|
|
padding: 4px 32px 4px 16px;
|
2018-08-06 18:09:00 +02:00
|
|
|
line-height: 1.5;
|
|
|
|
box-sizing: border-box;
|
|
|
|
width: 100%;
|
|
|
|
font-size: inherit;
|
|
|
|
color: #606266;
|
|
|
|
background-image: none;
|
|
|
|
border: 0;
|
|
|
|
border-radius: 4px;
|
|
|
|
resize: none;
|
|
|
|
height: 120px;
|
2018-08-07 16:20:58 +02:00
|
|
|
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 9.355, 1);
|
2018-08-06 18:09:00 +02:00
|
|
|
font-family: 'Lato', sans-serif;
|
|
|
|
|
|
|
|
&::placeholder {
|
|
|
|
color: #c0c4cc;
|
|
|
|
}
|
|
|
|
|
|
|
|
&:focus {
|
|
|
|
outline: 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.suggest-list {
|
|
|
|
list-style: none;
|
|
|
|
padding: 6px 0;
|
|
|
|
margin: 0;
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
|
|
li {
|
|
|
|
font-size: 14px;
|
|
|
|
padding: 0 20px;
|
|
|
|
white-space: nowrap;
|
|
|
|
overflow: hidden;
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
color: #606266;
|
|
|
|
height: 34px;
|
|
|
|
line-height: 34px;
|
|
|
|
box-sizing: border-box;
|
|
|
|
cursor: pointer;
|
2018-08-19 07:54:16 +02:00
|
|
|
|
|
|
|
.icon {
|
|
|
|
display: inline-block;
|
|
|
|
vertical-align: middle;
|
|
|
|
width: 20px;
|
|
|
|
height: 20px;
|
|
|
|
}
|
2018-08-06 18:09:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
.highlighted {
|
|
|
|
background-color: #f5f7fa;
|
|
|
|
}
|
|
|
|
}
|
2018-11-02 15:58:34 +01:00
|
|
|
|
|
|
|
.emoji-selector {
|
|
|
|
position: absolute;
|
|
|
|
top: 4px;
|
|
|
|
right: 8px;
|
|
|
|
padding: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.emoji-picker {
|
|
|
|
position: absolute;
|
|
|
|
top: 32px;
|
|
|
|
left: 240px;
|
|
|
|
}
|
2018-08-06 18:09:00 +02:00
|
|
|
}
|
|
|
|
</style>
|