diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/emoji_input/emoji_input.js | 33 | ||||
| -rw-r--r-- | src/components/emoji_input/emoji_input.vue | 16 | ||||
| -rw-r--r-- | src/components/emoji_picker/emoji_picker.js | 82 | ||||
| -rw-r--r-- | src/components/emoji_picker/emoji_picker.scss | 78 | ||||
| -rw-r--r-- | src/components/emoji_picker/emoji_picker.vue | 60 | ||||
| -rw-r--r-- | src/components/post_status_form/post_status_form.js | 25 | ||||
| -rw-r--r-- | src/components/post_status_form/post_status_form.vue | 26 | ||||
| -rw-r--r-- | src/components/sticker_picker/sticker_picker.js | 4 | ||||
| -rw-r--r-- | src/components/sticker_picker/sticker_picker.vue | 72 | ||||
| -rw-r--r-- | src/components/tab_switcher/tab_switcher.js | 26 | ||||
| -rw-r--r-- | src/components/tab_switcher/tab_switcher.scss | 11 | ||||
| -rw-r--r-- | src/i18n/en.json | 9 |
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", |
