feat: Autocomplete selection wraparound

pull/21833/head
Aviral Dasgupta 2016-07-03 01:11:34 +05:30
parent cd928fe6f5
commit 8961c87cf9
3 changed files with 59 additions and 16 deletions

View File

@ -36,7 +36,12 @@
"no-new-wrappers": ["error"],
"no-invalid-regexp": ["error"],
"no-extra-bind": ["error"],
"no-magic-numbers": ["error"],
"no-magic-numbers": ["error", {
"ignore": [-1, 0, 1], // usually used in array/string indexing
"ignoreArrayIndexes": true,
"enforceConst": true,
"detectObjects": true
}],
"consistent-return": ["error"],
"valid-jsdoc": ["error"],
"no-use-before-define": ["error"],

View File

@ -9,6 +9,7 @@ import {
SelectionState
} from 'draft-js';
import * as sdk from './index';
import * as emojione from 'emojione';
const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', {
element: 'span'
@ -35,6 +36,8 @@ const MARKDOWN_REGEX = {
const USERNAME_REGEX = /@\S+:\S+/g;
const ROOM_REGEX = /#\S+:\S+/g;
let EMOJI_REGEX = null;
window.EMOJI_REGEX = EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g');
export function contentStateToHTML(contentState: ContentState): string {
return contentState.getBlockMap().map((block) => {
@ -89,6 +92,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
return <span className="mx_UserPill">{avatar} {props.children}</span>;
}
};
let roomDecorator = {
strategy: (contentBlock, callback) => {
findWithRegex(ROOM_REGEX, contentBlock, callback);
@ -98,6 +102,16 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
}
};
// Unused for now, due to https://github.com/facebook/draft-js/issues/414
let emojiDecorator = {
strategy: (contentBlock, callback) => {
findWithRegex(EMOJI_REGEX, contentBlock, callback);
},
component: (props) => {
return <span dangerouslySetInnerHTML={{__html: ' ' + emojione.unicodeToImage(props.children[0].props.text)}}/>
}
};
return [usernameDecorator, roomDecorator];
}

View File

@ -11,26 +11,28 @@ export default class Autocomplete extends React.Component {
completions: [],
// how far down the completion list we are
selectionOffset: 0
selectionOffset: 0,
};
}
componentWillReceiveProps(props, state) {
if(props.query == this.props.query) return;
if (props.query === this.props.query) {
return;
}
getCompletions(props.query, props.selection).map(completionResult => {
getCompletions(props.query, props.selection).forEach(completionResult => {
try {
completionResult.completions.then(completions => {
let i = this.state.completions.findIndex(
completion => completion.provider === completionResult.provider
);
i = i == -1 ? this.state.completions.length : i;
i = i === -1 ? this.state.completions.length : i;
let newCompletions = Object.assign([], this.state.completions);
completionResult.completions = completions;
newCompletions[i] = completionResult;
this.setState({
completions: newCompletions
completions: newCompletions,
});
}, err => {
console.error(err);
@ -42,13 +44,25 @@ export default class Autocomplete extends React.Component {
});
}
onUpArrow() {
this.setState({selectionOffset: this.state.selectionOffset - 1});
countCompletions(): number {
return this.state.completions.map(completionResult => {
return completionResult.completions.length;
}).reduce((l, r) => l + r);
}
// called from MessageComposerInput
onUpArrow(): boolean {
let completionCount = this.countCompletions(),
selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount;
this.setState({selectionOffset});
return true;
}
onDownArrow() {
this.setState({selectionOffset: this.state.selectionOffset + 1});
// called from MessageComposerInput
onDownArrow(): boolean {
let completionCount = this.countCompletions(),
selectionOffset = (this.state.selectionOffset + 1) % completionCount;
this.setState({selectionOffset});
return true;
}
@ -58,18 +72,20 @@ export default class Autocomplete extends React.Component {
let completions = completionResult.completions.map((completion, i) => {
let Component = completion.component;
let className = classNames('mx_Autocomplete_Completion', {
'selected': position == this.state.selectionOffset
'selected': position === this.state.selectionOffset,
});
let componentPosition = position;
position++;
if(Component) {
if (Component) {
return Component;
}
let onMouseOver = () => this.setState({selectionOffset: componentPosition});
return (
<div key={i}
className={className}
onMouseOver={() => this.setState({selectionOffset: componentPosition})}>
onMouseOver={onMouseOver}>
<span style={{fontWeight: 600}}>{completion.title}</span>
<span>{completion.subtitle}</span>
<span style={{flex: 1}} />
@ -82,7 +98,11 @@ export default class Autocomplete extends React.Component {
return completions.length > 0 ? (
<div key={i} className="mx_Autocomplete_ProviderSection">
<span className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</span>
<ReactCSSTransitionGroup component="div" transitionName="autocomplete" transitionEnterTimeout={300} transitionLeaveTimeout={300}>
<ReactCSSTransitionGroup
component="div"
transitionName="autocomplete"
transitionEnterTimeout={300}
transitionLeaveTimeout={300}>
{completions}
</ReactCSSTransitionGroup>
</div>
@ -91,7 +111,11 @@ export default class Autocomplete extends React.Component {
return (
<div className="mx_Autocomplete">
<ReactCSSTransitionGroup component="div" transitionName="autocomplete" transitionEnterTimeout={300} transitionLeaveTimeout={300}>
<ReactCSSTransitionGroup
component="div"
transitionName="autocomplete"
transitionEnterTimeout={300}
transitionLeaveTimeout={300}>
{renderedCompletions}
</ReactCSSTransitionGroup>
</div>
@ -101,5 +125,5 @@ export default class Autocomplete extends React.Component {
Autocomplete.propTypes = {
// the query string for which to show autocomplete suggestions
query: React.PropTypes.string.isRequired
query: React.PropTypes.string.isRequired,
};