aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/components/emoji_input/emoji_input.js33
-rw-r--r--src/components/emoji_input/emoji_input.vue16
-rw-r--r--src/components/emoji_picker/emoji_picker.js82
-rw-r--r--src/components/emoji_picker/emoji_picker.scss78
-rw-r--r--src/components/emoji_picker/emoji_picker.vue60
-rw-r--r--src/components/post_status_form/post_status_form.js25
-rw-r--r--src/components/post_status_form/post_status_form.vue26
-rw-r--r--src/components/sticker_picker/sticker_picker.js4
-rw-r--r--src/components/sticker_picker/sticker_picker.vue72
-rw-r--r--src/components/tab_switcher/tab_switcher.js26
-rw-r--r--src/components/tab_switcher/tab_switcher.scss11
-rw-r--r--src/i18n/en.json9
12 files changed, 296 insertions, 146 deletions
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 5a9d1406..5ff27b20 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -58,6 +58,16 @@ const EmojiInput = {
required: false,
type: Boolean,
default: false
+ },
+ emojiPickerExternalTrigger: {
+ required: false,
+ type: Boolean,
+ default: false
+ },
+ stickerPicker: {
+ required: false,
+ type: Boolean,
+ default: false
}
},
data () {
@@ -95,9 +105,6 @@ const EmojiInput = {
textAtCaret () {
return (this.wordAtCaret || {}).word || ''
},
- pickerIconBottom () {
- return this.input && this.input.tag === 'textarea'
- },
wordAtCaret () {
if (this.value && this.caret) {
const word = Completion.wordAtPosition(this.value, this.caret - 1) || {}
@@ -133,6 +140,9 @@ const EmojiInput = {
}
},
methods: {
+ triggerShowPicker () {
+ this.showPicker = true
+ },
togglePicker () {
this.showPicker = !this.showPicker
},
@@ -148,6 +158,15 @@ const EmojiInput = {
this.value.substring(this.caret)
].join('')
this.$emit('input', newValue)
+ const position = this.caret + insertion.length
+
+ this.$nextTick(function () {
+ // Re-focus inputbox after clicking suggestion
+ this.input.elm.focus()
+ // Set selection right after the replacement instead of the very end
+ this.input.elm.setSelectionRange(position, position)
+ this.caret = position
+ })
},
replaceText (e, suggestion) {
const len = this.suggestions.length || 0
@@ -264,6 +283,14 @@ const EmojiInput = {
onClickOutside () {
this.showPicker = false
},
+ onStickerUploaded (e) {
+ this.showPicker = false
+ this.$emit('sticker-uploaded', e)
+ },
+ onStickerUploadFailed (e) {
+ this.showPicker = false
+ this.$emit('sticker-upload-Failed', e)
+ },
setCaret ({ target: { selectionStart } }) {
this.caret = selectionStart
},
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index 3ca12af1..b077e6e9 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -6,8 +6,8 @@
<slot />
<template v-if="emojiPicker">
<div
+ v-if="!emojiPickerExternalTrigger"
class="emoji-picker-icon"
- :class="pickerIconBottom ? 'picker-icon-bottom': 'picker-icon-right'"
@click.prevent="togglePicker"
>
<i class="icon-smile" />
@@ -16,8 +16,11 @@
v-if="emojiPicker"
ref="picker"
:class="{ hide: !showPicker }"
+ :sticker-picker="stickerPicker"
class="emoji-picker-panel"
@emoji="insert"
+ @sticker-uploaded="onStickerUploaded"
+ @sticker-upload-failed="onStickerUploadFailed"
/>
</template>
<div
@@ -62,6 +65,8 @@
.emoji-picker-icon {
position: absolute;
+ top: 0;
+ right: 0;
margin: 0 .25em;
font-size: 16px;
cursor: pointer;
@@ -70,15 +75,6 @@
color: $fallback--text;
color: var(--text, $fallback--text);
}
-
- &.picker-icon-bottom {
- bottom: 0;
- left: 0;
- }
- &.picker-icon-right {
- top: 0;
- right: 0;
- }
}
.emoji-picker-panel {
position: absolute;
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index e25f98ff..0a64f759 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,15 +1,26 @@
+
const filterByKeyword = (list, keyword = '') => {
return list.filter(x => x.displayText.includes(keyword))
}
const EmojiPicker = {
+ props: {
+ stickerPicker: {
+ required: false,
+ type: Boolean,
+ default: false
+ }
+ },
data () {
return {
keyword: '',
- activeGroup: 'standard',
- showingAdditional: false
+ activeGroup: 'custom',
+ showingStickers: false
}
},
+ components: {
+ StickerPicker: () => import('../sticker_picker/sticker_picker.vue')
+ },
methods: {
onEmoji (emoji) {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
@@ -19,37 +30,72 @@ const EmojiPicker = {
highlight (key) {
const ref = this.$refs['group-' + key]
const top = ref[0].offsetTop
- this.$refs['emoji-groups'].scrollTop = top + 1
+ this.setShowStickers(false)
this.activeGroup = key
+ this.$nextTick(() => {
+ this.$refs['emoji-groups'].scrollTop = top + 1
+ })
},
scrolledGroup (e) {
- const top = e.target.scrollTop
- Object.keys(this.emojis).forEach(key => {
- if (this.$refs['group-' + key][0].offsetTop < top) {
- this.activeGroup = key
- }
+ const target = (e && e.target) || this.$refs['emoji-groups']
+ const top = target.scrollTop + 5
+ this.$nextTick(() => {
+ this.emojisView.forEach(group => {
+ const ref = this.$refs['group-' + group.id]
+ if (ref[0].offsetTop <= top) {
+ this.activeGroup = group.id
+ }
+ })
})
},
- toggleAdditional (value) {
- this.showingAdditional = value
+ toggleStickers () {
+ this.showingStickers = !this.showingStickers
+ },
+ setShowStickers (value) {
+ this.showingStickers = value
+ },
+ onStickerUploaded (e) {
+ this.$emit('sticker-uploaded', e)
+ },
+ onStickerUploadFailed (e) {
+ this.$emit('sticker-upload-failed', e)
+ }
+ },
+ watch: {
+ keyword () {
+ this.scrolledGroup()
}
},
computed: {
+ activeGroupView () {
+ return this.showingStickers ? '' : this.activeGroup
+ },
+ stickersAvailable () {
+ if (this.$store.state.instance.stickers) {
+ return this.$store.state.instance.stickers.length > 0
+ }
+ return 0
+ },
emojis () {
const standardEmojis = this.$store.state.instance.emoji || []
const customEmojis = this.$store.state.instance.customEmoji || []
- return {
- custom: {
- text: 'Custom',
- icon: 'icon-picture',
+ return [
+ {
+ id: 'custom',
+ text: this.$t('emoji.custom'),
+ icon: 'icon-smile',
emojis: filterByKeyword(customEmojis, this.keyword)
},
- standard: {
- text: 'Standard',
- icon: 'icon-star',
+ {
+ id: 'standard',
+ text: this.$t('emoji.unicode'),
+ icon: 'icon-picture',
emojis: filterByKeyword(standardEmojis, this.keyword)
}
- }
+ ]
+ },
+ emojisView () {
+ return this.emojis.filter(value => value.emojis.length > 0)
}
}
}
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index 72889441..6c13e82b 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -1,39 +1,78 @@
@import '../../_variables.scss';
.emoji-picker {
+ display: flex;
+ flex-direction: column;
position: absolute;
- z-index: 1;
right: 0;
- width: 300px;
+ left: 0;
height: 300px;
- display: flex;
- flex-direction: column;
margin: 0 !important;
+ z-index: 1;
- .emoji {
- &-tabs {
- &-item {
- padding: 0 5px;
+ .panel-body {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 0;
+ min-height: 0px;
+ }
+
+ .additional-tabs {
+ border-left: 1px solid;
+ border-left-color: $fallback--icon;
+ border-left-color: var(--icon, $fallback--icon);
+ padding-left: 5px;
+ flex: 0 0 0;
+ }
+
+ .emoji-tabs {
+ flex: 1 1 0;
+ }
+
+ .additional-tabs,
+ .emoji-tabs {
+ &-item {
+ padding: 0 5px;
+ cursor: pointer;
+ font-size: 24px;
- &.active {
- border-bottom: 4px solid;
+ &.disabled {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+ &.active {
+ border-bottom: 4px solid;
- i {
- color: $fallback--lightText;
- color: var(--lightText, $fallback--lightText);
- }
+ i {
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
}
}
}
+ }
+ .sticker-picker {
+ flex: 1 1 0
+ }
+
+ .stickers,
+ .emoji {
&-content {
display: flex;
flex-direction: column;
+ flex: 1 1 0;
+ min-height: 0;
+
+ &.hidden {
+ display: none
+ }
}
+ }
+ .emoji {
&-search {
padding: 5px;
- flex: 0 0 1px;
+ flex: 0 0 0;
input {
width: 100%;
@@ -50,13 +89,16 @@
display: flex;
align-items: center;
flex-wrap: wrap;
- padding: 5px;
- justify-content: space-between;
+ padding-left: 5px;
+ justify-content: left;
&-title {
font-size: 12px;
width: 100%;
margin: 0;
+ &.disabled {
+ display: none;
+ }
}
}
@@ -68,7 +110,7 @@
font-size: 32px;
align-items: center;
justify-content: center;
- margin: 2px;
+ margin: 4px;
cursor: pointer;
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index ec1702f3..12b1569e 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -3,30 +3,44 @@
<div class="panel-heading">
<span class="emoji-tabs">
<span
- v-for="(value, key) in emojis"
- :key="key"
+ v-for="group in emojis"
+ :key="group.id"
class="emoji-tabs-item"
- :class="{'active': activeGroup === key}"
- :title="value.text"
- @click.prevent="highlight(key)"
+ :class="{
+ active: activeGroupView === group.id,
+ disabled: group.emojis.length === 0
+ }"
+ :title="group.text"
+ @click.prevent="highlight(group.id)"
>
- <i :class="value.icon" />
+ <i :class="group.icon" />
</span>
</span>
- <span class="additional-tabs">
- <slot name="tabs" />
+ <span
+ v-if="stickerPicker"
+ class="additional-tabs"
+ >
+ <span
+ class="stickers-tab-icon additional-tabs-item"
+ :class="{active: showingStickers}"
+ :title="$t('emoji.stickers')"
+ @click.prevent="toggleStickers"
+ >
+ <i class="icon-star" />
+ </span>
</span>
</div>
- <div class="panel-body emoji-dropdown-menu-content">
+ <div class="panel-body">
<div
- v-if="!showingAdditional"
class="emoji-content"
+ :class="{hidden: showingStickers}"
>
<div class="emoji-search">
<input
v-model="keyword"
type="text"
class="form-control"
+ :placeholder="$t('emoji.search_emoji')"
>
</div>
<div
@@ -35,22 +49,22 @@
@scroll="scrolledGroup"
>
<div
- v-for="(value, key) in emojis"
- :key="key"
+ v-for="group in emojisView"
+ :key="group.id"
class="emoji-group"
>
<h6
- :ref="'group-' + key"
+ :ref="'group-' + group.id"
class="emoji-group-title"
>
- {{ value.text }}
+ {{ group.text }}
</h6>
<span
- v-for="emoji in value.emojis"
- :key="key + emoji.displayText"
+ v-for="emoji in group.emojis"
+ :key="group.id + emoji.displayText"
:title="emoji.displayText"
class="emoji-item"
- @click="onEmoji(emoji)"
+ @click.stop.prevent="onEmoji(emoji)"
>
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
<img
@@ -61,11 +75,17 @@
</div>
</div>
</div>
- <div v-if="showingAdditional" class="additional-tabs-content">
- <slot name="tab-content" />
+ <div
+ v-if="showingStickers"
+ class="stickers-content"
+ >
+ <sticker-picker
+ @uploaded="onStickerUploaded"
+ @upload-failed="onStickerUploadFailed"
+ />
</div>
</div>
-</div>
+ </div>
</template>
<script src="./emoji_picker.js"></script>
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index f646aeb5..1359e75a 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -3,7 +3,6 @@ import MediaUpload from '../media_upload/media_upload.vue'
import ScopeSelector from '../scope_selector/scope_selector.vue'
import EmojiInput from '../emoji_input/emoji_input.vue'
import PollForm from '../poll/poll_form.vue'
-import StickerPicker from '../sticker_picker/sticker_picker.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
import { reject, map, uniqBy } from 'lodash'
import suggestor from '../emoji_input/suggestor.js'
@@ -35,7 +34,6 @@ const PostStatusForm = {
MediaUpload,
EmojiInput,
PollForm,
- StickerPicker,
ScopeSelector
},
mounted () {
@@ -84,8 +82,7 @@ const PostStatusForm = {
contentType
},
caret: 0,
- pollFormVisible: false,
- stickerPickerVisible: false
+ pollFormVisible: false
}
},
computed: {
@@ -161,12 +158,6 @@ const PostStatusForm = {
safeDMEnabled () {
return this.$store.state.instance.safeDM
},
- stickersAvailable () {
- if (this.$store.state.instance.stickers) {
- return this.$store.state.instance.stickers.length > 0
- }
- return 0
- },
pollsAvailable () {
return this.$store.state.instance.pollsAvailable &&
this.$store.state.instance.pollLimits.max_options >= 2
@@ -222,7 +213,6 @@ const PostStatusForm = {
poll: {}
}
this.pollFormVisible = false
- this.stickerPickerVisible = false
this.$refs.mediaUpload.clearFile()
this.clearPollForm()
this.$emit('posted')
@@ -239,7 +229,6 @@ const PostStatusForm = {
addMediaFile (fileInfo) {
this.newStatus.files.push(fileInfo)
this.enableSubmit()
- this.stickerPickerVisible = false
},
removeMediaFile (fileInfo) {
let index = this.newStatus.files.indexOf(fileInfo)
@@ -293,20 +282,16 @@ const PostStatusForm = {
target.style.height = null
}
},
+ showEmoji () {
+ this.$refs['textarea'].focus()
+ this.$refs['emoji-input'].triggerShowPicker()
+ },
clearError () {
this.error = null
},
changeVis (visibility) {
this.newStatus.visibility = visibility
},
- toggleStickerPicker () {
- this.stickerPickerVisible = !this.stickerPickerVisible
- },
- clearStickerPicker () {
- if (this.$refs.stickerPicker) {
- this.$refs.stickerPicker.clear()
- }
- },
togglePollForm () {
this.pollFormVisible = !this.pollFormVisible
},
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index e691acad..ad2c2218 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -74,10 +74,15 @@
>
</EmojiInput>
<EmojiInput
+ ref="emoji-input"
v-model="newStatus.status"
:suggest="emojiUserSuggestor"
- emoji-picker
class="form-control main-input"
+ emoji-picker
+ emoji-picker-external-trigger
+ sticker-picker
+ @sticker-uploaded="addMediaFile"
+ @sticker-upload-failed="uploadFailed"
>
<textarea
ref="textarea"
@@ -160,14 +165,12 @@
@upload-failed="uploadFailed"
/>
<div
- v-if="stickersAvailable"
- class="sticker-icon"
+ class="emoji-icon"
>
<i
- :title="$t('stickers.add_sticker')"
- class="icon-picture btn btn-default"
- :class="{ selected: stickerPickerVisible }"
- @click="toggleStickerPicker"
+ :title="$t('emoji.add_emoji')"
+ class="icon-smile btn btn-default"
+ @click.stop.prevent="showEmoji"
/>
</div>
<div
@@ -260,11 +263,6 @@
<label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label>
</div>
</form>
- <sticker-picker
- v-if="stickerPickerVisible"
- ref="stickerPicker"
- @uploaded="addMediaFile"
- />
</div>
</template>
@@ -327,7 +325,7 @@
}
}
- .poll-icon, .sticker-icon {
+ .poll-icon, .emoji-icon {
font-size: 26px;
flex: 1;
@@ -337,7 +335,7 @@
}
}
- .sticker-icon {
+ .emoji-icon {
flex: 0;
min-width: 50px;
}
diff --git a/src/components/sticker_picker/sticker_picker.js b/src/components/sticker_picker/sticker_picker.js
index a6dcded3..8daf3f07 100644
--- a/src/components/sticker_picker/sticker_picker.js
+++ b/src/components/sticker_picker/sticker_picker.js
@@ -3,9 +3,9 @@ import statusPosterService from '../../services/status_poster/status_poster.serv
import TabSwitcher from '../tab_switcher/tab_switcher.js'
const StickerPicker = {
- components: [
+ components: {
TabSwitcher
- ],
+ },
data () {
return {
meta: {
diff --git a/src/components/sticker_picker/sticker_picker.vue b/src/components/sticker_picker/sticker_picker.vue
index 938204c8..323855b9 100644
--- a/src/components/sticker_picker/sticker_picker.vue
+++ b/src/components/sticker_picker/sticker_picker.vue
@@ -2,32 +2,30 @@
<div
class="sticker-picker"
>
- <div
- class="sticker-picker-panel"
+ <tab-switcher
+ class="tab-switcher"
+ :render-only-focused="true"
+ scrollable-tabs
>
- <tab-switcher
- :render-only-focused="true"
+ <div
+ v-for="stickerpack in pack"
+ :key="stickerpack.path"
+ :image-tooltip="stickerpack.meta.title"
+ :image="stickerpack.path + stickerpack.meta.tabIcon"
+ class="sticker-picker-content"
>
<div
- v-for="stickerpack in pack"
- :key="stickerpack.path"
- :image-tooltip="stickerpack.meta.title"
- :image="stickerpack.path + stickerpack.meta.tabIcon"
- class="sticker-picker-content"
+ v-for="sticker in stickerpack.meta.stickers"
+ :key="sticker"
+ class="sticker"
+ @click.stop.prevent="pick(stickerpack.path + sticker, stickerpack.meta.title)"
>
- <div
- v-for="sticker in stickerpack.meta.stickers"
- :key="sticker"
- class="sticker"
- @click="pick(stickerpack.path + sticker, stickerpack.meta.title)"
+ <img
+ :src="stickerpack.path + sticker"
>
- <img
- :src="stickerpack.path + sticker"
- >
- </div>
</div>
- </tab-switcher>
- </div>
+ </div>
+ </tab-switcher>
</div>
</template>
@@ -37,22 +35,24 @@
@import '../../_variables.scss';
.sticker-picker {
- .sticker-picker-panel {
- display: inline-block;
- width: 100%;
- .sticker-picker-content {
- max-height: 300px;
- overflow-y: scroll;
- overflow-x: auto;
- .sticker {
- display: inline-block;
- width: 20%;
- height: 20%;
- img {
- width: 100%;
- &:hover {
- filter: drop-shadow(0 0 5px var(--link, $fallback--link));
- }
+ width: 100%;
+ position: relative;
+ .tab-switcher {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+ .sticker-picker-content {
+ .sticker {
+ display: inline-block;
+ width: 20%;
+ height: 20%;
+ img {
+ width: 100%;
+ &:hover {
+ filter: drop-shadow(0 0 5px var(--link, $fallback--link));
}
}
}
diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js
index a5fe019c..99428044 100644
--- a/src/components/tab_switcher/tab_switcher.js
+++ b/src/components/tab_switcher/tab_switcher.js
@@ -4,7 +4,26 @@ import './tab_switcher.scss'
export default Vue.component('tab-switcher', {
name: 'TabSwitcher',
- props: ['renderOnlyFocused', 'onSwitch', 'customActive'],
+ props: {
+ renderOnlyFocused: {
+ required: false,
+ type: Boolean,
+ default: false
+ },
+ onSwitch: {
+ required: false,
+ type: Function
+ },
+ customActive: {
+ required: false,
+ type: String
+ },
+ scrollableTabs: {
+ required: false,
+ type: Boolean,
+ default: false
+ }
+ },
data () {
return {
active: this.$slots.default.findIndex(_ => _.tag)
@@ -18,7 +37,8 @@ export default Vue.component('tab-switcher', {
},
methods: {
activateTab (index, dataset) {
- return () => {
+ return (e) => {
+ e.preventDefault()
if (typeof this.onSwitch === 'function') {
this.onSwitch.call(null, index, this.$slots.default[index].elm.dataset)
}
@@ -85,7 +105,7 @@ export default Vue.component('tab-switcher', {
<div class="tabs">
{tabs}
</div>
- <div class="contents">
+ <div class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}>
{contents}
</div>
</div>
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index 4eeb42e0..3e5eacd5 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -1,10 +1,21 @@
@import '../../_variables.scss';
.tab-switcher {
+ display: flex;
+ flex-direction: column;
+
.contents {
+ flex: 1 0 auto;
+ min-height: 0px;
+
.hidden {
display: none;
}
+
+ &.scrollable-tabs {
+ flex-basis: 0;
+ overflow-y: auto;
+ }
}
.tabs {
display: flex;
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 60a3e284..13f7168f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -106,8 +106,13 @@
"expired": "Poll ended {0} ago",
"not_enough_options": "Too few unique options in poll"
},
- "stickers": {
- "add_sticker": "Add Sticker"
+ "emoji": {
+ "stickers": "Stickers",
+ "emoji": "Emoji",
+ "search_emoji": "Search for an emoji",
+ "add_emoji": "Insert emoji",
+ "custom": "Custom emoji",
+ "unicode": "Unicode emoji"
},
"interactions": {
"favs_repeats": "Repeats and Favorites",