diff options
author | Thibaut Girka <thib@sitedethib.com> | 2019-04-21 12:44:30 +0200 |
---|---|---|
committer | ThibG <thib@sitedethib.com> | 2019-04-22 20:15:47 +0200 |
commit | 47faf47ed5a20d7d959110caefe6839d12343ec7 (patch) | |
tree | 5b324ebaee99b9b54d471f425bec3b3c48371d00 | |
parent | a243567a3e6100d65477162308e2c1bb5e056c21 (diff) |
ComposerTextarea → AutosuggestTextarea
10 files changed, 350 insertions, 499 deletions
diff --git a/app/javascript/flavours/glitch/components/autosuggest_emoji.js b/app/javascript/flavours/glitch/components/autosuggest_emoji.js new file mode 100644 index 000000000..c8609e48f --- /dev/null +++ b/app/javascript/flavours/glitch/components/autosuggest_emoji.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import unicodeMapping from 'flavours/glitch/util/emoji/emoji_unicode_mapping_light'; + +const assetHost = process.env.CDN_HOST || ''; + +export default class AutosuggestEmoji extends React.PureComponent { + + static propTypes = { + emoji: PropTypes.object.isRequired, + }; + + render () { + const { emoji } = this.props; + let url; + + if (emoji.custom) { + url = emoji.imageUrl; + } else { + const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')]; + + if (!mapping) { + return null; + } + + url = `${assetHost}/emoji/${mapping.filename}.svg`; + } + + return ( + <div className='emoji'> + <img + className='emojione' + src={url} + alt={emoji.native || emoji.colons} + /> + + {emoji.colons} + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.js b/app/javascript/flavours/glitch/components/autosuggest_textarea.js new file mode 100644 index 000000000..af8fbe406 --- /dev/null +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.js @@ -0,0 +1,224 @@ +import React from 'react'; +import AutosuggestAccountContainer from 'flavours/glitch/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 'flavours/glitch/util/rtl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import Textarea from 'react-textarea-autosize'; +import classNames from 'classnames'; + +const textAtCursorMatchesToken = (str, caretPosition) => { + let word; + + let left = str.slice(0, caretPosition).search(/[^\s\u200B]+$/); + let right = str.slice(caretPosition).search(/[\s\u200B]/); + + if (right < 0) { + word = str.slice(left); + } else { + word = str.slice(left, right + caretPosition); + } + + if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) { + return [null, null]; + } + + word = word.trim().toLowerCase(); + + if (word.length > 0) { + return [left, word]; + } else { + return [null, null]; + } +}; + +export default class AutosuggestTextarea 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, + onPaste: PropTypes.func.isRequired, + autoFocus: PropTypes.bool, + }; + + static defaultProps = { + autoFocus: true, + }; + + state = { + suggestionsHidden: false, + selectedSuggestion: 0, + lastToken: null, + tokenStart: 0, + }; + + onChange = (e) => { + const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart); + + 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 }); + } + + 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.textarea.focus(); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) { + this.setState({ suggestionsHidden: false }); + } + } + + setTextarea = (c) => { + this.textarea = c; + } + + onPaste = (e) => { + if (e.clipboardData && e.clipboardData.files.length === 1) { + this.props.onPaste(e.clipboardData.files); + e.preventDefault(); + } + } + + 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 } = this.props; + const { suggestionsHidden } = this.state; + const style = { direction: 'ltr' }; + + if (isRtl(value)) { + style.direction = 'rtl'; + } + + return ( + <div className='autosuggest-textarea'> + <label> + <span style={{ display: 'none' }}>{placeholder}</span> + + <Textarea + inputRef={this.setTextarea} + className='autosuggest-textarea__textarea' + disabled={disabled} + placeholder={placeholder} + autoFocus={autoFocus} + value={value} + onChange={this.onChange} + onKeyDown={this.onKeyDown} + onKeyUp={onKeyUp} + onBlur={this.onBlur} + onPaste={this.onPaste} + style={style} + aria-autocomplete='list' + /> + </label> + + <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> + {suggestions.map(this.renderSuggestion)} + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js new file mode 100644 index 000000000..fb9bb5035 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js @@ -0,0 +1,24 @@ +import React from 'react'; +import Avatar from 'flavours/glitch/components/avatar'; +import DisplayName from 'flavours/glitch/components/display_name'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +export default class AutosuggestAccount extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + }; + + render () { + const { account } = this.props; + + return ( + <div className='account small' title={account.get('acct')}> + <div className='account__avatar-wrapper'><Avatar account={account} size={24} /></div> + <DisplayName account={account} inline /> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index ecd1aed69..2ea137965 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -7,17 +7,20 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; // Components. import ComposerOptions from '../../composer/options'; import ComposerPublisher from '../../composer/publisher'; -import ComposerTextarea from '../../composer/textarea'; +import ComposerTextareaIcons from '../../composer/textarea/icons'; import UploadFormContainer from '../containers/upload_form_container'; import PollFormContainer from '../containers/poll_form_container'; import WarningContainer from '../containers/warning_container'; import ReplyIndicatorContainer from '../containers/reply_indicator_container'; +import EmojiPicker from 'flavours/glitch/features/emoji_picker'; +import AutosuggestTextarea from '../../../components/autosuggest_textarea'; // Utils. import { countableText } from 'flavours/glitch/util/counter'; import { isMobile } from 'flavours/glitch/util/is_mobile'; const messages = defineMessages({ + placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, missingDescriptionMessage: { id: 'confirmations.missing_media_description.message', defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' }, missingDescriptionConfirm: { id: 'confirmations.missing_media_description.confirm', @@ -183,7 +186,7 @@ class ComposeForm extends ImmutablePureComponent { } // Sets a reference to the textarea. - handleRefTextarea = (textareaComponent) => { + setAutosuggestTextarea = (textareaComponent) => { if (textareaComponent) { this.textarea = textareaComponent.textarea; } @@ -269,6 +272,10 @@ class ComposeForm extends ImmutablePureComponent { } } + handleChange = (e) => { + this.props.onChangeText(e.target.value); + } + render () { const { handleEmoji, @@ -339,27 +346,35 @@ class ComposeForm extends ImmutablePureComponent { </label> </div> - <ComposerTextarea - advancedOptions={advancedOptions} - autoFocus={!showSearch && !isMobile(window.innerWidth, layout)} - disabled={isSubmitting} - intl={intl} - onChange={onChangeText} - onPaste={onUpload} - onPickEmoji={handleEmoji} - onSubmit={handleSubmit} - onSecondarySubmit={handleSecondarySubmit} - onSuggestionsClearRequested={onClearSuggestions} - onSuggestionsFetchRequested={onFetchSuggestions} - onSuggestionSelected={handleSelect} - ref={handleRefTextarea} - suggestions={suggestions} - value={text} - /> + <div className='composer--textarea'> + <ComposerTextareaIcons + advancedOptions={advancedOptions} + intl={intl} + /> + + <AutosuggestTextarea + ref={this.setAutosuggestTextarea} + placeholder={intl.formatMessage(messages.placeholder)} + disabled={isSubmitting} + value={this.props.text} + onChange={this.handleChange} + suggestions={this.props.suggestions} + onKeyDown={this.handleKeyDown} + onSuggestionsFetchRequested={onFetchSuggestions} + onSuggestionsClearRequested={onClearSuggestions} + onSuggestionSelected={this.handleSelect} + onPaste={onUpload} + autoFocus={!showSearch && !isMobile(window.innerWidth, layout)} + /> + + <EmojiPicker onPickEmoji={handleEmoji} /> + </div> + <div className='compose-form__modifiers'> <UploadFormContainer /> <PollFormContainer /> </div> + <ComposerOptions acceptContentTypes={acceptContentTypes} advancedOptions={advancedOptions} @@ -385,6 +400,7 @@ class ComposeForm extends ImmutablePureComponent { sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)} spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler} /> + <ComposerPublisher countText={`${spoilerText}${countableText(text)}${advancedOptions && advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`} disabled={disabledButton} diff --git a/app/javascript/flavours/glitch/features/compose/containers/autosuggest_account_container.js b/app/javascript/flavours/glitch/features/compose/containers/autosuggest_account_container.js new file mode 100644 index 000000000..0e1c328fe --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/containers/autosuggest_account_container.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import AutosuggestAccount from '../components/autosuggest_account'; +import { makeGetAccount } from 'flavours/glitch/selectors'; + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, { id }) => ({ + account: getAccount(state, id), + }); + + return mapStateToProps; +}; + +export default connect(makeMapStateToProps)(AutosuggestAccount); diff --git a/app/javascript/flavours/glitch/features/composer/textarea/index.js b/app/javascript/flavours/glitch/features/composer/textarea/index.js deleted file mode 100644 index 50e46fc78..000000000 --- a/app/javascript/flavours/glitch/features/composer/textarea/index.js +++ /dev/null @@ -1,312 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { - defineMessages, - FormattedMessage, -} from 'react-intl'; -import Textarea from 'react-textarea-autosize'; - -// Components. -import EmojiPicker from 'flavours/glitch/features/emoji_picker'; -import ComposerTextareaIcons from './icons'; -import ComposerTextareaSuggestions from './suggestions'; - -// Utils. -import { isRtl } from 'flavours/glitch/util/rtl'; -import { - assignHandlers, - hiddenComponent, -} from 'flavours/glitch/util/react_helpers'; - -// Messages. -const messages = defineMessages({ - placeholder: { - defaultMessage: 'What is on your mind?', - id: 'compose_form.placeholder', - }, -}); - -// Handlers. -const handlers = { - - // When blurring the textarea, suggestions are hidden. - handleBlur () { - this.setState({ suggestionsHidden: true }); - }, - - // When the contents of the textarea change, we have to pull up new - // autosuggest suggestions if applicable, and also change the value - // of the textarea in our store. - handleChange ({ - target: { - selectionStart, - value, - }, - }) { - const { - onChange, - onSuggestionsFetchRequested, - onSuggestionsClearRequested, - } = this.props; - const { lastToken } = this.state; - - // This gets the token at the caret location, if it begins with an - // `@` (mentions) or `:` (shortcodes). - const left = value.slice(0, selectionStart).search(/[^\s\u200B]+$/); - const right = value.slice(selectionStart).search(/[\s\u200B]/); - const token = function () { - switch (true) { - case left < 0 || !/[@:#]/.test(value[left]): - return null; - case right < 0: - return value.slice(left); - default: - return value.slice(left, right + selectionStart).trim().toLowerCase(); - } - }(); - - // We only request suggestions for tokens which are at least 3 - // characters long. - if (onSuggestionsFetchRequested && token && token.length >= 3) { - if (lastToken !== token) { - this.setState({ - lastToken: token, - selectedSuggestion: 0, - tokenStart: left, - }); - onSuggestionsFetchRequested(token); - } - } else { - this.setState({ lastToken: null }); - if (onSuggestionsClearRequested) { - onSuggestionsClearRequested(); - } - } - - // Updates the value of the textarea. - if (onChange) { - onChange(value); - } - }, - - // Handles a click on an autosuggestion. - handleClickSuggestion (index) { - const { textarea } = this; - const { - onSuggestionSelected, - suggestions, - } = this.props; - const { - lastToken, - tokenStart, - } = this.state; - onSuggestionSelected(tokenStart, lastToken, suggestions.get(index)); - textarea.focus(); - }, - - // Handles a keypress. If the autosuggestions are visible, we need - // to allow keypresses to navigate and sleect them. - handleKeyDown (e) { - const { - disabled, - onSubmit, - onSecondarySubmit, - onSuggestionSelected, - suggestions, - } = this.props; - const { - lastToken, - suggestionsHidden, - selectedSuggestion, - tokenStart, - } = this.state; - - // Keypresses do nothing if the composer is disabled. - if (disabled) { - e.preventDefault(); - return; - } - - // We submit the status on control/meta + enter. - if (onSubmit && e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { - onSubmit(); - } - - // Submit the status with secondary visibility on alt + enter. - if (onSecondarySubmit && e.keyCode === 13 && e.altKey) { - onSecondarySubmit(); - } - - // Switches over the pressed key. - switch(e.key) { - - // On arrow down, we pick the next suggestion. - case 'ArrowDown': - if (suggestions && suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); - } - return; - - // On arrow up, we pick the previous suggestion. - case 'ArrowUp': - if (suggestions && suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); - } - return; - - // On enter or tab, we select the suggestion. - case 'Enter': - case 'Tab': - if (onSuggestionSelected && lastToken !== null && suggestions && suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - e.stopPropagation(); - onSuggestionSelected(tokenStart, lastToken, suggestions.get(selectedSuggestion)); - } - return; - } - }, - - // When the escape key is released, we either close the suggestions - // window or focus the UI. - handleKeyUp ({ key }) { - const { suggestionsHidden } = this.state; - if (key === 'Escape') { - if (!suggestionsHidden) { - this.setState({ suggestionsHidden: true }); - } else { - document.querySelector('.ui').parentElement.focus(); - } - } - }, - - // Handles the pasting of images into the composer. - handlePaste (e) { - const { onPaste } = this.props; - let d; - if (onPaste && (d = e.clipboardData) && (d = d.files).length === 1) { - onPaste(d); - e.preventDefault(); - } - }, - - // Saves a reference to the textarea. - handleRefTextarea (textarea) { - this.textarea = textarea; - }, -}; - -// The component. -export default class ComposerTextarea extends React.Component { - - // Constructor. - constructor (props) { - super(props); - assignHandlers(this, handlers); - this.state = { - suggestionsHidden: false, - selectedSuggestion: 0, - lastToken: null, - tokenStart: 0, - }; - - // Instance variables. - this.textarea = null; - } - - // When we receive new suggestions, we unhide the suggestions window - // if we didn't have any suggestions before. - componentWillReceiveProps (nextProps) { - const { suggestions } = this.props; - const { suggestionsHidden } = this.state; - if (nextProps.suggestions && nextProps.suggestions !== suggestions && nextProps.suggestions.size > 0 && suggestionsHidden) { - this.setState({ suggestionsHidden: false }); - } - } - - // Rendering. - render () { - const { - handleBlur, - handleChange, - handleClickSuggestion, - handleKeyDown, - handleKeyUp, - handlePaste, - handleRefTextarea, - } = this.handlers; - const { - advancedOptions, - autoFocus, - disabled, - intl, - onPickEmoji, - suggestions, - value, - } = this.props; - const { - selectedSuggestion, - suggestionsHidden, - } = this.state; - - // The result. - return ( - <div className='composer--textarea'> - <label> - <span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span> - <ComposerTextareaIcons - advancedOptions={advancedOptions} - intl={intl} - /> - <Textarea - aria-autocomplete='list' - autoFocus={autoFocus} - className='textarea' - disabled={disabled} - inputRef={handleRefTextarea} - onBlur={handleBlur} - onChange={handleChange} - onKeyDown={handleKeyDown} - onKeyUp={handleKeyUp} - onPaste={handlePaste} - placeholder={intl.formatMessage(messages.placeholder)} - value={value} - style={{ direction: isRtl(value) ? 'rtl' : 'ltr' }} - /> - </label> - <EmojiPicker onPickEmoji={onPickEmoji} /> - <ComposerTextareaSuggestions - hidden={suggestionsHidden} - onSuggestionClick={handleClickSuggestion} - suggestions={suggestions} - value={selectedSuggestion} - /> - </div> - ); - } - -} - -// Props. -ComposerTextarea.propTypes = { - advancedOptions: ImmutablePropTypes.map, - autoFocus: PropTypes.bool, - disabled: PropTypes.bool, - intl: PropTypes.object.isRequired, - onChange: PropTypes.func, - onPaste: PropTypes.func, - onPickEmoji: PropTypes.func, - onSubmit: PropTypes.func, - onSecondarySubmit: PropTypes.func, - onSuggestionsClearRequested: PropTypes.func, - onSuggestionsFetchRequested: PropTypes.func, - onSuggestionSelected: PropTypes.func, - suggestions: ImmutablePropTypes.list, - value: PropTypes.string, -}; - -// Default props. -ComposerTextarea.defaultProps = { autoFocus: true }; diff --git a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js deleted file mode 100644 index dc72585f2..000000000 --- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js +++ /dev/null @@ -1,43 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -// Components. -import ComposerTextareaSuggestionsItem from './item'; - -// The component. -export default function ComposerTextareaSuggestions ({ - hidden, - onSuggestionClick, - suggestions, - value, -}) { - - // The result. - return ( - <div - className='composer--textarea--suggestions' - hidden={hidden || !suggestions || suggestions.isEmpty()} - > - {!hidden && suggestions ? suggestions.map( - (suggestion, index) => ( - <ComposerTextareaSuggestionsItem - index={index} - key={typeof suggestion === 'object' ? suggestion.id : suggestion} - onClick={onSuggestionClick} - selected={index === value} - suggestion={suggestion} - /> - ) - ) : null} - </div> - ); -} - -ComposerTextareaSuggestions.propTypes = { - hidden: PropTypes.bool, - onSuggestionClick: PropTypes.func, - suggestions: ImmutablePropTypes.list, - value: PropTypes.number, -}; diff --git a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js deleted file mode 100644 index 1b7ae8904..000000000 --- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js +++ /dev/null @@ -1,118 +0,0 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; - -// Components. -import AccountContainer from 'flavours/glitch/containers/account_container'; - -// Utils. -import { unicodeMapping } from 'flavours/glitch/util/emoji'; -import { assignHandlers } from 'flavours/glitch/util/react_helpers'; - -// Gets our asset host from the environment, if available. -const assetHost = process.env.CDN_HOST || ''; - -// Handlers. -const handlers = { - - // Handles a click on a suggestion. - handleClick (e) { - const { - index, - onClick, - } = this.props; - if (onClick) { - e.preventDefault(); - e.stopPropagation(); // Prevents following account links - onClick(index); - } - }, - - // This prevents the focus from changing, which would mess with - // our suggestion code. - handleMouseDown (e) { - e.preventDefault(); - }, -}; - -// The component. -export default class ComposerTextareaSuggestionsItem extends React.Component { - - // Constructor. - constructor (props) { - super(props); - assignHandlers(this, handlers); - } - - // Rendering. - render () { - const { - handleMouseDown, - handleClick, - } = this.handlers; - const { - selected, - suggestion, - } = this.props; - const computedClass = classNames('composer--textarea--suggestions--item', { selected }); - - // If the suggestion is an object, then we render an emoji. - // Otherwise, we render a hashtag if it starts with #, or an account. - let inner; - if (typeof suggestion === 'object') { - let url; - if (suggestion.custom) { - url = suggestion.imageUrl; - } else { - const mapping = unicodeMapping[suggestion.native] || unicodeMapping[suggestion.native.replace(/\uFE0F$/, '')]; - if (mapping) { - url = `${assetHost}/emoji/${mapping.filename}.svg`; - } - } - if (url) { - inner = ( - <div className='emoji'> - <img - alt={suggestion.native || suggestion.colons} - className='emojione' - src={url} - /> - {suggestion.colons} - </div> - ); - } - } else if (suggestion[0] === '#') { - inner = suggestion; - } else { - inner = ( - <AccountContainer - id={suggestion} - small - /> - ); - } - - // The result. - return ( - <div - className={computedClass} - onMouseDown={handleMouseDown} - onClickCapture={handleClick} // Jumps in front of contents - role='button' - tabIndex='0' - > - { inner } - </div> - ); - } - -} - -// Props. -ComposerTextareaSuggestionsItem.propTypes = { - index: PropTypes.number, - onClick: PropTypes.func, - selected: PropTypes.bool, - suggestion: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), -}; diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss index f0729bedc..466b654de 100644 --- a/app/javascript/flavours/glitch/styles/components/composer.scss +++ b/app/javascript/flavours/glitch/styles/components/composer.scss @@ -131,8 +131,8 @@ .composer--textarea { position: relative; - & > label { - .textarea { + label { + .autosuggest-textarea__textarea { display: block; box-sizing: border-box; margin: 0; @@ -186,7 +186,7 @@ } } -.composer--textarea--suggestions { +.autosuggest-textarea__suggestions { display: block; position: absolute; box-sizing: border-box; @@ -199,11 +199,14 @@ box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4); font-size: 14px; z-index: 99; + display: none; +} - &[hidden] { display: none } +.autosuggest-textarea__suggestions--visible { + display: block; } -.composer--textarea--suggestions--item { +.autosuggest-textarea__suggestions__item { display: flex; flex-direction: row; align-items: center; diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss index 6f105d3fa..224272f24 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss @@ -196,11 +196,11 @@ border-color: $ui-base-color; } -.composer--textarea--suggestions { +.autosuggest-textarea__suggestions { background: lighten($ui-base-color, 10%) } -.composer--textarea--suggestions--item { +.autosuggest-textarea__suggestions__item { &:hover, &:focus, &:active, |