<div class="compose-autosuggest {{shown ? 'shown' : ''}} {{realm === 'dialog' ? 'is-dialog' : ''}}" aria-hidden="true" > <ComposeAutosuggestionList items="{{searchResults}}" on:click="onClick(event)" :type :selected /> </div> <style> .compose-autosuggest { position: absolute; left: 5px; top: 0; pointer-events: none; opacity: 0; transition: opacity 0.1s linear; min-width: 400px; max-width: calc(100vw - 20px); z-index: 7000; } .compose-autosuggest.is-dialog { z-index: 11000; } .compose-autosuggest.shown { pointer-events: auto; opacity: 1; } @media (max-width: 479px) { .compose-autosuggest { /* hack: move this over to the left on mobile so it's easier to see */ transform: translateX(-58px); /* avatar size 48px + 10px padding */ min-width: 0; width: calc(100vw - 20px); } } </style> <script> import { store } from '../../_store/store' import { database } from '../../_database/database' import { insertUsername } from '../../_actions/compose' import { insertEmojiAtPosition } from '../../_actions/emoji' import { scheduleIdleTask } from '../../_utils/scheduleIdleTask' import { once } from '../../_utils/once' import ComposeAutosuggestionList from './ComposeAutosuggestionList.html' const SEARCH_RESULTS_LIMIT = 4 const DATABASE_SEARCH_RESULTS_LIMIT = 30 const MIN_PREFIX_LENGTH = 1 const ACCOUNT_SEARCH_REGEX = new RegExp(`(?:\\s|^)(@\\S{${MIN_PREFIX_LENGTH},})$`) const EMOJI_SEARCH_REGEX = new RegExp(`(?:\\s|^)(:[^:]{${MIN_PREFIX_LENGTH},})$`) export default { oncreate() { // perf improves for input responsiveness this.observe('composeSelectionStart', () => { scheduleIdleTask(() => { this.set({composeSelectionStartDeferred: this.get('composeSelectionStart')}) }) }) this.observe('composeFocused', (composeFocused) => { let updateFocusedState = () => { scheduleIdleTask(() => { this.set({composeFocusedDeferred: this.get('composeFocused')}) }) } // TODO: hack so that when the user clicks the button, and the textarea blurs, // we don't immediately hide the dropdown which would cause the click to get lost if (composeFocused) { updateFocusedState() } else { Promise.race([ new Promise(resolve => setTimeout(resolve, 200)), new Promise(resolve => this.once('autosuggestItemSelected', resolve)) ]).then(updateFocusedState) } }) this.observe('searchText', async searchText => { if (!searchText) { return } let type = searchText.startsWith('@') ? 'account' : 'emoji' let results = (type === 'account') ? await this.searchAccounts(searchText) : await this.searchEmoji(searchText) this.store.set({ composeAutosuggestionSelected: 0, composeAutosuggestionSearchText: searchText, composeAutosuggestionSearchResults: results, composeAutosuggestionType: type, }) }) this.observe('shown', shown => { this.store.set({composeAutosuggestionShown: shown}) }) }, methods: { once: once, onClick(item) { this.fire('autosuggestItemSelected') let realm = this.get('realm') let selectionStart = this.store.get('composeSelectionStart') let searchText = this.store.get('composeAutosuggestionSearchText') let startIndex = selectionStart - searchText.length let endIndex = selectionStart if (item.acct) { /* no await */ insertUsername(realm, item.acct, startIndex, endIndex) } else { /* no await */ insertEmojiAtPosition(realm, item, startIndex, endIndex) } }, async searchAccounts(searchText) { searchText = searchText.substring(1) let currentInstance = this.store.get('currentInstance') let results = await database.searchAccountsByUsername( currentInstance, searchText, DATABASE_SEARCH_RESULTS_LIMIT) return results.slice(0, SEARCH_RESULTS_LIMIT) }, searchEmoji(searchText) { searchText = searchText.toLowerCase().substring(1) let customEmoji = this.store.get('currentCustomEmoji') let results = customEmoji.filter(emoji => emoji.shortcode.toLowerCase().startsWith(searchText)) .sort((a, b) => a.shortcode.toLowerCase() < b.shortcode.toLowerCase() ? -1 : 1) .slice(0, SEARCH_RESULTS_LIMIT) return results } }, computed: { composeSelectionStart: ($composeSelectionStart) => $composeSelectionStart, composeFocused: ($composeFocused) => $composeFocused, searchResults: ($composeAutosuggestionSearchResults) => $composeAutosuggestionSearchResults || [], type: ($composeAutosuggestionType) => $composeAutosuggestionType || 'account', selected: ($composeAutosuggestionSelected) => $composeAutosuggestionSelected || 0, searchText: (text, composeSelectionStartDeferred) => { let selectionStart = composeSelectionStartDeferred || 0 if (!text || selectionStart < MIN_PREFIX_LENGTH) { return } let textUpToCursor = text.substring(0, selectionStart) let match = textUpToCursor.match(ACCOUNT_SEARCH_REGEX) || textUpToCursor.match(EMOJI_SEARCH_REGEX) return match && match[1] }, shown: (composeFocusedDeferred, searchText, searchResults) => { return !!(composeFocusedDeferred && searchText && searchResults.length) } }, store: () => store, components: { ComposeAutosuggestionList } } </script>