pinafore/routes/_components/compose/ComposeAutosuggest.html

128 lines
4.3 KiB
HTML

<div class="compose-autosuggest {{shown ? 'shown' : ''}}"
aria-hidden="true" >
<ComposeAutosuggestionList
items="{{searchResults}}"
on:click="onUserSelected(event)"
:selected
/>
</div>
<style>
.compose-autosuggest {
opacity: 0;
pointer-events: none;
transition: opacity 0.1s linear;
min-width: 400px;
max-width: calc(100vw - 20px);
}
.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 */
width: calc(100vw - 20px);
}
}
</style>
<script>
import { store } from '../../_store/store'
import { database } from '../../_database/database'
import { insertUsername } from '../../_actions/compose'
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 SEARCH_REGEX = new RegExp(`(?:\\s|^)(@\\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('userSelected', resolve))
]).then(updateFocusedState)
}
})
this.observe('searchText', async searchText => {
if (!searchText) {
return
}
let results = await this.search(searchText)
this.store.set({
composeAutosuggestionSelected: 0,
composeAutosuggestionSearchText: searchText,
composeAutosuggestionSearchResults: results
})
})
this.observe('shown', shown => {
this.store.set({composeAutosuggestionShown: shown})
})
},
methods: {
once: once,
onUserSelected(account) {
this.fire('userSelected')
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
/* no await */ insertUsername(realm, account.acct, startIndex, endIndex)
},
async search(searchText) {
let currentInstance = this.store.get('currentInstance')
let results = await database.searchAccountsByUsername(
currentInstance, searchText.substring(1), DATABASE_SEARCH_RESULTS_LIMIT)
return results.slice(0, SEARCH_RESULTS_LIMIT)
}
},
computed: {
composeSelectionStart: ($composeSelectionStart) => $composeSelectionStart,
composeFocused: ($composeFocused) => $composeFocused,
searchResults: ($composeAutosuggestionSearchResults) => $composeAutosuggestionSearchResults || [],
selected: ($composeAutosuggestionSelected) => $composeAutosuggestionSelected || 0,
searchText: (text, composeSelectionStartDeferred) => {
let selectionStart = composeSelectionStartDeferred || 0
if (!text || selectionStart < MIN_PREFIX_LENGTH) {
return
}
let match = text.substring(0, selectionStart).match(SEARCH_REGEX)
return match && match[1]
},
shown: (composeFocusedDeferred, searchText, searchResults) => {
return !!(composeFocusedDeferred &&
searchText &&
searchResults.length)
}
},
store: () => store,
components: {
ComposeAutosuggestionList
}
}
</script>