pinafore/routes/_components/compose/ComposeAutosuggest.html

159 lines
5.7 KiB
HTML
Raw Normal View History

<div class="compose-autosuggest {{shown ? 'shown' : ''}} {{realm === 'dialog' ? 'is-dialog' : ''}}"
aria-hidden="true" >
2018-03-25 03:04:54 +02:00
<ComposeAutosuggestionList
items="{{searchResults}}"
2018-03-25 21:24:38 +02:00
on:click="onClick(event)"
:type
2018-03-25 03:04:54 +02:00
:selected
/>
</div>
<style>
.compose-autosuggest {
position: absolute;
left: 5px;
top: 0;
2018-03-25 03:04:54 +02:00
pointer-events: none;
opacity: 0;
2018-03-25 03:04:54 +02:00
transition: opacity 0.1s linear;
min-width: 400px;
max-width: calc(100vw - 20px);
z-index: 7000;
}
.compose-autosuggest.is-dialog {
z-index: 11000;
2018-03-25 03:04:54 +02:00
}
.compose-autosuggest.shown {
pointer-events: auto;
opacity: 1;
2018-03-25 03:04:54 +02:00
}
@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;
2018-03-25 03:04:54 +02:00
width: calc(100vw - 20px);
}
}
</style>
<script>
import { store } from '../../_store/store'
import { database } from '../../_database/database'
import { insertUsername } from '../../_actions/compose'
2018-03-25 21:24:38 +02:00
import { insertEmojiAtPosition } from '../../_actions/emoji'
2018-03-25 03:04:54 +02:00
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
2018-03-25 21:24:38 +02:00
const ACCOUNT_SEARCH_REGEX = new RegExp(`(?:\\s|^)(@\\S{${MIN_PREFIX_LENGTH},})$`)
const EMOJI_SEARCH_REGEX = new RegExp(`(?:\\s|^)(:[^:]{${MIN_PREFIX_LENGTH},})$`)
2018-03-25 03:04:54 +02:00
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)),
2018-03-25 21:24:38 +02:00
new Promise(resolve => this.once('autosuggestItemSelected', resolve))
2018-03-25 03:04:54 +02:00
]).then(updateFocusedState)
}
})
this.observe('searchText', async searchText => {
if (!searchText) {
return
}
2018-03-25 21:24:38 +02:00
let type = searchText.startsWith('@') ? 'account' : 'emoji'
let results = (type === 'account')
? await this.searchAccounts(searchText)
: await this.searchEmoji(searchText)
2018-03-25 03:04:54 +02:00
this.store.set({
composeAutosuggestionSelected: 0,
composeAutosuggestionSearchText: searchText,
2018-03-25 21:24:38 +02:00
composeAutosuggestionSearchResults: results,
composeAutosuggestionType: type,
2018-03-25 03:04:54 +02:00
})
})
this.observe('shown', shown => {
this.store.set({composeAutosuggestionShown: shown})
})
},
methods: {
once: once,
2018-03-25 21:24:38 +02:00
onClick(item) {
this.fire('autosuggestItemSelected')
2018-03-25 03:04:54 +02:00
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
2018-03-25 21:24:38 +02:00
if (item.acct) {
/* no await */ insertUsername(realm, item.acct, startIndex, endIndex)
} else {
/* no await */ insertEmojiAtPosition(realm, item, startIndex, endIndex)
}
2018-03-25 03:04:54 +02:00
},
2018-03-25 21:24:38 +02:00
async searchAccounts(searchText) {
searchText = searchText.substring(1)
2018-03-25 03:04:54 +02:00
let currentInstance = this.store.get('currentInstance')
let results = await database.searchAccountsByUsername(
2018-03-25 21:24:38 +02:00
currentInstance, searchText, DATABASE_SEARCH_RESULTS_LIMIT)
2018-03-25 03:04:54 +02:00
return results.slice(0, SEARCH_RESULTS_LIMIT)
2018-03-25 21:24:38 +02:00
},
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
2018-03-25 03:04:54 +02:00
}
},
computed: {
composeSelectionStart: ($composeSelectionStart) => $composeSelectionStart,
composeFocused: ($composeFocused) => $composeFocused,
searchResults: ($composeAutosuggestionSearchResults) => $composeAutosuggestionSearchResults || [],
2018-03-25 21:24:38 +02:00
type: ($composeAutosuggestionType) => $composeAutosuggestionType || 'account',
2018-03-25 03:04:54 +02:00
selected: ($composeAutosuggestionSelected) => $composeAutosuggestionSelected || 0,
searchText: (text, composeSelectionStartDeferred) => {
let selectionStart = composeSelectionStartDeferred || 0
if (!text || selectionStart < MIN_PREFIX_LENGTH) {
return
}
2018-03-25 21:24:38 +02:00
let textUpToCursor = text.substring(0, selectionStart)
let match = textUpToCursor.match(ACCOUNT_SEARCH_REGEX) || textUpToCursor.match(EMOJI_SEARCH_REGEX)
2018-03-25 03:04:54 +02:00
return match && match[1]
},
shown: (composeFocusedDeferred, searchText, searchResults) => {
return !!(composeFocusedDeferred &&
searchText &&
searchResults.length)
}
},
store: () => store,
components: {
ComposeAutosuggestionList
}
}
</script>