commit
19cb98b85f
@ -0,0 +1,13 @@
|
|||||||
|
# Changelog
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
### Added
|
||||||
|
- Emoji picker
|
||||||
|
- Started changelog anew
|
||||||
|
### Changed
|
||||||
|
- changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes
|
||||||
|
### Fixed
|
||||||
|
- improved hotkey behavior on autocomplete popup
|
After Width: | Height: | Size: 491 B |
@ -0,0 +1,115 @@
|
|||||||
|
|
||||||
|
const filterByKeyword = (list, keyword = '') => {
|
||||||
|
return list.filter(x => x.displayText.includes(keyword))
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmojiPicker = {
|
||||||
|
props: {
|
||||||
|
enableStickerPicker: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
labelKey: String(Math.random() * 100000),
|
||||||
|
keyword: '',
|
||||||
|
activeGroup: 'custom',
|
||||||
|
showingStickers: false,
|
||||||
|
groupsScrolledClass: 'scrolled-top',
|
||||||
|
keepOpen: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
StickerPicker: () => import('../sticker_picker/sticker_picker.vue')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onEmoji (emoji) {
|
||||||
|
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
|
||||||
|
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
|
||||||
|
},
|
||||||
|
highlight (key) {
|
||||||
|
const ref = this.$refs['group-' + key]
|
||||||
|
const top = ref[0].offsetTop
|
||||||
|
this.setShowStickers(false)
|
||||||
|
this.activeGroup = key
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs['emoji-groups'].scrollTop = top + 1
|
||||||
|
})
|
||||||
|
},
|
||||||
|
scrolledGroup (e) {
|
||||||
|
const target = (e && e.target) || this.$refs['emoji-groups']
|
||||||
|
const top = target.scrollTop + 5
|
||||||
|
if (target.scrollTop <= 5) {
|
||||||
|
this.groupsScrolledClass = 'scrolled-top'
|
||||||
|
} else if (target.scrollTop >= target.scrollTopMax - 5) {
|
||||||
|
this.groupsScrolledClass = 'scrolled-bottom'
|
||||||
|
} else {
|
||||||
|
this.groupsScrolledClass = 'scrolled-middle'
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.emojisView.forEach(group => {
|
||||||
|
const ref = this.$refs['group-' + group.id]
|
||||||
|
if (ref[0].offsetTop <= top) {
|
||||||
|
this.activeGroup = group.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
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 [
|
||||||
|
{
|
||||||
|
id: 'custom',
|
||||||
|
text: this.$t('emoji.custom'),
|
||||||
|
icon: 'icon-smile',
|
||||||
|
emojis: filterByKeyword(customEmojis, this.keyword)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
stickerPickerEnabled () {
|
||||||
|
return (this.$store.state.instance.stickers || []).length !== 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmojiPicker
|
@ -0,0 +1,165 @@
|
|||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.emoji-picker {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 320px;
|
||||||
|
margin: 0 !important;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.keep-open {
|
||||||
|
padding: 7px;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
.keep-open-label {
|
||||||
|
padding: 0 7px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
display: flex;
|
||||||
|
height: 32px;
|
||||||
|
padding: 10px 7px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-tabs {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.additional-tabs {
|
||||||
|
border-left: 1px solid;
|
||||||
|
border-left-color: $fallback--icon;
|
||||||
|
border-left-color: var(--icon, $fallback--icon);
|
||||||
|
padding-left: 7px;
|
||||||
|
flex: 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.additional-tabs,
|
||||||
|
.emoji-tabs {
|
||||||
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
flex-basis: auto;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
padding: 0 7px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 24px;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
border-bottom: 4px solid;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
&-search {
|
||||||
|
padding: 5px;
|
||||||
|
flex: 0 0 0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-groups {
|
||||||
|
flex: 1 1 1px;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
user-select: none;
|
||||||
|
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
|
||||||
|
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
|
||||||
|
linear-gradient(to top, white, white);
|
||||||
|
transition: mask-size 150ms;
|
||||||
|
mask-size: 100% 20px, 100% 20px, auto;
|
||||||
|
// Autoprefixed seem to ignore this one, and also syntax is different
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
&.scrolled {
|
||||||
|
&-top {
|
||||||
|
mask-size: 100% 20px, 100% 0, auto;
|
||||||
|
}
|
||||||
|
&-bottom {
|
||||||
|
mask-size: 100% 0, 100% 20px, auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-left: 5px;
|
||||||
|
justify-content: left;
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
font-size: 12px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
&.disabled {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
font-size: 32px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 4px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-fit: contain;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div class="emoji-picker panel panel-default panel-body">
|
||||||
|
<div class="heading">
|
||||||
|
<span class="emoji-tabs">
|
||||||
|
<span
|
||||||
|
v-for="group in emojis"
|
||||||
|
:key="group.id"
|
||||||
|
class="emoji-tabs-item"
|
||||||
|
:class="{
|
||||||
|
active: activeGroupView === group.id,
|
||||||
|
disabled: group.emojis.length === 0
|
||||||
|
}"
|
||||||
|
:title="group.text"
|
||||||
|
@click.prevent="highlight(group.id)"
|
||||||
|
>
|
||||||
|
<i :class="group.icon" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="stickerPickerEnabled"
|
||||||
|
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="content">
|
||||||
|
<div
|
||||||
|
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
|
||||||
|
ref="emoji-groups"
|
||||||
|
class="emoji-groups"
|
||||||
|
:class="groupsScrolledClass"
|
||||||
|
@scroll="scrolledGroup"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="group in emojisView"
|
||||||
|
:key="group.id"
|
||||||
|
class="emoji-group"
|
||||||
|
>
|
||||||
|
<h6
|
||||||
|
:ref="'group-' + group.id"
|
||||||
|
class="emoji-group-title"
|
||||||
|
>
|
||||||
|
{{ group.text }}
|
||||||
|
</h6>
|
||||||
|
<span
|
||||||
|
v-for="emoji in group.emojis"
|
||||||
|
:key="group.id + emoji.displayText"
|
||||||
|
:title="emoji.displayText"
|
||||||
|
class="emoji-item"
|
||||||
|
@click.stop.prevent="onEmoji(emoji)"
|
||||||
|
>
|
||||||
|
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
:src="emoji.imageUrl"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="keep-open"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="labelKey + 'keep-open'"
|
||||||
|
v-model="keepOpen"
|
||||||
|
type="checkbox"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="keep-open-label"
|
||||||
|
:for="labelKey + 'keep-open'"
|
||||||
|
>
|
||||||
|
<div class="keep-open-label-text">
|
||||||
|
{{ $t('emoji.keep_open') }}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showingStickers"
|
||||||
|
class="stickers-content"
|
||||||
|
>
|
||||||
|
<sticker-picker
|
||||||
|
@uploaded="onStickerUploaded"
|
||||||
|
@upload-failed="onStickerUploadFailed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./emoji_picker.js"></script>
|
||||||
|
<style lang="scss" src="./emoji_picker.scss"></style>
|
@ -0,0 +1,32 @@
|
|||||||
|
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||||
|
|
||||||
|
const PostStatusModal = {
|
||||||
|
components: {
|
||||||
|
PostStatusForm
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isLoggedIn () {
|
||||||
|
return !!this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
isOpen () {
|
||||||
|
return this.isLoggedIn && this.$store.state.postStatus.modalActivated
|
||||||
|
},
|
||||||
|
params () {
|
||||||
|
return this.$store.state.postStatus.params || {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
isOpen (val) {
|
||||||
|
if (val) {
|
||||||
|
this.$nextTick(() => this.$el.querySelector('textarea').focus())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeModal () {
|
||||||
|
this.$store.dispatch('closePostStatusModal')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostStatusModal
|
@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
|
class="post-form-modal-view modal-view"
|
||||||
|
@click="closeModal"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="post-form-modal-panel panel"
|
||||||
|
@click.stop=""
|
||||||
|
>
|
||||||
|
<div class="panel-heading">
|
||||||
|
{{ $t('post_status.new_status') }}
|
||||||
|
</div>
|
||||||
|
<PostStatusForm
|
||||||
|
class="panel-body"
|
||||||
|
v-bind="params"
|
||||||
|
@posted="closeModal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./post_status_modal.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.post-form-modal-view {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-form-modal-panel {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 25%;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 700px;
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
margin-top: 8%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,69 @@
|
|||||||
|
import * as bodyScrollLock from 'body-scroll-lock'
|
||||||
|
|
||||||
|
let previousNavPaddingRight
|
||||||
|
let previousAppBgWrapperRight
|
||||||
|
|
||||||
|
const disableBodyScroll = (el) => {
|
||||||
|
const scrollBarGap = window.innerWidth - document.documentElement.clientWidth
|
||||||
|
bodyScrollLock.disableBodyScroll(el, {
|
||||||
|
reserveScrollBarGap: true
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
// If previousNavPaddingRight is already set, don't set it again.
|
||||||
|
if (previousNavPaddingRight === undefined) {
|
||||||
|
const navEl = document.getElementById('nav')
|
||||||
|
previousNavPaddingRight = window.getComputedStyle(navEl).getPropertyValue('padding-right')
|
||||||
|
navEl.style.paddingRight = previousNavPaddingRight ? `calc(${previousNavPaddingRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
|
||||||
|
}
|
||||||
|
// If previousAppBgWrapeprRight is already set, don't set it again.
|
||||||
|
if (previousAppBgWrapperRight === undefined) {
|
||||||
|
const appBgWrapperEl = document.getElementById('app_bg_wrapper')
|
||||||
|
previousAppBgWrapperRight = window.getComputedStyle(appBgWrapperEl).getPropertyValue('right')
|
||||||
|
appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
|
||||||
|
}
|
||||||
|
document.body.classList.add('scroll-locked')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const enableBodyScroll = (el) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (previousNavPaddingRight !== undefined) {
|
||||||
|
document.getElementById('nav').style.paddingRight = previousNavPaddingRight
|
||||||
|
// Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again.
|
||||||
|
previousNavPaddingRight = undefined
|
||||||
|
}
|
||||||
|
if (previousAppBgWrapperRight !== undefined) {
|
||||||
|
document.getElementById('app_bg_wrapper').style.right = previousAppBgWrapperRight
|
||||||
|
// Restore previousAppBgWrapperRight to undefined so disableBodyScroll knows it can be set again.
|
||||||
|
previousAppBgWrapperRight = undefined
|
||||||
|
}
|
||||||
|
document.body.classList.remove('scroll-locked')
|
||||||
|
})
|
||||||
|
bodyScrollLock.enableBodyScroll(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
const directive = {
|
||||||
|
inserted: (el, binding) => {
|
||||||
|
if (binding.value) {
|
||||||
|
disableBodyScroll(el)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
componentUpdated: (el, binding) => {
|
||||||
|
if (binding.oldValue === binding.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.value) {
|
||||||
|
disableBodyScroll(el)
|
||||||
|
} else {
|
||||||
|
enableBodyScroll(el)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unbind: (el) => {
|
||||||
|
enableBodyScroll(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (Vue) => {
|
||||||
|
Vue.directive('body-scroll-lock', directive)
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
const postStatus = {
|
||||||
|
state: {
|
||||||
|
params: null,
|
||||||
|
modalActivated: false
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
openPostStatusModal (state, params) {
|
||||||
|
state.params = params
|
||||||
|
state.modalActivated = true
|
||||||
|
},
|
||||||
|
closePostStatusModal (state) {
|
||||||
|
state.modalActivated = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
openPostStatusModal ({ commit }, params) {
|
||||||
|
commit('openPostStatusModal', params)
|
||||||
|
},
|
||||||
|
closePostStatusModal ({ commit }) {
|
||||||
|
commit('closePostStatusModal')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default postStatus
|
@ -0,0 +1,31 @@
|
|||||||
|
export const findOffset = (child, parent, { top = 0, left = 0 } = {}, ignorePadding = true) => {
|
||||||
|
const result = {
|
||||||
|
top: top + child.offsetTop,
|
||||||
|
left: left + child.offsetLeft
|
||||||
|
}
|
||||||
|
if (!ignorePadding && child !== window) {
|
||||||
|
const { topPadding, leftPadding } = findPadding(child)
|
||||||
|
result.top += ignorePadding ? 0 : topPadding
|
||||||
|
result.left += ignorePadding ? 0 : leftPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.offsetParent && (parent === window || parent.contains(child.offsetParent) || parent === child.offsetParent)) {
|
||||||
|
return findOffset(child.offsetParent, parent, result, false)
|
||||||
|
} else {
|
||||||
|
if (parent !== window) {
|
||||||
|
const { topPadding, leftPadding } = findPadding(parent)
|
||||||
|
result.top += topPadding
|
||||||
|
result.left += leftPadding
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const findPadding = (el) => {
|
||||||
|
const topPaddingStr = window.getComputedStyle(el)['padding-top']
|
||||||
|
const topPadding = Number(topPaddingStr.substring(0, topPaddingStr.length - 2))
|
||||||
|
const leftPaddingStr = window.getComputedStyle(el)['padding-left']
|
||||||
|
const leftPadding = Number(leftPaddingStr.substring(0, leftPaddingStr.length - 2))
|
||||||
|
|
||||||
|
return { topPadding, leftPadding }
|
||||||
|
}
|
@ -0,0 +1,131 @@
|
|||||||
|
import { shallowMount, createLocalVue } from '@vue/test-utils'
|
||||||
|
import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
|
||||||
|
|
||||||
|
const generateInput = (value, padEmoji = true) => {
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
localVue.directive('click-outside', () => {})
|
||||||
|
const wrapper = shallowMount(EmojiInput, {
|
||||||
|
propsData: {
|
||||||
|
suggest: () => [],
|
||||||
|
enableEmojiPicker: true,
|
||||||
|
value
|
||||||
|
},
|
||||||
|
mocks: {
|
||||||
|
$store: {
|
||||||
|
state: {
|
||||||
|
config: {
|
||||||
|
padEmoji
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: '<input />'
|
||||||
|
},
|
||||||
|
localVue
|
||||||
|
})
|
||||||
|
return [wrapper, localVue]
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('EmojiInput', () => {
|
||||||
|
describe('insertion mechanism', () => {
|
||||||
|
it('inserts string at the end with trailing space', () => {
|
||||||
|
const initialString = 'Testing'
|
||||||
|
const [wrapper] = generateInput(initialString)
|
||||||
|
const input = wrapper.find('input')
|
||||||
|
input.setValue(initialString)
|
||||||
|
wrapper.setData({ caret: initialString.length })
|
||||||
|
wrapper.vm.insert({ insertion: '(test)', keepOpen: false })
|
||||||
|
expect(wrapper.emitted().input[0][0]).to.eql('Testing (test) ')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('inserts string at the end with trailing space (source has a trailing space)', () => {
|
||||||
|
const initialString = 'Testing '
|
||||||
|
const [wrapper] = generateInput(initialString)
|
||||||
|
const input = wrapper.find('input')
|
||||||
|
input.setValue(initialString)
|
||||||
|
wrapper.setData({ caret: initialString.length })
|
||||||
|
wrapper.vm.insert({ insertion: '(test)', keepOpen: false })
|
||||||
|
expect(wrapper.emitted().input[0][0]).to.eql('Testing (test) ')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('inserts string at the begginning without leading space', () => {
|
||||||
|
const initialString = 'Testing'
|
||||||
|
const [wrapper] = generateInput(initialString)
|
||||||
|
const input = wrapper.find('input')
|
||||||
|
input.setValue(initialString)
|
||||||
|
wrapper.setData({ caret: 0 })
|
||||||
|
wrapper.vm.insert({ insertion: '(test)', keepOpen: false })
|
||||||
|
expect(wrapper.emitted().input[0][0]).to.eql('(test) Testing')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('inserts string between words without creating extra spaces', () => {
|
||||||
|
const initialString = 'Spurdo Sparde'
|
||||||
|
const [wrapper] = generateInput(initialString)
|
||||||
|
const input = wrapper.find('input')
|
||||||
|
input.setValue(initialString)
|
||||||
|
wrapper.setData({ caret: 6 })
|
||||||
|
wrapper.vm.insert({ insertion: ':ebin:', keepOpen: false })
|
||||||
|
expect(wrapper.emitted().input[0][0]).to.eql('Spurdo :ebin: Sparde')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('inserts string between words without creating extra spaces (other caret)', () => {
|
||||||
|
const initialString = 'Spurdo Sparde'
|
||||||
|
const [wrapper] = generateInput(initialString)
|
||||||
|
const input = wrapper.find('input')
|
||||||
|
input.setValue(initialString)
|
||||||
|
wrapper.setData({ caret: 7 })
|
||||||
|
wrapper.vm.insert({ insertion: ':ebin:', keepOpen: false })
|
||||||
|
expect(wrapper.emitted().input[0][0]).to.eql('Spurdo :ebin: Sparde')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('inserts string without any padding if padEmoji setting is set to false', () => {
|
||||||
|
const initialString = 'Eat some spam!'
|
||||||
|
const [wrapper] = generateInput(initialString, false)
|
||||||
|
const input = wrapper.find('input')
|
||||||
|
input.setValue(initialString)
|
||||||
|
wrapper.setData({ caret: initialString.length, keepOpen: false })
|
||||||
|
wrapper.vm.insert({ insertion: ':spam:' })
|
||||||
|
expect(wrapper.emitted().input[0][0]).to.eql('Eat some spam!:spam:')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('correctly sets caret after insertion at beginning', (done) => {
|
||||||
|
const initialString = '1234'
|
||||||
|
const [wrapper, vue] = generateInput(initialString)
|
||||||
|
const input = wrapper.find('input')
|
||||||
|
input.setValue(initialString)
|
||||||
|
wrapper.setData({ caret: 0 })
|
||||||
|
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
|
||||||
|
vue.nextTick(() => {
|
||||||
|
expect(wrapper.vm.caret).to.eql(5)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('correctly sets caret after insertion at end', (done) => {
|
||||||
|
const initialString = '1234'
|
||||||
|
const [wrapper, vue] = generateInput(initialString)
|
||||||
|
const input = wrapper.find('input')
|
||||||
|
input.setValue(initialString)
|
||||||
|
wrapper.setData({ caret: initialString.length })
|
||||||
|
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
|
||||||
|
vue.nextTick(() => {
|
||||||
|
expect(wrapper.vm.caret).to.eql(10)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('correctly sets caret after insertion if padEmoji setting is set to false', (done) => {
|
||||||
|
const initialString = '1234'
|
||||||
|
const [wrapper, vue] = generateInput(initialString, false)
|
||||||
|
const input = wrapper.find('input')
|
||||||
|
input.setValue(initialString)
|
||||||
|
wrapper.setData({ caret: initialString.length })
|
||||||
|
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
|
||||||
|
vue.nextTick(() => {
|
||||||
|
expect(wrapper.vm.caret).to.eql(8)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in new issue