Merge branch 'emoji-optimizations' into shigusegubu
* emoji-optimizations: eslint fixed emoji picker showing up beyond viewport start loading emoji when picker is open remove the "textbox grows the 'wrong' way" behavior, replace it with more conditions to scroll to bottom arbitrary limit with option to overcome it emoji picker gradual render moved emoji stuff away from after-store and into users module since we only need emoji after login
This commit is contained in:
commit
d6a7f46480
13 changed files with 211 additions and 73 deletions
|
|
@ -165,6 +165,7 @@ const EmojiInput = {
|
|||
methods: {
|
||||
triggerShowPicker () {
|
||||
this.showPicker = true
|
||||
this.$refs.picker.startEmojiLoad()
|
||||
this.$nextTick(() => {
|
||||
this.scrollIntoView()
|
||||
})
|
||||
|
|
@ -181,6 +182,7 @@ const EmojiInput = {
|
|||
this.showPicker = !this.showPicker
|
||||
if (this.showPicker) {
|
||||
this.scrollIntoView()
|
||||
this.$refs.picker.startEmojiLoad()
|
||||
}
|
||||
},
|
||||
replace (replacement) {
|
||||
|
|
@ -306,6 +308,17 @@ const EmojiInput = {
|
|||
} else {
|
||||
scrollerRef.scrollTop = targetScroll
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
const { offsetHeight } = this.input.elm
|
||||
const { picker } = this.$refs
|
||||
console.log(picker)
|
||||
const pickerBottom = picker.$el.getBoundingClientRect().bottom
|
||||
if (pickerBottom > window.innerHeight) {
|
||||
picker.$el.style.top = 'auto'
|
||||
picker.$el.style.bottom = offsetHeight + 'px'
|
||||
}
|
||||
})
|
||||
},
|
||||
onTransition (e) {
|
||||
this.resize()
|
||||
|
|
@ -419,11 +432,14 @@ const EmojiInput = {
|
|||
this.caret = selectionStart
|
||||
},
|
||||
resize () {
|
||||
const { panel } = this.$refs
|
||||
const { panel, picker } = this.$refs
|
||||
if (!panel) return
|
||||
const { offsetHeight, offsetTop } = this.input.elm
|
||||
this.$refs.panel.style.top = (offsetTop + offsetHeight) + 'px'
|
||||
this.$refs.picker.$el.style.top = (offsetTop + offsetHeight) + 'px'
|
||||
const offsetBottom = offsetTop + offsetHeight
|
||||
|
||||
panel.style.top = offsetBottom + 'px'
|
||||
picker.$el.style.top = offsetBottom + 'px'
|
||||
picker.$el.style.bottom = 'auto'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import { set } from 'vue'
|
||||
|
||||
const LOAD_EMOJI_BY = 50
|
||||
const LOAD_EMOJI_INTERVAL = 100
|
||||
const LOAD_EMOJI_SANE_AMOUNT = 500
|
||||
|
||||
const filterByKeyword = (list, keyword = '') => {
|
||||
return list.filter(x => x.displayText.includes(keyword))
|
||||
|
|
@ -18,7 +23,11 @@ const EmojiPicker = {
|
|||
activeGroup: 'custom',
|
||||
showingStickers: false,
|
||||
groupsScrolledClass: 'scrolled-top',
|
||||
keepOpen: false
|
||||
keepOpen: false,
|
||||
customEmojiBuffer: [],
|
||||
customEmojiTimeout: null,
|
||||
customEmojiCounter: 0,
|
||||
customEmojiLoadAllConfirmed: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
@ -58,6 +67,40 @@ const EmojiPicker = {
|
|||
})
|
||||
})
|
||||
},
|
||||
loadEmojiInsane () {
|
||||
this.customEmojiLoadAllConfirmed = true
|
||||
this.continueEmojiLoad()
|
||||
},
|
||||
loadEmoji () {
|
||||
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
|
||||
const saneLoaded = this.customEmojiBuffer.length >= LOAD_EMOJI_SANE_AMOUNT &&
|
||||
!this.customEmojiLoadAllConfirmed
|
||||
|
||||
if (allLoaded || saneLoaded) {
|
||||
return
|
||||
}
|
||||
|
||||
this.customEmojiBuffer.push(
|
||||
...this.filteredEmoji.slice(
|
||||
this.customEmojiCounter,
|
||||
this.customEmojiCounter + LOAD_EMOJI_BY
|
||||
)
|
||||
)
|
||||
this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
|
||||
this.customEmojiCounter += LOAD_EMOJI_BY
|
||||
},
|
||||
startEmojiLoad () {
|
||||
if (this.customEmojiTimeout) {
|
||||
window.clearTimeout(this.customEmojiTimeout)
|
||||
}
|
||||
|
||||
set(this, 'customEmojiBuffer', [])
|
||||
this.customEmojiCounter = 0
|
||||
this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
|
||||
},
|
||||
continueEmojiLoad () {
|
||||
this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
|
||||
},
|
||||
toggleStickers () {
|
||||
this.showingStickers = !this.showingStickers
|
||||
},
|
||||
|
|
@ -74,6 +117,7 @@ const EmojiPicker = {
|
|||
watch: {
|
||||
keyword () {
|
||||
this.scrolledGroup()
|
||||
this.startEmojiLoad()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -86,15 +130,30 @@ const EmojiPicker = {
|
|||
}
|
||||
return 0
|
||||
},
|
||||
saneAmount () {
|
||||
// for UI
|
||||
return LOAD_EMOJI_SANE_AMOUNT
|
||||
},
|
||||
filteredEmoji () {
|
||||
return filterByKeyword(
|
||||
this.$store.state.instance.customEmoji || [],
|
||||
this.keyword
|
||||
)
|
||||
},
|
||||
askForSanity () {
|
||||
return this.customEmojiBuffer.length >= LOAD_EMOJI_SANE_AMOUNT &&
|
||||
!this.customEmojiLoadAllConfirmed
|
||||
},
|
||||
emojis () {
|
||||
const standardEmojis = this.$store.state.instance.emoji || []
|
||||
const customEmojis = this.$store.state.instance.customEmoji || []
|
||||
const customEmojis = this.customEmojiBuffer
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'custom',
|
||||
text: this.$t('emoji.custom'),
|
||||
icon: 'icon-smile',
|
||||
emojis: filterByKeyword(customEmojis, this.keyword)
|
||||
emojis: customEmojis
|
||||
},
|
||||
{
|
||||
id: 'standard',
|
||||
|
|
|
|||
|
|
@ -6,15 +6,25 @@
|
|||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 320px;
|
||||
margin: 0 !important;
|
||||
z-index: 1;
|
||||
|
||||
.keep-open {
|
||||
.keep-open,
|
||||
.too-many-emoji {
|
||||
padding: 7px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.too-many-emoji {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.keep-open-label {
|
||||
padding: 0 7px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
|
|
@ -24,7 +34,7 @@
|
|||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 0;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0px;
|
||||
}
|
||||
|
||||
|
|
@ -32,12 +42,16 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.emoji-groups {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.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;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.additional-tabs,
|
||||
|
|
@ -68,7 +82,7 @@
|
|||
}
|
||||
|
||||
.sticker-picker {
|
||||
flex: 1 1 0
|
||||
flex: 1 1 auto
|
||||
}
|
||||
|
||||
.stickers,
|
||||
|
|
@ -76,7 +90,7 @@
|
|||
&-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 0;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
|
||||
&.hidden {
|
||||
|
|
@ -90,7 +104,7 @@
|
|||
.emoji {
|
||||
&-search {
|
||||
padding: 5px;
|
||||
flex: 0 0 0;
|
||||
flex: 0 0 auto;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -80,6 +80,20 @@
|
|||
{{ $t('emoji.keep_open') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div
|
||||
v-if="askForSanity"
|
||||
class="too-many-emoji"
|
||||
>
|
||||
<div class="alert warning hint">
|
||||
{{ $t('emoji.load_all_hint', { saneAmount } ) }}
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="loadEmojiInsane"
|
||||
>
|
||||
{{ $t('emoji.load_all', { emojiAmount: filteredEmoji.length } ) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showingStickers"
|
||||
|
|
|
|||
|
|
@ -292,9 +292,6 @@ const PostStatusForm = {
|
|||
const bottomPadding = Number(bottomPaddingStr.substring(0, bottomPaddingStr.length - 2))
|
||||
const vertPadding = topPadding + bottomPadding
|
||||
|
||||
const oldHeightStr = target.style.height || ''
|
||||
const oldHeight = Number(oldHeightStr.substring(0, oldHeightStr.length - 2))
|
||||
|
||||
/* Explanation:
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
|
||||
|
|
@ -331,12 +328,17 @@ const PostStatusForm = {
|
|||
// to find offset relative to scrollable container (scroller)
|
||||
const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
|
||||
|
||||
const textareaSizeChangeDelta = newHeight - oldHeight || 0
|
||||
const isBottomObstructed = scrollerBottomBorder < rootBottomBorder
|
||||
const isRootBiggerThanScroller = scrollerHeight < rootRef.offsetHeight
|
||||
const rootChangeDelta = rootBottomBorder - scrollerBottomBorder
|
||||
const totalDelta = textareaSizeChangeDelta +
|
||||
(isBottomObstructed ? rootChangeDelta : 0)
|
||||
|
||||
// The intention is basically this;
|
||||
// Keep bottom side always visible so that submit button is in view EXCEPT
|
||||
// if root element bigger than scroller and caret isn't at the end, so that
|
||||
// if you scroll up and edit middle of text you won't get scrolled back to bottom
|
||||
const shouldScrollToBottom = isBottomObstructed &&
|
||||
!(isRootBiggerThanScroller &&
|
||||
this.$refs.textarea.selectionStart !== this.$refs.textarea.value.length)
|
||||
const totalDelta = shouldScrollToBottom ? rootChangeDelta : 0
|
||||
const targetScroll = currentScroll + totalDelta
|
||||
|
||||
if (scrollerRef === window) {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ export default {
|
|||
topBarLinkColorLocal: undefined,
|
||||
|
||||
alertErrorColorLocal: undefined,
|
||||
alertWarningColorLocal: undefined,
|
||||
|
||||
badgeOpacityLocal: undefined,
|
||||
badgeNotificationColorLocal: undefined,
|
||||
|
|
@ -147,6 +148,7 @@ export default {
|
|||
btnText: this.btnTextColorLocal,
|
||||
|
||||
alertError: this.alertErrorColorLocal,
|
||||
alertWarning: this.alertWarningColorLocal,
|
||||
badgeNotification: this.badgeNotificationColorLocal,
|
||||
|
||||
faint: this.faintColorLocal,
|
||||
|
|
@ -230,6 +232,7 @@ export default {
|
|||
topBar: hex2rgb(colors.topBar),
|
||||
input: hex2rgb(colors.input),
|
||||
alertError: hex2rgb(colors.alertError),
|
||||
alertWarning: hex2rgb(colors.alertWarning),
|
||||
badgeNotification: hex2rgb(colors.badgeNotification)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -201,6 +201,13 @@
|
|||
:fallback="previewTheme.colors.alertError"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.alertError" />
|
||||
<ColorInput
|
||||
v-model="alertWarningColorLocal"
|
||||
name="alertWarning"
|
||||
:label="$t('settings.style.advanced_colors.alert_warning')"
|
||||
:fallback="previewTheme.colors.alertWarning"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.alertWarning" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue