mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 11:47:23 +03:00
Merge pull request #419 from aviraldg/feature-autocomplete-improvements
Update autocomplete design and scroll it correctly
This commit is contained in:
commit
2f0599aae1
8 changed files with 150 additions and 58 deletions
|
@ -1,4 +1,5 @@
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
export default class AutocompleteProvider {
|
export default class AutocompleteProvider {
|
||||||
constructor(commandRegex?: RegExp, fuseOpts?: any) {
|
constructor(commandRegex?: RegExp, fuseOpts?: any) {
|
||||||
|
@ -51,4 +52,9 @@ export default class AutocompleteProvider {
|
||||||
getName(): string {
|
getName(): string {
|
||||||
return 'Default Provider';
|
return 'Default Provider';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
console.error('stub; should be implemented in subclasses');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return 'Commands';
|
return '*️⃣ Commands';
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(): CommandProvider {
|
static getInstance(): CommandProvider {
|
||||||
|
@ -83,4 +83,10 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
return <div className="mx_Autocomplete_Completion_container_block">
|
||||||
|
{completions}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,62 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export function TextualCompletion({
|
/* These were earlier stateless functional components but had to be converted
|
||||||
|
since we need to use refs/findDOMNode to access the underlying DOM node to focus the correct completion,
|
||||||
|
something that is not entirely possible with stateless functional components. One could
|
||||||
|
presumably wrap them in a <div> before rendering but I think this is the better way to do it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class TextualCompletion extends React.Component {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
description,
|
description,
|
||||||
}: {
|
className,
|
||||||
title: ?string,
|
...restProps,
|
||||||
subtitle: ?string,
|
} = this.props;
|
||||||
description: ?string
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div style={{width: '100%'}}>
|
<div className={classNames('mx_Autocomplete_Completion_block', className)} {...restProps}>
|
||||||
<span>{title}</span>
|
<span className="mx_Autocomplete_Completion_title">{title}</span>
|
||||||
<em>{subtitle}</em>
|
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span>
|
||||||
<span style={{color: 'gray', float: 'right'}}>{description}</span>
|
<span className="mx_Autocomplete_Completion_description">{description}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
TextualCompletion.propTypes = {
|
||||||
|
title: React.PropTypes.string,
|
||||||
|
subtitle: React.PropTypes.string,
|
||||||
|
description: React.PropTypes.string,
|
||||||
|
className: React.PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PillCompletion extends React.Component {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
description,
|
||||||
|
initialComponent,
|
||||||
|
className,
|
||||||
|
...restProps,
|
||||||
|
} = this.props;
|
||||||
|
return (
|
||||||
|
<div className={classNames('mx_Autocomplete_Completion_pill', className)} {...restProps}>
|
||||||
|
{initialComponent}
|
||||||
|
<span className="mx_Autocomplete_Completion_title">{title}</span>
|
||||||
|
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span>
|
||||||
|
<span className="mx_Autocomplete_Completion_description">{description}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PillCompletion.propTypes = {
|
||||||
|
title: React.PropTypes.string,
|
||||||
|
subtitle: React.PropTypes.string,
|
||||||
|
description: React.PropTypes.string,
|
||||||
|
initialComponent: React.PropTypes.element,
|
||||||
|
className: React.PropTypes.string,
|
||||||
|
};
|
||||||
|
|
|
@ -78,7 +78,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return 'Results from DuckDuckGo';
|
return '🔍 Results from DuckDuckGo';
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(): DuckDuckGoProvider {
|
static getInstance(): DuckDuckGoProvider {
|
||||||
|
@ -87,4 +87,10 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
return <div className="mx_Autocomplete_Completion_container_block">
|
||||||
|
{completions}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
|
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
|
import sdk from '../index';
|
||||||
|
import {PillCompletion} from './Components';
|
||||||
|
|
||||||
const EMOJI_REGEX = /:\w*:?/g;
|
const EMOJI_REGEX = /:\w*:?/g;
|
||||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
||||||
|
@ -16,28 +18,28 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||||
|
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection);
|
let {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (command) {
|
||||||
completions = this.fuse.search(command[0]).map(result => {
|
completions = this.fuse.search(command[0]).map(result => {
|
||||||
let shortname = EMOJI_SHORTNAMES[result];
|
const shortname = EMOJI_SHORTNAMES[result];
|
||||||
let imageHTML = shortnameToImage(shortname);
|
const unicode = shortnameToUnicode(shortname);
|
||||||
return {
|
return {
|
||||||
completion: shortnameToUnicode(shortname),
|
completion: unicode,
|
||||||
component: (
|
component: (
|
||||||
<div className="mx_Autocomplete_Completion">
|
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{unicode}</EmojiText>} />
|
||||||
<span style={{maxWidth: '1em'}} dangerouslySetInnerHTML={{__html: imageHTML}}></span> {shortname}
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
}).slice(0, 4);
|
}).slice(0, 8);
|
||||||
}
|
}
|
||||||
return Q.when(completions);
|
return Q.when(completions);
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return 'Emoji';
|
return '😃 Emoji';
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance() {
|
static getInstance() {
|
||||||
|
@ -45,4 +47,10 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
instance = new EmojiProvider();
|
instance = new EmojiProvider();
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
return <div className="mx_Autocomplete_Completion_container_pill">
|
||||||
|
{completions}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,9 @@ import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import {TextualCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import {getDisplayAliasForRoom} from '../MatrixTools';
|
import {getDisplayAliasForRoom} from '../MatrixTools';
|
||||||
|
import sdk from '../index';
|
||||||
|
|
||||||
const ROOM_REGEX = /(?=#)([^\s]*)/g;
|
const ROOM_REGEX = /(?=#)([^\s]*)/g;
|
||||||
|
|
||||||
|
@ -21,6 +22,8 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||||
|
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||||
|
|
||||||
let client = MatrixClientPeg.get();
|
let client = MatrixClientPeg.get();
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
|
@ -39,7 +42,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
return {
|
return {
|
||||||
completion: displayAlias,
|
completion: displayAlias,
|
||||||
component: (
|
component: (
|
||||||
<TextualCompletion title={room.name} description={displayAlias} />
|
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
|
@ -49,7 +52,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return 'Rooms';
|
return '💬 Rooms';
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance() {
|
static getInstance() {
|
||||||
|
@ -59,4 +62,10 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
return <div className="mx_Autocomplete_Completion_container_pill">
|
||||||
|
{completions}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,8 @@ import React from 'react';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import {TextualCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
|
import sdk from '../index';
|
||||||
|
|
||||||
const USER_REGEX = /@[^\s]*/g;
|
const USER_REGEX = /@[^\s]*/g;
|
||||||
|
|
||||||
|
@ -20,6 +21,8 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||||
|
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection);
|
let {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (command) {
|
||||||
|
@ -29,7 +32,8 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
return {
|
return {
|
||||||
completion: user.userId,
|
completion: user.userId,
|
||||||
component: (
|
component: (
|
||||||
<TextualCompletion
|
<PillCompletion
|
||||||
|
initialComponent={<MemberAvatar member={user} width={24} height={24}/>}
|
||||||
title={displayName}
|
title={displayName}
|
||||||
description={user.userId} />
|
description={user.userId} />
|
||||||
),
|
),
|
||||||
|
@ -41,7 +45,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return 'Users';
|
return '👥 Users';
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserList(users) {
|
setUserList(users) {
|
||||||
|
@ -54,4 +58,10 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
return <div className="mx_Autocomplete_Completion_container_pill">
|
||||||
|
{completions}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
import ReactDOM from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import flatMap from 'lodash/flatMap';
|
import flatMap from 'lodash/flatMap';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||||
|
|
||||||
|
@ -100,11 +101,27 @@ export default class Autocomplete extends React.Component {
|
||||||
this.setState({selectionOffset});
|
this.setState({selectionOffset});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
// this is the selected completion, so scroll it into view if needed
|
||||||
|
const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`];
|
||||||
|
if (selectedCompletion && this.container) {
|
||||||
|
const domNode = ReactDOM.findDOMNode(selectedCompletion);
|
||||||
|
const offsetTop = domNode && domNode.offsetTop;
|
||||||
|
if (offsetTop > this.container.scrollTop + this.container.offsetHeight ||
|
||||||
|
offsetTop < this.container.scrollTop) {
|
||||||
|
this.container.scrollTop = offsetTop - this.container.offsetTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||||
|
|
||||||
let position = 0;
|
let position = 0;
|
||||||
let renderedCompletions = this.state.completions.map((completionResult, i) => {
|
let renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||||
let completions = completionResult.completions.map((completion, i) => {
|
let completions = completionResult.completions.map((completion, i) => {
|
||||||
let className = classNames('mx_Autocomplete_Completion', {
|
|
||||||
|
const className = classNames('mx_Autocomplete_Completion', {
|
||||||
'selected': position === this.state.selectionOffset,
|
'selected': position === this.state.selectionOffset,
|
||||||
});
|
});
|
||||||
let componentPosition = position;
|
let componentPosition = position;
|
||||||
|
@ -116,40 +133,27 @@ export default class Autocomplete extends React.Component {
|
||||||
this.onConfirm();
|
this.onConfirm();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return React.cloneElement(completion.component, {
|
||||||
<div key={i}
|
key: i,
|
||||||
className={className}
|
ref: `completion${i}`,
|
||||||
onMouseOver={onMouseOver}
|
className,
|
||||||
onClick={onClick}>
|
onMouseOver,
|
||||||
{completion.component}
|
onClick,
|
||||||
</div>
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
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>
|
<EmojiText element="div" className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</EmojiText>
|
||||||
<ReactCSSTransitionGroup
|
{completionResult.provider.renderCompletions(completions)}
|
||||||
component="div"
|
|
||||||
transitionName="autocomplete"
|
|
||||||
transitionEnterTimeout={300}
|
|
||||||
transitionLeaveTimeout={300}>
|
|
||||||
{completions}
|
|
||||||
</ReactCSSTransitionGroup>
|
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_Autocomplete">
|
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
|
||||||
<ReactCSSTransitionGroup
|
|
||||||
component="div"
|
|
||||||
transitionName="autocomplete"
|
|
||||||
transitionEnterTimeout={300}
|
|
||||||
transitionLeaveTimeout={300}>
|
|
||||||
{renderedCompletions}
|
{renderedCompletions}
|
||||||
</ReactCSSTransitionGroup>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue