forked from cybrespace/pinafore
		
	
		
			
				
	
	
		
			159 lines
		
	
	
		
			No EOL
		
	
	
		
			5.7 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			159 lines
		
	
	
		
			No EOL
		
	
	
		
			5.7 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 { 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> |