aboutsummaryrefslogtreecommitdiff
path: root/src/components/emoji-input
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/emoji-input')
-rw-r--r--src/components/emoji-input/emoji-input.js250
-rw-r--r--src/components/emoji-input/emoji-input.vue117
-rw-r--r--src/components/emoji-input/suggestor.js94
3 files changed, 0 insertions, 461 deletions
diff --git a/src/components/emoji-input/emoji-input.js b/src/components/emoji-input/emoji-input.js
deleted file mode 100644
index fab64a69..00000000
--- a/src/components/emoji-input/emoji-input.js
+++ /dev/null
@@ -1,250 +0,0 @@
-import Completion from '../../services/completion/completion.js'
-import { take } from 'lodash'
-
-/**
- * EmojiInput - augmented inputs for emoji and autocomplete support in inputs
- * without having to give up the comfort of <input/> and <textarea/> elements
- *
- * Intended usage is:
- * <EmojiInput v-model="something">
- * <input v-model="something"/>
- * </EmojiInput>
- *
- * Works only with <input> and <textarea>. Intended to use with only one nested
- * input. It will find first input or textarea and work with that, multiple
- * nested children not tested. You HAVE TO duplicate v-model for both
- * <emoji-input> and <input>/<textarea> otherwise it will not work.
- *
- * Be prepared for CSS troubles though because it still wraps component in a div
- * while TRYING to make it look like nothing happened, but it could break stuff.
- */
-
-const EmojiInput = {
- props: {
- suggest: {
- /**
- * suggest: function (input: String) => Suggestion[]
- *
- * Function that takes input string which takes string (textAtCaret)
- * and returns an array of Suggestions
- *
- * Suggestion is an object containing following properties:
- * displayText: string. Main display text, what actual suggestion
- * represents (user's screen name/emoji shortcode)
- * replacement: string. Text that should replace the textAtCaret
- * detailText: string, optional. Subtitle text, providing additional info
- * if present (user's nickname)
- * imageUrl: string, optional. Image to display alongside with suggestion,
- * currently if no image is provided, replacement will be used (for
- * unicode emojis)
- *
- * TODO: make it asynchronous when adding proper server-provided user
- * suggestions
- *
- * For commonly used suggestors (emoji, users, both) use suggestor.js
- */
- required: true,
- type: Function
- },
- value: {
- /**
- * Used for v-model
- */
- required: true,
- type: String
- }
- },
- data () {
- return {
- input: undefined,
- highlighted: 0,
- caret: 0,
- focused: false,
- blurTimeout: null
- }
- },
- computed: {
- suggestions () {
- const firstchar = this.textAtCaret.charAt(0)
- if (this.textAtCaret === firstchar) { return [] }
- const matchedSuggestions = this.suggest(this.textAtCaret)
- if (matchedSuggestions.length <= 0) {
- return []
- }
- return take(matchedSuggestions, 5)
- .map(({ imageUrl, ...rest }, index) => ({
- ...rest,
- // eslint-disable-next-line camelcase
- img: imageUrl || '',
- highlighted: index === this.highlighted
- }))
- },
- showPopup () {
- return this.focused && this.suggestions && this.suggestions.length > 0
- },
- textAtCaret () {
- return (this.wordAtCaret || {}).word || ''
- },
- wordAtCaret () {
- if (this.value && this.caret) {
- const word = Completion.wordAtPosition(this.value, this.caret - 1) || {}
- return word
- }
- }
- },
- mounted () {
- const slots = this.$slots.default
- if (!slots || slots.length === 0) return
- const input = slots.find(slot => ['input', 'textarea'].includes(slot.tag))
- if (!input) return
- this.input = input
- this.resize()
- input.elm.addEventListener('blur', this.onBlur)
- input.elm.addEventListener('focus', this.onFocus)
- input.elm.addEventListener('paste', this.onPaste)
- input.elm.addEventListener('keyup', this.onKeyUp)
- input.elm.addEventListener('keydown', this.onKeyDown)
- input.elm.addEventListener('transitionend', this.onTransition)
- input.elm.addEventListener('compositionupdate', this.onCompositionUpdate)
- },
- unmounted () {
- const { input } = this
- if (input) {
- input.elm.removeEventListener('blur', this.onBlur)
- input.elm.removeEventListener('focus', this.onFocus)
- input.elm.removeEventListener('paste', this.onPaste)
- input.elm.removeEventListener('keyup', this.onKeyUp)
- input.elm.removeEventListener('keydown', this.onKeyDown)
- input.elm.removeEventListener('transitionend', this.onTransition)
- input.elm.removeEventListener('compositionupdate', this.onCompositionUpdate)
- }
- },
- methods: {
- replace (replacement) {
- const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
- this.$emit('input', newValue)
- this.caret = 0
- },
- replaceText (e, suggestion) {
- const len = this.suggestions.length || 0
- if (this.textAtCaret.length === 1) { return }
- if (len > 0 || suggestion) {
- const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
- const replacement = chosenSuggestion.replacement
- const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
- this.$emit('input', newValue)
- this.highlighted = 0
- const position = this.wordAtCaret.start + replacement.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
- })
- e.preventDefault()
- }
- },
- cycleBackward (e) {
- const len = this.suggestions.length || 0
- if (len > 0) {
- this.highlighted -= 1
- if (this.highlighted < 0) {
- this.highlighted = this.suggestions.length - 1
- }
- e.preventDefault()
- } else {
- this.highlighted = 0
- }
- },
- cycleForward (e) {
- const len = this.suggestions.length || 0
- if (len > 0) {
- this.highlighted += 1
- if (this.highlighted >= len) {
- this.highlighted = 0
- }
- e.preventDefault()
- } else {
- this.highlighted = 0
- }
- },
- onTransition (e) {
- this.resize()
- },
- onBlur (e) {
- // Clicking on any suggestion removes focus from autocomplete,
- // preventing click handler ever executing.
- this.blurTimeout = setTimeout(() => {
- this.focused = false
- this.setCaret(e)
- this.resize()
- }, 200)
- },
- onClick (e, suggestion) {
- this.replaceText(e, suggestion)
- },
- onFocus (e) {
- if (this.blurTimeout) {
- clearTimeout(this.blurTimeout)
- this.blurTimeout = null
- }
-
- this.focused = true
- this.setCaret(e)
- this.resize()
- },
- onKeyUp (e) {
- this.setCaret(e)
- this.resize()
- },
- onPaste (e) {
- this.setCaret(e)
- this.resize()
- },
- onKeyDown (e) {
- this.setCaret(e)
- this.resize()
-
- const { ctrlKey, shiftKey, key } = e
- if (key === 'Tab') {
- if (shiftKey) {
- this.cycleBackward(e)
- } else {
- this.cycleForward(e)
- }
- }
- if (key === 'ArrowUp') {
- this.cycleBackward(e)
- } else if (key === 'ArrowDown') {
- this.cycleForward(e)
- }
- if (key === 'Enter') {
- if (!ctrlKey) {
- this.replaceText(e)
- }
- }
- },
- onInput (e) {
- this.setCaret(e)
- this.$emit('input', e.target.value)
- },
- onCompositionUpdate (e) {
- this.setCaret(e)
- this.resize()
- this.$emit('input', e.target.value)
- },
- setCaret ({ target: { selectionStart } }) {
- this.caret = selectionStart
- },
- resize () {
- const { panel } = this.$refs
- if (!panel) return
- const { offsetHeight, offsetTop } = this.input.elm
- this.$refs.panel.style.top = (offsetTop + offsetHeight) + 'px'
- }
- }
-}
-
-export default EmojiInput
diff --git a/src/components/emoji-input/emoji-input.vue b/src/components/emoji-input/emoji-input.vue
deleted file mode 100644
index 48739ec8..00000000
--- a/src/components/emoji-input/emoji-input.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<template>
- <div class="emoji-input">
- <slot />
- <div
- ref="panel"
- class="autocomplete-panel"
- :class="{ hide: !showPopup }"
- >
- <div class="autocomplete-panel-body">
- <div
- v-for="(suggestion, index) in suggestions"
- :key="index"
- class="autocomplete-item"
- :class="{ highlighted: suggestion.highlighted }"
- @click.stop.prevent="onClick($event, suggestion)"
- >
- <span class="image">
- <img
- v-if="suggestion.img"
- :src="suggestion.img"
- >
- <span v-else>{{ suggestion.replacement }}</span>
- </span>
- <div class="label">
- <span class="displayText">{{ suggestion.displayText }}</span>
- <span class="detailText">{{ suggestion.detailText }}</span>
- </div>
- </div>
- </div>
- </div>
- </div>
-</template>
-
-<script src="./emoji-input.js"></script>
-
-<style lang="scss">
-@import '../../_variables.scss';
-
-.emoji-input {
- display: flex;
- flex-direction: column;
-
- .autocomplete {
- &-panel {
- position: absolute;
- z-index: 9;
- margin-top: 2px;
-
- &.hide {
- display: none
- }
-
- &-body {
- margin: 0 0.5em 0 0.5em;
- border-radius: $fallback--tooltipRadius;
- border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
- box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
- box-shadow: var(--popupShadow);
- min-width: 75%;
- background: $fallback--bg;
- background: var(--bg, $fallback--bg);
- color: $fallback--lightText;
- color: var(--lightText, $fallback--lightText);
- }
- }
-
- &-item {
- display: flex;
- cursor: pointer;
- padding: 0.2em 0.4em;
- border-bottom: 1px solid rgba(0, 0, 0, 0.4);
- height: 32px;
-
- .image {
- width: 32px;
- height: 32px;
- line-height: 32px;
- text-align: center;
- font-size: 32px;
-
- margin-right: 4px;
-
- img {
- width: 32px;
- height: 32px;
- object-fit: contain;
- }
- }
-
- .label {
- display: flex;
- flex-direction: column;
- justify-content: center;
- margin: 0 0.1em 0 0.2em;
-
- .displayText {
- line-height: 1.5;
- }
-
- .detailText {
- font-size: 9px;
- line-height: 9px;
- }
- }
-
- &.highlighted {
- background-color: $fallback--fg;
- background-color: var(--lightBg, $fallback--fg);
- }
- }
- }
-
- input, textarea {
- flex: 1 0 auto;
- }
-}
-</style>
diff --git a/src/components/emoji-input/suggestor.js b/src/components/emoji-input/suggestor.js
deleted file mode 100644
index aec5c39d..00000000
--- a/src/components/emoji-input/suggestor.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import { debounce } from 'lodash'
-/**
- * suggest - generates a suggestor function to be used by emoji-input
- * data: object providing source information for specific types of suggestions:
- * data.emoji - optional, an array of all emoji available i.e.
- * (state.instance.emoji + state.instance.customEmoji)
- * data.users - optional, an array of all known users
- * updateUsersList - optional, a function to search and append to users
- *
- * Depending on data present one or both (or none) can be present, so if field
- * doesn't support user linking you can just provide only emoji.
- */
-
-const debounceUserSearch = debounce((data, input) => {
- data.updateUsersList(input)
-}, 500, { leading: true, trailing: false })
-
-export default data => input => {
- const firstChar = input[0]
- if (firstChar === ':' && data.emoji) {
- return suggestEmoji(data.emoji)(input)
- }
- if (firstChar === '@' && data.users) {
- return suggestUsers(data)(input)
- }
- return []
-}
-
-export const suggestEmoji = emojis => input => {
- const noPrefix = input.toLowerCase().substr(1)
- return emojis
- .filter(({ displayText }) => displayText.toLowerCase().startsWith(noPrefix))
- .sort((a, b) => {
- let aScore = 0
- let bScore = 0
-
- // Make custom emojis a priority
- aScore += a.imageUrl ? 10 : 0
- bScore += b.imageUrl ? 10 : 0
-
- // Sort alphabetically
- const alphabetically = a.displayText > b.displayText ? 1 : -1
-
- return bScore - aScore + alphabetically
- })
-}
-
-export const suggestUsers = data => input => {
- const noPrefix = input.toLowerCase().substr(1)
- const users = data.users
-
- const newUsers = users.filter(
- user =>
- user.screen_name.toLowerCase().startsWith(noPrefix) ||
- user.name.toLowerCase().startsWith(noPrefix)
-
- /* taking only 20 results so that sorting is a bit cheaper, we display
- * only 5 anyway. could be inaccurate, but we ideally we should query
- * backend anyway
- */
- ).slice(0, 20).sort((a, b) => {
- let aScore = 0
- let bScore = 0
-
- // Matches on screen name (i.e. user@instance) makes a priority
- aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
- bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
-
- // Matches on name takes second priority
- aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
- bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
-
- const diff = (bScore - aScore) * 10
-
- // Then sort alphabetically
- const nameAlphabetically = a.name > b.name ? 1 : -1
- const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
-
- return diff + nameAlphabetically + screenNameAlphabetically
- /* eslint-disable camelcase */
- }).map(({ screen_name, name, profile_image_url_original }) => ({
- displayText: screen_name,
- detailText: name,
- imageUrl: profile_image_url_original,
- replacement: '@' + screen_name + ' '
- }))
-
- // BE search users if there are no matches
- if (newUsers.length === 0 && data.updateUsersList) {
- debounceUserSearch(data, noPrefix)
- }
- return newUsers
- /* eslint-enable camelcase */
-}