Basic username autocomplete for text area
This commit is contained in:
@ -13,6 +13,9 @@ export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
|
||||
export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
|
||||
export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
|
||||
|
||||
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
||||
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
||||
|
||||
export function changeCompose(text) {
|
||||
return {
|
||||
type: COMPOSE_CHANGE,
|
||||
@ -129,3 +132,27 @@ export function undoUploadCompose(media_id) {
|
||||
media_id: media_id
|
||||
};
|
||||
};
|
||||
|
||||
export function clearComposeSuggestions() {
|
||||
return {
|
||||
type: COMPOSE_SUGGESTIONS_CLEAR
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchComposeSuggestions(token) {
|
||||
return (dispatch, getState) => {
|
||||
const loadedCandidates = getState().get('accounts').filter(item => item.get('acct').toLowerCase().slice(0, token.length) === token).map(item => ({
|
||||
label: item.get('acct'),
|
||||
completion: item.get('acct').slice(0, token.length)
|
||||
})).toList().toJS();
|
||||
|
||||
dispatch(readyComposeSuggestions(loadedCandidates));
|
||||
};
|
||||
};
|
||||
|
||||
export function readyComposeSuggestions(accounts) {
|
||||
return {
|
||||
type: COMPOSE_SUGGESTIONS_READY,
|
||||
accounts
|
||||
};
|
||||
};
|
||||
|
@ -4,11 +4,62 @@ import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ReplyIndicator from './reply_indicator';
|
||||
import UploadButton from './upload_button';
|
||||
import Autosuggest from 'react-autosuggest';
|
||||
|
||||
const getTokenForSuggestions = (str, caretPosition) => {
|
||||
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 < 2 || word[0] !== '@') {
|
||||
return null;
|
||||
}
|
||||
|
||||
word = word.trim().toLowerCase().slice(1);
|
||||
|
||||
if (word.length > 0) {
|
||||
return word;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getSuggestionValue = suggestion => suggestion;
|
||||
|
||||
const renderSuggestion = suggestion => (
|
||||
<span>{suggestion}</span>
|
||||
);
|
||||
|
||||
const textareaStyle = {
|
||||
display: 'block',
|
||||
boxSizing: 'border-box',
|
||||
width: '100%',
|
||||
height: '100px',
|
||||
resize: 'none',
|
||||
border: 'none',
|
||||
color: '#282c37',
|
||||
padding: '10px',
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: '14px',
|
||||
margin: '0'
|
||||
};
|
||||
|
||||
const renderInputComponent = inputProps => (
|
||||
<textarea {...inputProps} placeholder='What is on your mind?' className='compose-form__textarea' style={textareaStyle} />
|
||||
);
|
||||
|
||||
const ComposeForm = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
text: React.PropTypes.string.isRequired,
|
||||
suggestions: React.PropTypes.array,
|
||||
is_submitting: React.PropTypes.bool,
|
||||
is_uploading: React.PropTypes.bool,
|
||||
in_reply_to: ImmutablePropTypes.map,
|
||||
@ -35,7 +86,39 @@ const ComposeForm = React.createClass({
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.text !== this.props.text || prevProps.in_reply_to !== this.props.in_reply_to) {
|
||||
this.refs.textarea.focus();
|
||||
const node = ReactDOM.findDOMNode(this.refs.autosuggest);
|
||||
const textarea = node.querySelector('textarea');
|
||||
|
||||
if (textarea) {
|
||||
textarea.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onSuggestionsClearRequested () {
|
||||
this.props.onClearSuggestions();
|
||||
},
|
||||
|
||||
onSuggestionsFetchRequested ({ value }) {
|
||||
const node = ReactDOM.findDOMNode(this.refs.autosuggest);
|
||||
const textarea = node.querySelector('textarea');
|
||||
|
||||
if (textarea) {
|
||||
const token = getTokenForSuggestions(value, textarea.selectionStart);
|
||||
|
||||
if (token !== null) {
|
||||
this.props.onFetchSuggestions(token);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onSuggestionSelected (e, { suggestionValue, method }) {
|
||||
const node = ReactDOM.findDOMNode(this.refs.autosuggest);
|
||||
const textarea = node.querySelector('textarea');
|
||||
|
||||
if (textarea) {
|
||||
const str = this.props.text;
|
||||
this.props.onChange([str.slice(0, textarea.selectionStart), suggestionValue, str.slice(textarea.selectionStart)].join(''));
|
||||
}
|
||||
},
|
||||
|
||||
@ -47,11 +130,29 @@ const ComposeForm = React.createClass({
|
||||
replyArea = <ReplyIndicator status={this.props.in_reply_to} onCancel={this.props.onCancelReply} />;
|
||||
}
|
||||
|
||||
const inputProps = {
|
||||
placeholder: 'What is on your mind?',
|
||||
value: this.props.text,
|
||||
onKeyUp: this.handleKeyUp,
|
||||
onChange: this.handleChange,
|
||||
disabled: disabled
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '10px' }}>
|
||||
{replyArea}
|
||||
|
||||
<textarea ref='textarea' disabled={disabled} placeholder='What is on your mind?' value={this.props.text} onKeyUp={this.handleKeyUp} onChange={this.handleChange} className='compose-form__textarea' style={{ display: 'block', boxSizing: 'border-box', width: '100%', height: '100px', resize: 'none', border: 'none', color: '#282c37', padding: '10px', fontFamily: 'Roboto', fontSize: '14px', margin: '0' }} />
|
||||
<Autosuggest
|
||||
ref='autosuggest'
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
getSuggestionValue={getSuggestionValue}
|
||||
renderSuggestion={renderSuggestion}
|
||||
renderInputComponent={renderInputComponent}
|
||||
inputProps={inputProps}
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
||||
<div style={{ float: 'right' }}><Button text='Publish' onClick={this.handleSubmit} disabled={disabled} /></div>
|
||||
|
@ -24,7 +24,7 @@ const UploadButton = React.createClass({
|
||||
return (
|
||||
<div>
|
||||
<Button disabled={this.props.disabled} onClick={this.handleClick} block={true}>
|
||||
<i className='fa fa-fw fa-photo' /> Add images
|
||||
<i className='fa fa-fw fa-photo' /> Add media
|
||||
</Button>
|
||||
|
||||
<input ref='fileElement' type='file' multiple={false} onChange={this.handleChange} disabled={this.props.disabled} style={{ display: 'none' }} />
|
||||
|
@ -1,7 +1,13 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ComposeForm from '../components/compose_form';
|
||||
import { changeCompose, submitCompose, cancelReplyCompose } from '../../../actions/compose';
|
||||
import { makeGetStatus } from '../../../selectors';
|
||||
import { connect } from 'react-redux';
|
||||
import ComposeForm from '../components/compose_form';
|
||||
import {
|
||||
changeCompose,
|
||||
submitCompose,
|
||||
cancelReplyCompose,
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions
|
||||
} from '../../../actions/compose';
|
||||
import { makeGetStatus } from '../../../selectors';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
@ -9,6 +15,7 @@ const makeMapStateToProps = () => {
|
||||
const mapStateToProps = function (state, props) {
|
||||
return {
|
||||
text: state.getIn(['compose', 'text']),
|
||||
suggestions: state.getIn(['compose', 'suggestions']),
|
||||
is_submitting: state.getIn(['compose', 'is_submitting']),
|
||||
is_uploading: state.getIn(['compose', 'is_uploading']),
|
||||
in_reply_to: getStatus(state, state.getIn(['compose', 'in_reply_to']))
|
||||
@ -20,16 +27,24 @@ const makeMapStateToProps = () => {
|
||||
|
||||
const mapDispatchToProps = function (dispatch) {
|
||||
return {
|
||||
onChange: function (text) {
|
||||
onChange (text) {
|
||||
dispatch(changeCompose(text));
|
||||
},
|
||||
|
||||
onSubmit: function () {
|
||||
onSubmit () {
|
||||
dispatch(submitCompose());
|
||||
},
|
||||
|
||||
onCancelReply: function () {
|
||||
onCancelReply () {
|
||||
dispatch(cancelReplyCompose());
|
||||
},
|
||||
|
||||
onClearSuggestions () {
|
||||
dispatch(clearComposeSuggestions());
|
||||
},
|
||||
|
||||
onFetchSuggestions (token) {
|
||||
dispatch(fetchComposeSuggestions(token));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -10,7 +10,9 @@ import {
|
||||
COMPOSE_UPLOAD_SUCCESS,
|
||||
COMPOSE_UPLOAD_FAIL,
|
||||
COMPOSE_UPLOAD_UNDO,
|
||||
COMPOSE_UPLOAD_PROGRESS
|
||||
COMPOSE_UPLOAD_PROGRESS,
|
||||
COMPOSE_SUGGESTIONS_CLEAR,
|
||||
COMPOSE_SUGGESTIONS_READY
|
||||
} from '../actions/compose';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
import { ACCOUNT_SET_SELF } from '../actions/accounts';
|
||||
@ -22,7 +24,8 @@ const initialState = Immutable.Map({
|
||||
is_submitting: false,
|
||||
is_uploading: false,
|
||||
progress: 0,
|
||||
media_attachments: Immutable.List([]),
|
||||
media_attachments: Immutable.List(),
|
||||
suggestions: [],
|
||||
me: null
|
||||
});
|
||||
|
||||
@ -95,6 +98,10 @@ export default function compose(state = initialState, action) {
|
||||
return state.set('progress', Math.round((action.loaded / action.total) * 100));
|
||||
case COMPOSE_MENTION:
|
||||
return state.update('text', text => `${text}@${action.account.get('acct')} `);
|
||||
case COMPOSE_SUGGESTIONS_CLEAR:
|
||||
return state.set('suggestions', []);
|
||||
case COMPOSE_SUGGESTIONS_READY:
|
||||
return state.set('suggestions', action.accounts);
|
||||
case TIMELINE_DELETE:
|
||||
if (action.id === state.get('in_reply_to')) {
|
||||
return state.set('in_reply_to', null);
|
||||
|
@ -266,3 +266,31 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.react-autosuggest__container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.react-autosuggest__suggestions-container {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.react-autosuggest__suggestions-list {
|
||||
background: #9baec8;
|
||||
color: #282c37;
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.react-autosuggest__suggestion {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.react-autosuggest__suggestion--focused {
|
||||
background: #2b90d9;
|
||||
color: #fff;
|
||||
}
|
||||
|
Reference in New Issue
Block a user