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