pinafore/routes/_components/compose/ComposeAutosuggest.html
2018-04-19 21:38:01 -07:00

161 lines
5.8 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,
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>