204 lines
		
	
	
	
		
			6.1 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			204 lines
		
	
	
	
		
			6.1 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
| <textarea
 | |
|   id="the-compose-box-input-{realm}"
 | |
|   class="compose-box-input"
 | |
|   placeholder="What's on your mind?"
 | |
|   ref:textarea
 | |
|   bind:value=rawText
 | |
|   on:blur="onBlur()"
 | |
|   on:focus="onFocus()"
 | |
|   on:selectionChange="onSelectionChange(event)"
 | |
|   on:keydown="onKeydown(event)"
 | |
| ></textarea>
 | |
| <label for="the-compose-box-input-{realm}" class="sr-only">
 | |
|   What's on your mind?
 | |
| </label>
 | |
| <style>
 | |
|   .compose-box-input {
 | |
|     grid-area: input;
 | |
|     margin: 10px 0 0 5px;
 | |
|     padding: 10px;
 | |
|     border: 1px solid var(--input-border);
 | |
|     min-height: 75px;
 | |
|     resize: none;
 | |
|     overflow: hidden;
 | |
|     word-wrap: break-word;
 | |
|     /* Text must be at least 16px or else iOS Safari zooms in */
 | |
|     font-size: 1.2em;
 | |
|     /* Hack to make Edge stretch the element all the way to the right.
 | |
|      * Also desktop Safari makes the gauge stretch too far to the right without it.
 | |
|      */
 | |
|     width: calc(100% - 5px);
 | |
|   }
 | |
| </style>
 | |
| <script>
 | |
|   import { store } from '../../_store/store'
 | |
|   import { autosize } from '../../_thirdparty/autosize/autosize'
 | |
|   import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
 | |
|   import debounce from 'lodash-es/debounce'
 | |
|   import { mark, stop } from '../../_utils/marks'
 | |
|   import { selectionChange } from '../../_utils/events'
 | |
|   import {
 | |
|     clickSelectedAutosuggestionUsername,
 | |
|     clickSelectedAutosuggestionEmoji
 | |
|   } from '../../_actions/autosuggest'
 | |
|   import { observe } from 'svelte-extras'
 | |
| 
 | |
|   export default {
 | |
|     oncreate () {
 | |
|       this.setupSyncFromStore()
 | |
|       this.setupSyncToStore()
 | |
|       this.setupAutosize()
 | |
|     },
 | |
|     ondestroy () {
 | |
|       this.teardownAutosize()
 | |
|     },
 | |
|     methods: {
 | |
|       observe,
 | |
|       setupSyncFromStore () {
 | |
|         let textarea = this.refs.textarea
 | |
|         let firstTime = true
 | |
|         this.observe('text', text => {
 | |
|           let { rawText } = this.get()
 | |
|           if (rawText !== text) {
 | |
|             this.set({ rawText: text })
 | |
|             // this next autosize is required to resize after
 | |
|             // the user clicks the "toot" button
 | |
|             mark('autosize.update()')
 | |
|             autosize.update(textarea)
 | |
|             stop('autosize.update()')
 | |
|           }
 | |
|           if (firstTime) {
 | |
|             firstTime = false
 | |
|             let { autoFocus } = this.get()
 | |
|             if (autoFocus) {
 | |
|               requestAnimationFrame(() => textarea.focus())
 | |
|             }
 | |
|           }
 | |
|         })
 | |
|       },
 | |
|       setupSyncToStore () {
 | |
|         const saveStore = debounce(() => scheduleIdleTask(() => this.store.save()), 1000)
 | |
| 
 | |
|         this.observe('rawText', rawText => {
 | |
|           mark('observe rawText')
 | |
|           let { realm } = this.get()
 | |
|           this.store.setComposeData(realm, { text: rawText })
 | |
|           saveStore()
 | |
|           stop('observe rawText')
 | |
|         }, { init: false })
 | |
|       },
 | |
|       setupAutosize () {
 | |
|         let textarea = this.refs.textarea
 | |
|         requestAnimationFrame(() => {
 | |
|           mark('autosize()')
 | |
|           autosize(textarea)
 | |
|           stop('autosize()')
 | |
|         })
 | |
|       },
 | |
|       teardownAutosize () {
 | |
|         mark('autosize.destroy()')
 | |
|         autosize.destroy(this.refs.textarea)
 | |
|         stop('autosize.destroy()')
 | |
|       },
 | |
|       onBlur () {
 | |
|         scheduleIdleTask(() => {
 | |
|           this.store.setForCurrentAutosuggest({ composeFocused: false })
 | |
|         })
 | |
|       },
 | |
|       onFocus () {
 | |
|         scheduleIdleTask(() => {
 | |
|           let { realm } = this.get()
 | |
|           this.store.set({ currentComposeRealm: realm })
 | |
|           this.store.setForCurrentAutosuggest({ composeFocused: true })
 | |
|         })
 | |
|       },
 | |
|       onSelectionChange (selectionStart) {
 | |
|         scheduleIdleTask(() => {
 | |
|           this.store.setForCurrentAutosuggest({ composeSelectionStart: selectionStart })
 | |
|         })
 | |
|       },
 | |
|       onKeydown (e) {
 | |
|         let { keyCode } = e
 | |
|         // ctrl or cmd (on macs) was pressed; ctrl-enter means post a toot
 | |
|         const ctrlPressed = e.getModifierState('Control') || e.getModifierState('Meta')
 | |
|         switch (keyCode) {
 | |
|           case 9: // tab
 | |
|             this.clickSelectedAutosuggestion(e)
 | |
|             break
 | |
|           case 13: // enter
 | |
|             const autosuggestionClicked = this.clickSelectedAutosuggestion(e)
 | |
|             if (!autosuggestionClicked && ctrlPressed) {
 | |
|               this.fire('postAction')
 | |
|             }
 | |
|             break
 | |
|           case 38: // up
 | |
|             this.incrementAutosuggestSelected(-1, e)
 | |
|             break
 | |
|           case 40: // down
 | |
|             this.incrementAutosuggestSelected(1, e)
 | |
|             break
 | |
|           case 27: // escape
 | |
|             this.clearAutosuggestions(e)
 | |
|             break
 | |
|           default:
 | |
|         }
 | |
|       },
 | |
|       clickSelectedAutosuggestion (event) {
 | |
|         let {
 | |
|           autosuggestShown,
 | |
|           autosuggestType
 | |
|         } = this.store.get()
 | |
|         if (!autosuggestShown) {
 | |
|           return false
 | |
|         }
 | |
|         let { realm } = this.get()
 | |
|         if (autosuggestType === 'account') {
 | |
|           /* no await */ clickSelectedAutosuggestionUsername(realm)
 | |
|         } else { // emoji
 | |
|           /* no await */ clickSelectedAutosuggestionEmoji(realm)
 | |
|         }
 | |
|         event.preventDefault()
 | |
|         event.stopPropagation()
 | |
|         return true
 | |
|       },
 | |
|       incrementAutosuggestSelected (increment, event) {
 | |
|         let {
 | |
|           autosuggestShown,
 | |
|           autosuggestSelected,
 | |
|           autosuggestSearchResults
 | |
|         } = this.store.get()
 | |
|         if (!autosuggestShown) {
 | |
|           return
 | |
|         }
 | |
|         autosuggestSelected += increment
 | |
|         if (autosuggestSelected >= 0) {
 | |
|           autosuggestSelected = autosuggestSelected % autosuggestSearchResults.length
 | |
|         } else {
 | |
|           autosuggestSelected = autosuggestSearchResults.length + autosuggestSelected
 | |
|         }
 | |
|         this.store.setForCurrentAutosuggest({ autosuggestSelected })
 | |
|         event.preventDefault()
 | |
|         event.stopPropagation()
 | |
|       },
 | |
|       clearAutosuggestions (event) {
 | |
|         let { autosuggestShown } = this.store.get()
 | |
|         if (!autosuggestShown) {
 | |
|           return
 | |
|         }
 | |
|         this.store.setForCurrentAutosuggest({
 | |
|           autosuggestSearchResults: [],
 | |
|           autosuggestSelected: 0
 | |
|         })
 | |
|         event.preventDefault()
 | |
|         event.stopPropagation()
 | |
|       }
 | |
|     },
 | |
|     store: () => store,
 | |
|     data: () => ({
 | |
|       rawText: ''
 | |
|     }),
 | |
|     events: {
 | |
|       selectionChange
 | |
|     }
 | |
|   }
 | |
| </script>
 |