parent
a243567a3e
commit
47faf47ed5
@ -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> |
||||
); |
||||
} |
||||
|
||||
} |
@ -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> |
||||
); |
||||
} |
||||
|
||||
} |
@ -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> |
||||
); |
||||
} |
||||
|
||||
} |
@ -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); |
@ -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 }; |
@ -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, |
||||
}; |
@ -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]), |
||||
}; |
Loading…
Reference in new issue