Add emoji suggestions to CW and poll option fields (#10555)
* Refactor selectComposeSuggestion so that different paths can be updated * Add suggestions in CW field * Add emoji suggestion to poll options * Attempt to fix CSS * Hide suggestions by default They will be enabled if the input has focus
This commit is contained in:
		| @@ -383,7 +383,7 @@ export function readyComposeSuggestionsAccounts(token, accounts) { | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function selectComposeSuggestion(position, token, suggestion) { | ||||
| export function selectComposeSuggestion(position, token, suggestion, path) { | ||||
|   return (dispatch, getState) => { | ||||
|     let completion, startPosition; | ||||
|  | ||||
| @@ -405,6 +405,7 @@ export function selectComposeSuggestion(position, token, suggestion) { | ||||
|       position: startPosition, | ||||
|       token, | ||||
|       completion, | ||||
|       path, | ||||
|     }); | ||||
|   }; | ||||
| }; | ||||
|   | ||||
							
								
								
									
										229
									
								
								app/javascript/mastodon/components/autosuggest_input.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								app/javascript/mastodon/components/autosuggest_input.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,229 @@ | ||||
| import React from 'react'; | ||||
| import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; | ||||
| import AutosuggestEmoji from './autosuggest_emoji'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { isRtl } from '../rtl'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| import classNames from 'classnames'; | ||||
| import { List as ImmutableList } from 'immutable'; | ||||
|  | ||||
| const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { | ||||
|   let word; | ||||
|  | ||||
|   let left  = str.slice(0, caretPosition).search(/\S+$/); | ||||
|   let right = str.slice(caretPosition).search(/\s/); | ||||
|  | ||||
|   if (right < 0) { | ||||
|     word = str.slice(left); | ||||
|   } else { | ||||
|     word = str.slice(left, right + caretPosition); | ||||
|   } | ||||
|  | ||||
|   if (!word || word.trim().length < 3 || searchTokens.indexOf(word[0]) === -1) { | ||||
|     return [null, null]; | ||||
|   } | ||||
|  | ||||
|   word = word.trim().toLowerCase(); | ||||
|  | ||||
|   if (word.length > 0) { | ||||
|     return [left + 1, word]; | ||||
|   } else { | ||||
|     return [null, null]; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export default class AutosuggestInput extends ImmutablePureComponent { | ||||
|  | ||||
|   static propTypes = { | ||||
|     value: PropTypes.string, | ||||
|     suggestions: ImmutablePropTypes.list, | ||||
|     disabled: PropTypes.bool, | ||||
|     placeholder: PropTypes.string, | ||||
|     onSuggestionSelected: PropTypes.func.isRequired, | ||||
|     onSuggestionsClearRequested: PropTypes.func.isRequired, | ||||
|     onSuggestionsFetchRequested: PropTypes.func.isRequired, | ||||
|     onChange: PropTypes.func.isRequired, | ||||
|     onKeyUp: PropTypes.func, | ||||
|     onKeyDown: PropTypes.func, | ||||
|     autoFocus: PropTypes.bool, | ||||
|     className: PropTypes.string, | ||||
|     id: PropTypes.string, | ||||
|     searchTokens: PropTypes.list, | ||||
|     maxLength: PropTypes.number, | ||||
|   }; | ||||
|  | ||||
|   static defaultProps = { | ||||
|     autoFocus: true, | ||||
|     searchTokens: ImmutableList(['@', ':', '#']), | ||||
|   }; | ||||
|  | ||||
|   state = { | ||||
|     suggestionsHidden: true, | ||||
|     focused: false, | ||||
|     selectedSuggestion: 0, | ||||
|     lastToken: null, | ||||
|     tokenStart: 0, | ||||
|   }; | ||||
|  | ||||
|   onChange = (e) => { | ||||
|     const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens); | ||||
|  | ||||
|     if (token !== null && this.state.lastToken !== token) { | ||||
|       this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); | ||||
|       this.props.onSuggestionsFetchRequested(token); | ||||
|     } else if (token === null) { | ||||
|       this.setState({ lastToken: null }); | ||||
|       this.props.onSuggestionsClearRequested(); | ||||
|     } | ||||
|  | ||||
|     this.props.onChange(e); | ||||
|   } | ||||
|  | ||||
|   onKeyDown = (e) => { | ||||
|     const { suggestions, disabled } = this.props; | ||||
|     const { selectedSuggestion, suggestionsHidden } = this.state; | ||||
|  | ||||
|     if (disabled) { | ||||
|       e.preventDefault(); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (e.which === 229 || e.isComposing) { | ||||
|       // Ignore key events during text composition | ||||
|       // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac) | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     switch(e.key) { | ||||
|     case 'Escape': | ||||
|       if (suggestions.size === 0 || suggestionsHidden) { | ||||
|         document.querySelector('.ui').parentElement.focus(); | ||||
|       } else { | ||||
|         e.preventDefault(); | ||||
|         this.setState({ suggestionsHidden: true }); | ||||
|       } | ||||
|  | ||||
|       break; | ||||
|     case 'ArrowDown': | ||||
|       if (suggestions.size > 0 && !suggestionsHidden) { | ||||
|         e.preventDefault(); | ||||
|         this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); | ||||
|       } | ||||
|  | ||||
|       break; | ||||
|     case 'ArrowUp': | ||||
|       if (suggestions.size > 0 && !suggestionsHidden) { | ||||
|         e.preventDefault(); | ||||
|         this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); | ||||
|       } | ||||
|  | ||||
|       break; | ||||
|     case 'Enter': | ||||
|     case 'Tab': | ||||
|       // Select suggestion | ||||
|       if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
|         this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); | ||||
|       } | ||||
|  | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     if (e.defaultPrevented || !this.props.onKeyDown) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this.props.onKeyDown(e); | ||||
|   } | ||||
|  | ||||
|   onBlur = () => { | ||||
|     this.setState({ suggestionsHidden: true, focused: false }); | ||||
|   } | ||||
|  | ||||
|   onFocus = () => { | ||||
|     this.setState({ focused: true }); | ||||
|   } | ||||
|  | ||||
|   onSuggestionClick = (e) => { | ||||
|     const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); | ||||
|     e.preventDefault(); | ||||
|     this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); | ||||
|     this.input.focus(); | ||||
|   } | ||||
|  | ||||
|   componentWillReceiveProps (nextProps) { | ||||
|     if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { | ||||
|       this.setState({ suggestionsHidden: false }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   setInput = (c) => { | ||||
|     this.input = c; | ||||
|   } | ||||
|  | ||||
|   renderSuggestion = (suggestion, i) => { | ||||
|     const { selectedSuggestion } = this.state; | ||||
|     let inner, key; | ||||
|  | ||||
|     if (typeof suggestion === 'object') { | ||||
|       inner = <AutosuggestEmoji emoji={suggestion} />; | ||||
|       key   = suggestion.id; | ||||
|     } else if (suggestion[0] === '#') { | ||||
|       inner = suggestion; | ||||
|       key   = suggestion; | ||||
|     } else { | ||||
|       inner = <AutosuggestAccountContainer id={suggestion} />; | ||||
|       key   = suggestion; | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       <div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}> | ||||
|         {inner} | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   render () { | ||||
|     const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props; | ||||
|     const { suggestionsHidden } = this.state; | ||||
|     const style = { direction: 'ltr' }; | ||||
|  | ||||
|     if (isRtl(value)) { | ||||
|       style.direction = 'rtl'; | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       <div className='autosuggest-input'> | ||||
|         <label> | ||||
|           <span style={{ display: 'none' }}>{placeholder}</span> | ||||
|  | ||||
|           <input | ||||
|             type='text' | ||||
|             ref={this.setInput} | ||||
|             disabled={disabled} | ||||
|             placeholder={placeholder} | ||||
|             autoFocus={autoFocus} | ||||
|             value={value} | ||||
|             onChange={this.onChange} | ||||
|             onKeyDown={this.onKeyDown} | ||||
|             onKeyUp={onKeyUp} | ||||
|             onFocus={this.onFocus} | ||||
|             onBlur={this.onBlur} | ||||
|             style={style} | ||||
|             aria-autocomplete='list' | ||||
|             id={id} | ||||
|             className={className} | ||||
|             maxLength={maxLength} | ||||
|           /> | ||||
|         </label> | ||||
|  | ||||
|         <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> | ||||
|           {suggestions.map(this.renderSuggestion)} | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -55,7 +55,8 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { | ||||
|   }; | ||||
|  | ||||
|   state = { | ||||
|     suggestionsHidden: false, | ||||
|     suggestionsHidden: true, | ||||
|     focused: false, | ||||
|     selectedSuggestion: 0, | ||||
|     lastToken: null, | ||||
|     tokenStart: 0, | ||||
| @@ -134,7 +135,11 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { | ||||
|   } | ||||
|  | ||||
|   onBlur = () => { | ||||
|     this.setState({ suggestionsHidden: true }); | ||||
|     this.setState({ suggestionsHidden: true, focused: false }); | ||||
|   } | ||||
|  | ||||
|   onFocus = () => { | ||||
|     this.setState({ focused: true }); | ||||
|   } | ||||
|  | ||||
|   onSuggestionClick = (e) => { | ||||
| @@ -145,7 +150,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { | ||||
|   } | ||||
|  | ||||
|   componentWillReceiveProps (nextProps) { | ||||
|     if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) { | ||||
|     if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { | ||||
|       this.setState({ suggestionsHidden: false }); | ||||
|     } | ||||
|   } | ||||
| @@ -207,6 +212,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { | ||||
|             onChange={this.onChange} | ||||
|             onKeyDown={this.onKeyDown} | ||||
|             onKeyUp={onKeyUp} | ||||
|             onFocus={this.onFocus} | ||||
|             onBlur={this.onBlur} | ||||
|             onPaste={this.onPaste} | ||||
|             style={style} | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import ReplyIndicatorContainer from '../containers/reply_indicator_container'; | ||||
| import AutosuggestTextarea from '../../../components/autosuggest_textarea'; | ||||
| import AutosuggestInput from '../../../components/autosuggest_input'; | ||||
| import PollButtonContainer from '../containers/poll_button_container'; | ||||
| import UploadButtonContainer from '../containers/upload_button_container'; | ||||
| import { defineMessages, injectIntl } from 'react-intl'; | ||||
| @@ -102,7 +103,11 @@ class ComposeForm extends ImmutablePureComponent { | ||||
|   } | ||||
|  | ||||
|   onSuggestionSelected = (tokenStart, token, value) => { | ||||
|     this.props.onSuggestionSelected(tokenStart, token, value); | ||||
|     this.props.onSuggestionSelected(tokenStart, token, value, ['text']); | ||||
|   } | ||||
|  | ||||
|   onSpoilerSuggestionSelected = (tokenStart, token, value) => { | ||||
|     this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']); | ||||
|   } | ||||
|  | ||||
|   handleChangeSpoilerText = (e) => { | ||||
| @@ -135,7 +140,7 @@ class ComposeForm extends ImmutablePureComponent { | ||||
|       this.autosuggestTextarea.textarea.focus(); | ||||
|     } else if (this.props.spoiler !== prevProps.spoiler) { | ||||
|       if (this.props.spoiler) { | ||||
|         this.spoilerText.focus(); | ||||
|         this.spoilerText.input.focus(); | ||||
|       } else { | ||||
|         this.autosuggestTextarea.textarea.focus(); | ||||
|       } | ||||
| @@ -178,10 +183,21 @@ class ComposeForm extends ImmutablePureComponent { | ||||
|         <ReplyIndicatorContainer /> | ||||
|  | ||||
|         <div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}> | ||||
|           <label> | ||||
|             <span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span> | ||||
|             <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoilerText} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} tabIndex={this.props.spoiler ? 0 : -1} type='text' className='spoiler-input__input'  id='cw-spoiler-input' ref={this.setSpoilerText} /> | ||||
|           </label> | ||||
|           <AutosuggestInput | ||||
|             placeholder={intl.formatMessage(messages.spoiler_placeholder)} | ||||
|             value={this.props.spoilerText} | ||||
|             onChange={this.handleChangeSpoilerText} | ||||
|             onKeyDown={this.handleKeyDown} | ||||
|             disabled={!this.props.spoiler} | ||||
|             ref={this.setSpoilerText} | ||||
|             suggestions={this.props.suggestions} | ||||
|             onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} | ||||
|             onSuggestionsClearRequested={this.onSuggestionsClearRequested} | ||||
|             onSuggestionSelected={this.onSpoilerSuggestionSelected} | ||||
|             searchTokens={[':']} | ||||
|             id='cw-spoiler-input' | ||||
|             className='spoiler-input__input' | ||||
|           /> | ||||
|         </div> | ||||
|  | ||||
|         <div className='compose-form__autosuggest-wrapper'> | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||
| import IconButton from 'mastodon/components/icon_button'; | ||||
| import Icon from 'mastodon/components/icon'; | ||||
| import AutosuggestInput from 'mastodon/components/autosuggest_input'; | ||||
| import classNames from 'classnames'; | ||||
|  | ||||
| const messages = defineMessages({ | ||||
| @@ -27,6 +28,10 @@ class Option extends React.PureComponent { | ||||
|     onChange: PropTypes.func.isRequired, | ||||
|     onRemove: PropTypes.func.isRequired, | ||||
|     onToggleMultiple: PropTypes.func.isRequired, | ||||
|     suggestions: ImmutablePropTypes.list, | ||||
|     onClearSuggestions: PropTypes.func.isRequired, | ||||
|     onFetchSuggestions: PropTypes.func.isRequired, | ||||
|     onSuggestionSelected: PropTypes.func.isRequired, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|   }; | ||||
|  | ||||
| @@ -38,12 +43,25 @@ class Option extends React.PureComponent { | ||||
|     this.props.onRemove(this.props.index); | ||||
|   }; | ||||
|  | ||||
|  | ||||
|   handleToggleMultiple = e => { | ||||
|     this.props.onToggleMultiple(); | ||||
|     e.preventDefault(); | ||||
|     e.stopPropagation(); | ||||
|   }; | ||||
|  | ||||
|   onSuggestionsClearRequested = () => { | ||||
|     this.props.onClearSuggestions(); | ||||
|   } | ||||
|  | ||||
|   onSuggestionsFetchRequested = (token) => { | ||||
|     this.props.onFetchSuggestions(token); | ||||
|   } | ||||
|  | ||||
|   onSuggestionSelected = (tokenStart, token, value) => { | ||||
|     this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]); | ||||
|   } | ||||
|  | ||||
|   render () { | ||||
|     const { isPollMultiple, title, index, intl } = this.props; | ||||
|  | ||||
| @@ -57,12 +75,16 @@ class Option extends React.PureComponent { | ||||
|             tabIndex='0' | ||||
|           /> | ||||
|  | ||||
|           <input | ||||
|             type='text' | ||||
|           <AutosuggestInput | ||||
|             placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })} | ||||
|             maxLength={25} | ||||
|             value={title} | ||||
|             onChange={this.handleOptionTitleChange} | ||||
|             suggestions={this.props.suggestions} | ||||
|             onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} | ||||
|             onSuggestionsClearRequested={this.onSuggestionsClearRequested} | ||||
|             onSuggestionSelected={this.onSuggestionSelected} | ||||
|             searchTokens={[':']} | ||||
|           /> | ||||
|         </label> | ||||
|  | ||||
| @@ -87,6 +109,10 @@ class PollForm extends ImmutablePureComponent { | ||||
|     onAddOption: PropTypes.func.isRequired, | ||||
|     onRemoveOption: PropTypes.func.isRequired, | ||||
|     onChangeSettings: PropTypes.func.isRequired, | ||||
|     suggestions: ImmutablePropTypes.list, | ||||
|     onClearSuggestions: PropTypes.func.isRequired, | ||||
|     onFetchSuggestions: PropTypes.func.isRequired, | ||||
|     onSuggestionSelected: PropTypes.func.isRequired, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|   }; | ||||
|  | ||||
| @@ -103,7 +129,7 @@ class PollForm extends ImmutablePureComponent { | ||||
|   }; | ||||
|  | ||||
|   render () { | ||||
|     const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props; | ||||
|     const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props; | ||||
|  | ||||
|     if (!options) { | ||||
|       return null; | ||||
| @@ -112,7 +138,7 @@ class PollForm extends ImmutablePureComponent { | ||||
|     return ( | ||||
|       <div className='compose-form__poll-wrapper'> | ||||
|         <ul> | ||||
|           {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} />)} | ||||
|           {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} {...other} />)} | ||||
|         </ul> | ||||
|  | ||||
|         <div className='poll__footer'> | ||||
|   | ||||
| @@ -45,8 +45,8 @@ const mapDispatchToProps = (dispatch) => ({ | ||||
|     dispatch(fetchComposeSuggestions(token)); | ||||
|   }, | ||||
|  | ||||
|   onSuggestionSelected (position, token, suggestion) { | ||||
|     dispatch(selectComposeSuggestion(position, token, suggestion)); | ||||
|   onSuggestionSelected (position, token, suggestion, path) { | ||||
|     dispatch(selectComposeSuggestion(position, token, suggestion, path)); | ||||
|   }, | ||||
|  | ||||
|   onChangeSpoilerText (checked) { | ||||
|   | ||||
| @@ -1,8 +1,14 @@ | ||||
| import { connect } from 'react-redux'; | ||||
| import PollForm from '../components/poll_form'; | ||||
| import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose'; | ||||
| import { | ||||
|   clearComposeSuggestions, | ||||
|   fetchComposeSuggestions, | ||||
|   selectComposeSuggestion, | ||||
| } from '../../../actions/compose'; | ||||
|  | ||||
| const mapStateToProps = state => ({ | ||||
|   suggestions: state.getIn(['compose', 'suggestions']), | ||||
|   options: state.getIn(['compose', 'poll', 'options']), | ||||
|   expiresIn: state.getIn(['compose', 'poll', 'expires_in']), | ||||
|   isMultiple: state.getIn(['compose', 'poll', 'multiple']), | ||||
| @@ -24,6 +30,19 @@ const mapDispatchToProps = dispatch => ({ | ||||
|   onChangeSettings(expiresIn, isMultiple) { | ||||
|     dispatch(changePollSettings(expiresIn, isMultiple)); | ||||
|   }, | ||||
|  | ||||
|   onClearSuggestions () { | ||||
|     dispatch(clearComposeSuggestions()); | ||||
|   }, | ||||
|  | ||||
|   onFetchSuggestions (token) { | ||||
|     dispatch(fetchComposeSuggestions(token)); | ||||
|   }, | ||||
|  | ||||
|   onSuggestionSelected (position, token, accountId, path) { | ||||
|     dispatch(selectComposeSuggestion(position, token, accountId, path)); | ||||
|   }, | ||||
|  | ||||
| }); | ||||
|  | ||||
| export default connect(mapStateToProps, mapDispatchToProps)(PollForm); | ||||
|   | ||||
| @@ -131,13 +131,15 @@ function removeMedia(state, mediaId) { | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| const insertSuggestion = (state, position, token, completion) => { | ||||
| const insertSuggestion = (state, position, token, completion, path) => { | ||||
|   return state.withMutations(map => { | ||||
|     map.update('text', oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`); | ||||
|     map.updateIn(path, oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`); | ||||
|     map.set('suggestion_token', null); | ||||
|     map.update('suggestions', ImmutableList(), list => list.clear()); | ||||
|     map.set('focusDate', new Date()); | ||||
|     map.set('caretPosition', position + completion.length + 1); | ||||
|     map.set('suggestions', ImmutableList()); | ||||
|     if (path.length === 1 && path[0] === 'text') { | ||||
|       map.set('focusDate', new Date()); | ||||
|       map.set('caretPosition', position + completion.length + 1); | ||||
|     } | ||||
|     map.set('idempotencyKey', uuid()); | ||||
|   }); | ||||
| }; | ||||
| @@ -304,7 +306,7 @@ export default function compose(state = initialState, action) { | ||||
|   case COMPOSE_SUGGESTIONS_READY: | ||||
|     return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token); | ||||
|   case COMPOSE_SUGGESTION_SELECT: | ||||
|     return insertSuggestion(state, action.position, action.token, action.completion); | ||||
|     return insertSuggestion(state, action.position, action.token, action.completion, action.path); | ||||
|   case COMPOSE_SUGGESTION_TAGS_UPDATE: | ||||
|     return updateSuggestionTags(state, action.token); | ||||
|   case COMPOSE_TAG_HISTORY_UPDATE: | ||||
|   | ||||
| @@ -319,6 +319,7 @@ | ||||
|   } | ||||
|  | ||||
|   .autosuggest-textarea, | ||||
|   .autosuggest-input, | ||||
|   .spoiler-input { | ||||
|     position: relative; | ||||
|   } | ||||
|   | ||||
| @@ -37,11 +37,14 @@ | ||||
|       display: none; | ||||
|     } | ||||
|  | ||||
|     .autossugest-input { | ||||
|       flex: 1 1 auto; | ||||
|     } | ||||
|  | ||||
|     input[type=text] { | ||||
|       display: block; | ||||
|       box-sizing: border-box; | ||||
|       flex: 1 1 auto; | ||||
|       width: 20px; | ||||
|       width: 100%; | ||||
|       font-size: 14px; | ||||
|       color: $inverted-text-color; | ||||
|       display: block; | ||||
| @@ -64,6 +67,7 @@ | ||||
|     &.editable { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       overflow: visible; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user