Converting to Webpack. Stage 1.
Этот коммит содержится в:
120
webapp/components/suggestion/at_mention_provider.jsx
Обычный файл
120
webapp/components/suggestion/at_mention_provider.jsx
Обычный файл
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
const MaxUserSuggestions = 40;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class AtMentionSuggestion extends React.Component {
|
||||
render() {
|
||||
const {item, isSelection, onClick} = this.props;
|
||||
|
||||
let username;
|
||||
let description;
|
||||
let icon;
|
||||
if (item.username === 'all') {
|
||||
username = 'all';
|
||||
description = (
|
||||
<FormattedMessage
|
||||
id='suggestion.mention.all'
|
||||
defaultMessage='Notifies everyone in the team'
|
||||
/>
|
||||
);
|
||||
icon = <i className='mention__image fa fa-users fa-2x'/>;
|
||||
} else if (item.username === 'channel') {
|
||||
username = 'channel';
|
||||
description = (
|
||||
<FormattedMessage
|
||||
id='suggestion.mention.channel'
|
||||
defaultMessage='Notifies everyone in the channel'
|
||||
/>
|
||||
);
|
||||
icon = <i className='mention__image fa fa-users fa-2x'/>;
|
||||
} else {
|
||||
username = item.username;
|
||||
description = Utils.getFullName(item);
|
||||
icon = (
|
||||
<img
|
||||
className='mention__image'
|
||||
src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let className = 'mentions__name';
|
||||
if (isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className='pull-left'>
|
||||
{icon}
|
||||
</div>
|
||||
<div className='pull-left mention--align'>
|
||||
<span>
|
||||
{'@' + username}
|
||||
</span>
|
||||
<span className='mention__fullname'>
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AtMentionSuggestion.propTypes = {
|
||||
item: React.PropTypes.object.isRequired,
|
||||
isSelection: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func
|
||||
};
|
||||
|
||||
export default class AtMentionProvider {
|
||||
handlePretextChanged(suggestionId, pretext) {
|
||||
const captured = (/@([a-z0-9\-\._]*)$/i).exec(pretext);
|
||||
if (captured) {
|
||||
const usernamePrefix = captured[1];
|
||||
|
||||
const users = UserStore.getActiveOnlyProfiles(true);
|
||||
let filtered = [];
|
||||
|
||||
for (const id of Object.keys(users)) {
|
||||
const user = users[id];
|
||||
|
||||
if (user.username.startsWith(usernamePrefix) && user.delete_at <= 0) {
|
||||
filtered.push(user);
|
||||
}
|
||||
|
||||
if (filtered.length >= MaxUserSuggestions) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// add dummy users to represent the @all and @channel special mentions
|
||||
if ('all'.startsWith(usernamePrefix)) {
|
||||
filtered.push({username: 'all'});
|
||||
}
|
||||
|
||||
if ('channel'.startsWith(usernamePrefix)) {
|
||||
filtered.push({username: 'channel'});
|
||||
}
|
||||
|
||||
filtered = filtered.sort((a, b) => a.username.localeCompare(b.username));
|
||||
|
||||
const mentions = filtered.map((user) => '@' + user.username);
|
||||
|
||||
SuggestionStore.setMatchedPretext(suggestionId, captured[0]);
|
||||
SuggestionStore.addSuggestions(suggestionId, mentions, filtered, AtMentionSuggestion);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
webapp/components/suggestion/command_provider.jsx
Обычный файл
45
webapp/components/suggestion/command_provider.jsx
Обычный файл
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class CommandSuggestion extends React.Component {
|
||||
render() {
|
||||
const {item, isSelection, onClick} = this.props;
|
||||
|
||||
let className = 'command';
|
||||
if (isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className='command__title'>
|
||||
<string>{item.suggestion} {item.hint}</string>
|
||||
</div>
|
||||
<div className='command__desc'>
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommandSuggestion.propTypes = {
|
||||
item: React.PropTypes.object.isRequired,
|
||||
isSelection: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func
|
||||
};
|
||||
|
||||
export default class CommandProvider {
|
||||
handlePretextChanged(suggestionId, pretext) {
|
||||
if (pretext.startsWith('/')) {
|
||||
AsyncClient.getSuggestedCommands(pretext, suggestionId, CommandSuggestion);
|
||||
}
|
||||
}
|
||||
}
|
||||
93
webapp/components/suggestion/emoticon_provider.jsx
Обычный файл
93
webapp/components/suggestion/emoticon_provider.jsx
Обычный файл
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
import * as Emoticons from 'utils/emoticons.jsx';
|
||||
|
||||
const MAX_EMOTICON_SUGGESTIONS = 40;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class EmoticonSuggestion extends React.Component {
|
||||
render() {
|
||||
const text = this.props.term;
|
||||
const name = this.props.item;
|
||||
|
||||
let className = 'emoticon-suggestion';
|
||||
if (this.props.isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
<div className='pull-left'>
|
||||
<img
|
||||
alt={text}
|
||||
className='emoticon-suggestion__image'
|
||||
src={Emoticons.getImagePathForEmoticon(name)}
|
||||
title={text}
|
||||
/>
|
||||
</div>
|
||||
<div className='pull-left'>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EmoticonSuggestion.propTypes = {
|
||||
item: React.PropTypes.string.isRequired,
|
||||
term: React.PropTypes.string.isRequired,
|
||||
isSelection: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func
|
||||
};
|
||||
|
||||
export default class EmoticonProvider {
|
||||
handlePretextChanged(suggestionId, pretext) {
|
||||
const captured = (/(?:^|\s)(:([a-zA-Z0-9_+\-]*))$/g).exec(pretext);
|
||||
if (captured) {
|
||||
const text = captured[1];
|
||||
const partialName = captured[2];
|
||||
|
||||
const names = [];
|
||||
|
||||
for (const emoticon of Emoticons.emoticonMap.keys()) {
|
||||
if (emoticon.indexOf(partialName) !== -1) {
|
||||
names.push(emoticon);
|
||||
|
||||
if (names.length >= MAX_EMOTICON_SUGGESTIONS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort the emoticons so that emoticons starting with the entered text come first
|
||||
names.sort((a, b) => {
|
||||
const aPrefix = a.startsWith(partialName);
|
||||
const bPrefix = b.startsWith(partialName);
|
||||
|
||||
if (aPrefix === bPrefix) {
|
||||
return a.localeCompare(b);
|
||||
} else if (aPrefix) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
|
||||
const terms = names.map((name) => ':' + name + ':');
|
||||
|
||||
if (terms.length > 0) {
|
||||
SuggestionStore.setMatchedPretext(suggestionId, text);
|
||||
SuggestionStore.addSuggestions(suggestionId, terms, names, EmoticonSuggestion);
|
||||
|
||||
// force the selection to be cleared since the order of elements may have changed
|
||||
SuggestionStore.clearSelection(suggestionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
webapp/components/suggestion/search_channel_provider.jsx
Обычный файл
71
webapp/components/suggestion/search_channel_provider.jsx
Обычный файл
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class SearchChannelSuggestion extends React.Component {
|
||||
render() {
|
||||
const {item, isSelection, onClick} = this.props;
|
||||
|
||||
let className = 'search-autocomplete__item';
|
||||
if (isSelection) {
|
||||
className += ' selected';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
>
|
||||
<i className='fa fa fa-plus-square'></i>{item.name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchChannelSuggestion.propTypes = {
|
||||
item: React.PropTypes.object.isRequired,
|
||||
isSelection: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func
|
||||
};
|
||||
|
||||
export default class SearchChannelProvider {
|
||||
handlePretextChanged(suggestionId, pretext) {
|
||||
const captured = (/\b(?:in|channel):\s*(\S*)$/i).exec(pretext);
|
||||
if (captured) {
|
||||
const channelPrefix = captured[1];
|
||||
|
||||
const channels = ChannelStore.getAll();
|
||||
const publicChannels = [];
|
||||
const privateChannels = [];
|
||||
|
||||
for (const id of Object.keys(channels)) {
|
||||
const channel = channels[id];
|
||||
|
||||
// don't show direct channels
|
||||
if (channel.type !== Constants.DM_CHANNEL && channel.name.startsWith(channelPrefix)) {
|
||||
if (channel.type === Constants.OPEN_CHANNEL) {
|
||||
publicChannels.push(channel);
|
||||
} else {
|
||||
privateChannels.push(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publicChannels.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const publicChannelNames = publicChannels.map((channel) => channel.name);
|
||||
|
||||
privateChannels.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const privateChannelNames = privateChannels.map((channel) => channel.name);
|
||||
|
||||
SuggestionStore.setMatchedPretext(suggestionId, channelPrefix);
|
||||
|
||||
SuggestionStore.addSuggestions(suggestionId, publicChannelNames, publicChannels, SearchChannelSuggestion);
|
||||
SuggestionStore.addSuggestions(suggestionId, privateChannelNames, privateChannels, SearchChannelSuggestion);
|
||||
}
|
||||
}
|
||||
}
|
||||
95
webapp/components/suggestion/search_suggestion_list.jsx
Обычный файл
95
webapp/components/suggestion/search_suggestion_list.jsx
Обычный файл
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import SuggestionList from './suggestion_list.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Popover} from 'react-bootstrap';
|
||||
|
||||
export default class SearchSuggestionList extends SuggestionList {
|
||||
getContent() {
|
||||
return $(ReactDOM.findDOMNode(this.refs.popover)).find('.popover-content');
|
||||
}
|
||||
|
||||
renderChannelDivider(type) {
|
||||
let text;
|
||||
if (type === Constants.OPEN_CHANNEL) {
|
||||
text = (
|
||||
<FormattedMessage
|
||||
id='suggestion.search.public'
|
||||
defaultMessage='Public Channels'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
text = (
|
||||
<FormattedMessage
|
||||
id='suggestion.search.private'
|
||||
defaultMessage='Private Groups'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={type + '-divider'}
|
||||
className='search-autocomplete__divider'
|
||||
>
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = [];
|
||||
for (let i = 0; i < this.state.items.length; i++) {
|
||||
const item = this.state.items[i];
|
||||
const term = this.state.terms[i];
|
||||
const isSelection = term === this.state.selection;
|
||||
|
||||
// ReactComponent names need to be upper case when used in JSX
|
||||
const Component = this.state.components[i];
|
||||
|
||||
// temporary hack to add dividers between public and private channels in the search suggestion list
|
||||
if (i === 0 || item.type !== this.state.items[i - 1].type) {
|
||||
if (item.type === Constants.OPEN_CHANNEL) {
|
||||
items.push(this.renderChannelDivider(Constants.OPEN_CHANNEL));
|
||||
} else if (item.type === Constants.PRIVATE_CHANNEL) {
|
||||
items.push(this.renderChannelDivider(Constants.PRIVATE_CHANNEL));
|
||||
}
|
||||
}
|
||||
|
||||
items.push(
|
||||
<Component
|
||||
key={term}
|
||||
ref={term}
|
||||
item={item}
|
||||
isSelection={isSelection}
|
||||
onClick={this.handleItemClick.bind(this, term)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
ref='popover'
|
||||
id='search-autocomplete__popover'
|
||||
className='search-help-popover autocomplete visible'
|
||||
placement='bottom'
|
||||
>
|
||||
{items}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchSuggestionList.propTypes = {
|
||||
...SuggestionList.propTypes
|
||||
};
|
||||
64
webapp/components/suggestion/search_user_provider.jsx
Обычный файл
64
webapp/components/suggestion/search_user_provider.jsx
Обычный файл
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class SearchUserSuggestion extends React.Component {
|
||||
render() {
|
||||
const {item, isSelection, onClick} = this.props;
|
||||
|
||||
let className = 'search-autocomplete__item';
|
||||
if (isSelection) {
|
||||
className += ' selected';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<img
|
||||
className='profile-img rounded'
|
||||
src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at}
|
||||
/>
|
||||
<i className='fa fa fa-plus-square'></i>{item.username}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchUserSuggestion.propTypes = {
|
||||
item: React.PropTypes.object.isRequired,
|
||||
isSelection: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func
|
||||
};
|
||||
|
||||
export default class SearchUserProvider {
|
||||
handlePretextChanged(suggestionId, pretext) {
|
||||
const captured = (/\bfrom:\s*(\S*)$/i).exec(pretext);
|
||||
if (captured) {
|
||||
const usernamePrefix = captured[1];
|
||||
|
||||
const users = UserStore.getProfiles();
|
||||
let filtered = [];
|
||||
|
||||
for (const id of Object.keys(users)) {
|
||||
const user = users[id];
|
||||
|
||||
if (user.username.startsWith(usernamePrefix)) {
|
||||
filtered.push(user);
|
||||
}
|
||||
}
|
||||
|
||||
filtered = filtered.sort((a, b) => a.username.localeCompare(b.username));
|
||||
|
||||
const usernames = filtered.map((user) => user.username);
|
||||
|
||||
SuggestionStore.setMatchedPretext(suggestionId, usernamePrefix);
|
||||
SuggestionStore.addSuggestions(suggestionId, usernames, filtered, SearchUserSuggestion);
|
||||
}
|
||||
}
|
||||
}
|
||||
167
webapp/components/suggestion/suggestion_box.jsx
Обычный файл
167
webapp/components/suggestion/suggestion_box.jsx
Обычный файл
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import * as GlobalActions from 'action_creators/global_actions.jsx';
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
const KeyCodes = Constants.KeyCodes;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class SuggestionBox extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleDocumentClick = this.handleDocumentClick.bind(this);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleCompleteWord = this.handleCompleteWord.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handlePretextChanged = this.handlePretextChanged.bind(this);
|
||||
|
||||
this.suggestionId = Utils.generateId();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
SuggestionStore.registerSuggestionBox(this.suggestionId);
|
||||
$(document).on('click', this.handleDocumentClick);
|
||||
|
||||
SuggestionStore.addCompleteWordListener(this.suggestionId, this.handleCompleteWord);
|
||||
SuggestionStore.addPretextChangedListener(this.suggestionId, this.handlePretextChanged);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
SuggestionStore.removeCompleteWordListener(this.suggestionId, this.handleCompleteWord);
|
||||
SuggestionStore.removePretextChangedListener(this.suggestionId, this.handlePretextChanged);
|
||||
|
||||
SuggestionStore.unregisterSuggestionBox(this.suggestionId);
|
||||
$(document).off('click', this.handleDocumentClick);
|
||||
}
|
||||
|
||||
getTextbox() {
|
||||
// this is to support old code that looks at the input/textarea DOM nodes
|
||||
return ReactDOM.findDOMNode(this.refs.textbox);
|
||||
}
|
||||
|
||||
handleDocumentClick(e) {
|
||||
const container = $(ReactDOM.findDOMNode(this));
|
||||
if (!(container.is(e.target) || container.has(e.target).length > 0)) {
|
||||
// we can't just use blur for this because it fires and hides the children before
|
||||
// their click handlers can be called
|
||||
GlobalActions.emitClearSuggestions(this.suggestionId);
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
const textbox = ReactDOM.findDOMNode(this.refs.textbox);
|
||||
const caret = Utils.getCaretPosition(textbox);
|
||||
const pretext = textbox.value.substring(0, caret);
|
||||
|
||||
GlobalActions.emitSuggestionPretextChanged(this.suggestionId, pretext);
|
||||
|
||||
if (this.props.onUserInput) {
|
||||
this.props.onUserInput(textbox.value);
|
||||
}
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleCompleteWord(term) {
|
||||
const textbox = ReactDOM.findDOMNode(this.refs.textbox);
|
||||
const caret = Utils.getCaretPosition(textbox);
|
||||
|
||||
const text = this.props.value;
|
||||
const prefix = text.substring(0, caret - SuggestionStore.getMatchedPretext(this.suggestionId).length);
|
||||
const suffix = text.substring(caret);
|
||||
|
||||
if (this.props.onUserInput) {
|
||||
this.props.onUserInput(prefix + term + ' ' + suffix);
|
||||
}
|
||||
|
||||
// set the caret position after the next rendering
|
||||
window.requestAnimationFrame(() => {
|
||||
Utils.setCaretPosition(textbox, prefix.length + term.length + 1);
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (SuggestionStore.hasSuggestions(this.suggestionId)) {
|
||||
if (e.which === KeyCodes.UP) {
|
||||
GlobalActions.emitSelectPreviousSuggestion(this.suggestionId);
|
||||
e.preventDefault();
|
||||
} else if (e.which === KeyCodes.DOWN) {
|
||||
GlobalActions.emitSelectNextSuggestion(this.suggestionId);
|
||||
e.preventDefault();
|
||||
} else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.TAB) {
|
||||
GlobalActions.emitCompleteWordSuggestion(this.suggestionId);
|
||||
e.preventDefault();
|
||||
} else if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
} else if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
}
|
||||
|
||||
handlePretextChanged(pretext) {
|
||||
for (const provider of this.props.providers) {
|
||||
provider.handlePretextChanged(this.suggestionId, pretext);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const newProps = Object.assign({}, this.props, {
|
||||
onChange: this.handleChange,
|
||||
onKeyDown: this.handleKeyDown
|
||||
});
|
||||
|
||||
let textbox = null;
|
||||
if (this.props.type === 'input') {
|
||||
textbox = (
|
||||
<input
|
||||
ref='textbox'
|
||||
type='text'
|
||||
{...newProps}
|
||||
/>
|
||||
);
|
||||
} else if (this.props.type === 'textarea') {
|
||||
textbox = (
|
||||
<textarea
|
||||
ref='textbox'
|
||||
{...newProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const SuggestionListComponent = this.props.listComponent;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{textbox}
|
||||
<SuggestionListComponent suggestionId={this.suggestionId}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SuggestionBox.defaultProps = {
|
||||
type: 'input'
|
||||
};
|
||||
|
||||
SuggestionBox.propTypes = {
|
||||
listComponent: React.PropTypes.func.isRequired,
|
||||
type: React.PropTypes.oneOf(['input', 'textarea']).isRequired,
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onUserInput: React.PropTypes.func,
|
||||
providers: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
|
||||
// explicitly name any input event handlers we override and need to manually call
|
||||
onChange: React.PropTypes.func,
|
||||
onKeyDown: React.PropTypes.func
|
||||
};
|
||||
129
webapp/components/suggestion/suggestion_list.jsx
Обычный файл
129
webapp/components/suggestion/suggestion_list.jsx
Обычный файл
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as GlobalActions from 'action_creators/global_actions.jsx';
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class SuggestionList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getContent = this.getContent.bind(this);
|
||||
|
||||
this.handleItemClick = this.handleItemClick.bind(this);
|
||||
this.handleSuggestionsChanged = this.handleSuggestionsChanged.bind(this);
|
||||
|
||||
this.scrollToItem = this.scrollToItem.bind(this);
|
||||
|
||||
this.state = {
|
||||
items: [],
|
||||
terms: [],
|
||||
components: [],
|
||||
selection: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
SuggestionStore.addSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
SuggestionStore.removeSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
|
||||
}
|
||||
|
||||
getContent() {
|
||||
return $(ReactDOM.findDOMNode(this.refs.content));
|
||||
}
|
||||
|
||||
handleItemClick(term, e) {
|
||||
GlobalActions.emitCompleteWordSuggestion(this.props.suggestionId, term);
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
handleSuggestionsChanged() {
|
||||
const selection = SuggestionStore.getSelection(this.props.suggestionId);
|
||||
|
||||
this.setState({
|
||||
items: SuggestionStore.getItems(this.props.suggestionId),
|
||||
terms: SuggestionStore.getTerms(this.props.suggestionId),
|
||||
components: SuggestionStore.getComponents(this.props.suggestionId),
|
||||
selection
|
||||
});
|
||||
|
||||
if (selection) {
|
||||
window.requestAnimationFrame(() => this.scrollToItem(this.state.selection));
|
||||
}
|
||||
}
|
||||
|
||||
scrollToItem(term) {
|
||||
const content = this.getContent();
|
||||
const visibleContentHeight = content[0].clientHeight;
|
||||
const actualContentHeight = content[0].scrollHeight;
|
||||
|
||||
if (visibleContentHeight < actualContentHeight) {
|
||||
const contentTop = content.scrollTop();
|
||||
const contentTopPadding = parseInt(content.css('padding-top'), 10);
|
||||
const contentBottomPadding = parseInt(content.css('padding-top'), 10);
|
||||
|
||||
const item = $(ReactDOM.findDOMNode(this.refs[term]));
|
||||
const itemTop = item[0].offsetTop - parseInt(item.css('margin-top'), 10);
|
||||
const itemBottomMargin = parseInt(item.css('margin-bottom'), 10) + parseInt(item.css('padding-bottom'), 10);
|
||||
const itemBottom = item[0].offsetTop + item.height() + itemBottomMargin;
|
||||
|
||||
if (itemTop - contentTopPadding < contentTop) {
|
||||
// the item is off the top of the visible space
|
||||
content.scrollTop(itemTop - contentTopPadding);
|
||||
} else if (itemBottom + contentTopPadding + contentBottomPadding > contentTop + visibleContentHeight) {
|
||||
// the item has gone off the bottom of the visible space
|
||||
content.scrollTop(itemBottom - visibleContentHeight + contentTopPadding + contentBottomPadding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = [];
|
||||
for (let i = 0; i < this.state.items.length; i++) {
|
||||
const item = this.state.items[i];
|
||||
const term = this.state.terms[i];
|
||||
const isSelection = term === this.state.selection;
|
||||
|
||||
// ReactComponent names need to be upper case when used in JSX
|
||||
const Component = this.state.components[i];
|
||||
|
||||
items.push(
|
||||
<Component
|
||||
key={term}
|
||||
ref={term}
|
||||
item={item}
|
||||
term={term}
|
||||
isSelection={isSelection}
|
||||
onClick={this.handleItemClick.bind(this, term)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='suggestion-list suggestion-list--top'>
|
||||
<div
|
||||
ref='content'
|
||||
className='suggestion-list__content suggestion-list__content--top'
|
||||
>
|
||||
{items}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SuggestionList.propTypes = {
|
||||
suggestionId: React.PropTypes.string.isRequired
|
||||
};
|
||||
Ссылка в новой задаче
Block a user