581a0fcd00
* fix multiple compose autosuggest dropdowns Fixes #170 * fix failing test * remove console.log
162 lines
5.9 KiB
HTML
162 lines
5.9 KiB
HTML
<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 { insertUsername } from '../../_actions/compose'
|
|
import { insertEmojiAtPosition } from '../../_actions/emoji'
|
|
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
|
import { once } from '../../_utils/once'
|
|
import ComposeAutosuggestionList from './ComposeAutosuggestionList.html'
|
|
import {
|
|
searchAccountsByUsername as searchAccountsByUsernameInDatabase
|
|
} from '../../_database/accountsAndRelationships'
|
|
|
|
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(() => {
|
|
let { composeSelectionStart } = this.get() || {} // TODO: wtf svelte?
|
|
this.set({composeSelectionStartDeferred: composeSelectionStart})
|
|
})
|
|
})
|
|
this.observe('composeFocused', (composeFocused) => {
|
|
let updateFocusedState = () => {
|
|
scheduleIdleTask(() => {
|
|
let { composeFocused } = this.get() || {} // TODO: wtf svelte?
|
|
this.set({composeFocusedDeferred: 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()
|
|
let { composeSelectionStart, composeAutosuggestionSearchText } = this.store.get()
|
|
let startIndex = composeSelectionStart - composeAutosuggestionSearchText.length
|
|
let endIndex = composeSelectionStart
|
|
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()
|
|
let results = await searchAccountsByUsernameInDatabase(
|
|
currentInstance, searchText, DATABASE_SEARCH_RESULTS_LIMIT)
|
|
return results.slice(0, SEARCH_RESULTS_LIMIT)
|
|
},
|
|
searchEmoji (searchText) {
|
|
searchText = searchText.toLowerCase().substring(1)
|
|
let { currentCustomEmoji } = this.store.get()
|
|
let results = currentCustomEmoji.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,
|
|
thisComposeFocused: (composeFocusedDeferred, realm) => composeFocusedDeferred === realm,
|
|
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: (thisComposeFocused, searchText, searchResults) => {
|
|
return !!(thisComposeFocused &&
|
|
searchText &&
|
|
searchResults.length)
|
|
}
|
|
},
|
|
store: () => store,
|
|
components: {
|
|
ComposeAutosuggestionList
|
|
}
|
|
}
|
|
</script> |