about summary refs log tree commit diff
path: root/app/assets/javascripts/components/features
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2016-10-30 18:13:05 +0100
committerEugen Rochko <eugen@zeonfederated.com>2016-10-30 18:13:05 +0100
commitc49f6290eb9c93720bd5407f4320bb0fd6c96ed9 (patch)
tree6b39ea083c41313b1443d71a6b1adaaf1d31f431 /app/assets/javascripts/components/features
parentfa1cc2d05a12783e166f30bf7a0b3239ebccf732 (diff)
Basic username autocomplete for text area
Diffstat (limited to 'app/assets/javascripts/components/features')
-rw-r--r--app/assets/javascripts/components/features/ui/components/compose_form.jsx105
-rw-r--r--app/assets/javascripts/components/features/ui/components/upload_button.jsx2
-rw-r--r--app/assets/javascripts/components/features/ui/containers/compose_form_container.jsx29
3 files changed, 126 insertions, 10 deletions
diff --git a/app/assets/javascripts/components/features/ui/components/compose_form.jsx b/app/assets/javascripts/components/features/ui/components/compose_form.jsx
index 5b00fc1b9..464423cf8 100644
--- a/app/assets/javascripts/components/features/ui/components/compose_form.jsx
+++ b/app/assets/javascripts/components/features/ui/components/compose_form.jsx
@@ -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>
diff --git a/app/assets/javascripts/components/features/ui/components/upload_button.jsx b/app/assets/javascripts/components/features/ui/components/upload_button.jsx
index d1b093242..9e9fc7298 100644
--- a/app/assets/javascripts/components/features/ui/components/upload_button.jsx
+++ b/app/assets/javascripts/components/features/ui/components/upload_button.jsx
@@ -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' }} />
diff --git a/app/assets/javascripts/components/features/ui/containers/compose_form_container.jsx b/app/assets/javascripts/components/features/ui/containers/compose_form_container.jsx
index 163d6fa20..dcfeef752 100644
--- a/app/assets/javascripts/components/features/ui/containers/compose_form_container.jsx
+++ b/app/assets/javascripts/components/features/ui/containers/compose_form_container.jsx
@@ -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));
     }
   }
 };