feat: Autocomplete selection wraparound
This commit is contained in:
parent
cd928fe6f5
commit
8961c87cf9
3 changed files with 59 additions and 16 deletions
|
@ -36,7 +36,12 @@
|
||||||
"no-new-wrappers": ["error"],
|
"no-new-wrappers": ["error"],
|
||||||
"no-invalid-regexp": ["error"],
|
"no-invalid-regexp": ["error"],
|
||||||
"no-extra-bind": ["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"],
|
"consistent-return": ["error"],
|
||||||
"valid-jsdoc": ["error"],
|
"valid-jsdoc": ["error"],
|
||||||
"no-use-before-define": ["error"],
|
"no-use-before-define": ["error"],
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
SelectionState
|
SelectionState
|
||||||
} from 'draft-js';
|
} from 'draft-js';
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
|
import * as emojione from 'emojione';
|
||||||
|
|
||||||
const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', {
|
const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', {
|
||||||
element: 'span'
|
element: 'span'
|
||||||
|
@ -35,6 +36,8 @@ const MARKDOWN_REGEX = {
|
||||||
|
|
||||||
const USERNAME_REGEX = /@\S+:\S+/g;
|
const USERNAME_REGEX = /@\S+:\S+/g;
|
||||||
const ROOM_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 {
|
export function contentStateToHTML(contentState: ContentState): string {
|
||||||
return contentState.getBlockMap().map((block) => {
|
return contentState.getBlockMap().map((block) => {
|
||||||
|
@ -89,6 +92,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
|
||||||
return <span className="mx_UserPill">{avatar} {props.children}</span>;
|
return <span className="mx_UserPill">{avatar} {props.children}</span>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let roomDecorator = {
|
let roomDecorator = {
|
||||||
strategy: (contentBlock, callback) => {
|
strategy: (contentBlock, callback) => {
|
||||||
findWithRegex(ROOM_REGEX, 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];
|
return [usernameDecorator, roomDecorator];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,26 +11,28 @@ export default class Autocomplete extends React.Component {
|
||||||
completions: [],
|
completions: [],
|
||||||
|
|
||||||
// how far down the completion list we are
|
// how far down the completion list we are
|
||||||
selectionOffset: 0
|
selectionOffset: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(props, state) {
|
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 {
|
try {
|
||||||
completionResult.completions.then(completions => {
|
completionResult.completions.then(completions => {
|
||||||
let i = this.state.completions.findIndex(
|
let i = this.state.completions.findIndex(
|
||||||
completion => completion.provider === completionResult.provider
|
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);
|
let newCompletions = Object.assign([], this.state.completions);
|
||||||
completionResult.completions = completions;
|
completionResult.completions = completions;
|
||||||
newCompletions[i] = completionResult;
|
newCompletions[i] = completionResult;
|
||||||
this.setState({
|
this.setState({
|
||||||
completions: newCompletions
|
completions: newCompletions,
|
||||||
});
|
});
|
||||||
}, err => {
|
}, err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@ -42,13 +44,25 @@ export default class Autocomplete extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpArrow() {
|
countCompletions(): number {
|
||||||
this.setState({selectionOffset: this.state.selectionOffset - 1});
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onDownArrow() {
|
// called from MessageComposerInput
|
||||||
this.setState({selectionOffset: this.state.selectionOffset + 1});
|
onDownArrow(): boolean {
|
||||||
|
let completionCount = this.countCompletions(),
|
||||||
|
selectionOffset = (this.state.selectionOffset + 1) % completionCount;
|
||||||
|
this.setState({selectionOffset});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +72,7 @@ export default class Autocomplete extends React.Component {
|
||||||
let completions = completionResult.completions.map((completion, i) => {
|
let completions = completionResult.completions.map((completion, i) => {
|
||||||
let Component = completion.component;
|
let Component = completion.component;
|
||||||
let className = classNames('mx_Autocomplete_Completion', {
|
let className = classNames('mx_Autocomplete_Completion', {
|
||||||
'selected': position == this.state.selectionOffset
|
'selected': position === this.state.selectionOffset,
|
||||||
});
|
});
|
||||||
let componentPosition = position;
|
let componentPosition = position;
|
||||||
position++;
|
position++;
|
||||||
|
@ -66,10 +80,12 @@ export default class Autocomplete extends React.Component {
|
||||||
return Component;
|
return Component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let onMouseOver = () => this.setState({selectionOffset: componentPosition});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={i}
|
<div key={i}
|
||||||
className={className}
|
className={className}
|
||||||
onMouseOver={() => this.setState({selectionOffset: componentPosition})}>
|
onMouseOver={onMouseOver}>
|
||||||
<span style={{fontWeight: 600}}>{completion.title}</span>
|
<span style={{fontWeight: 600}}>{completion.title}</span>
|
||||||
<span>{completion.subtitle}</span>
|
<span>{completion.subtitle}</span>
|
||||||
<span style={{flex: 1}} />
|
<span style={{flex: 1}} />
|
||||||
|
@ -82,7 +98,11 @@ export default class Autocomplete extends React.Component {
|
||||||
return completions.length > 0 ? (
|
return completions.length > 0 ? (
|
||||||
<div key={i} className="mx_Autocomplete_ProviderSection">
|
<div key={i} className="mx_Autocomplete_ProviderSection">
|
||||||
<span className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</span>
|
<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}
|
{completions}
|
||||||
</ReactCSSTransitionGroup>
|
</ReactCSSTransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
|
@ -91,7 +111,11 @@ export default class Autocomplete extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_Autocomplete">
|
<div className="mx_Autocomplete">
|
||||||
<ReactCSSTransitionGroup component="div" transitionName="autocomplete" transitionEnterTimeout={300} transitionLeaveTimeout={300}>
|
<ReactCSSTransitionGroup
|
||||||
|
component="div"
|
||||||
|
transitionName="autocomplete"
|
||||||
|
transitionEnterTimeout={300}
|
||||||
|
transitionLeaveTimeout={300}>
|
||||||
{renderedCompletions}
|
{renderedCompletions}
|
||||||
</ReactCSSTransitionGroup>
|
</ReactCSSTransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
|
@ -101,5 +125,5 @@ export default class Autocomplete extends React.Component {
|
||||||
|
|
||||||
Autocomplete.propTypes = {
|
Autocomplete.propTypes = {
|
||||||
// the query string for which to show autocomplete suggestions
|
// the query string for which to show autocomplete suggestions
|
||||||
query: React.PropTypes.string.isRequired
|
query: React.PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue