From 06e0919bca7ee4280c6eed58838ca2be10e7b4fb Mon Sep 17 00:00:00 2001 From: hmhealey Date: Tue, 1 Dec 2015 15:24:53 -0500 Subject: Moved autocomplete components into web/react/components/suggestion --- web/react/components/at_mention_provider.jsx | 100 ---------- web/react/components/command_provider.jsx | 68 ------- web/react/components/search_bar.jsx | 8 +- web/react/components/search_channel_provider.jsx | 69 ------- web/react/components/search_suggestion_list.jsx | 86 --------- web/react/components/search_user_provider.jsx | 62 ------- .../components/suggestion/at_mention_provider.jsx | 100 ++++++++++ .../components/suggestion/command_provider.jsx | 68 +++++++ .../suggestion/search_channel_provider.jsx | 69 +++++++ .../suggestion/search_suggestion_list.jsx | 86 +++++++++ .../components/suggestion/search_user_provider.jsx | 62 +++++++ web/react/components/suggestion/suggestion_box.jsx | 203 +++++++++++++++++++++ .../components/suggestion/suggestion_list.jsx | 130 +++++++++++++ web/react/components/suggestion_box.jsx | 203 --------------------- web/react/components/suggestion_list.jsx | 130 ------------- web/react/components/textbox.jsx | 8 +- 16 files changed, 726 insertions(+), 726 deletions(-) delete mode 100644 web/react/components/at_mention_provider.jsx delete mode 100644 web/react/components/command_provider.jsx delete mode 100644 web/react/components/search_channel_provider.jsx delete mode 100644 web/react/components/search_suggestion_list.jsx delete mode 100644 web/react/components/search_user_provider.jsx create mode 100644 web/react/components/suggestion/at_mention_provider.jsx create mode 100644 web/react/components/suggestion/command_provider.jsx create mode 100644 web/react/components/suggestion/search_channel_provider.jsx create mode 100644 web/react/components/suggestion/search_suggestion_list.jsx create mode 100644 web/react/components/suggestion/search_user_provider.jsx create mode 100644 web/react/components/suggestion/suggestion_box.jsx create mode 100644 web/react/components/suggestion/suggestion_list.jsx delete mode 100644 web/react/components/suggestion_box.jsx delete mode 100644 web/react/components/suggestion_list.jsx (limited to 'web/react') diff --git a/web/react/components/at_mention_provider.jsx b/web/react/components/at_mention_provider.jsx deleted file mode 100644 index 2feaad379..000000000 --- a/web/react/components/at_mention_provider.jsx +++ /dev/null @@ -1,100 +0,0 @@ -// 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'; - -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 = 'Notifies everyone in the team'; - icon = ; - } else if (item.username === 'channel') { - username = 'channel'; - description = 'Notifies everyone in the channel'; - icon = ; - } else { - username = item.username; - description = Utils.getFullName(item); - icon = ( - - ); - } - - let className = 'mentions-name'; - if (isSelection) { - className += ' suggestion--selected'; - } - - return ( -
-
- {icon} -
-
- - {'@' + username} - - - {description} - -
-
- ); - } -} - -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.getProfiles(); - let filtered = []; - - for (const id of Object.keys(users)) { - const user = users[id]; - - if (user.username.startsWith(usernamePrefix)) { - filtered.push(user); - } - } - - // 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); - } - } -} diff --git a/web/react/components/command_provider.jsx b/web/react/components/command_provider.jsx deleted file mode 100644 index 98a89cba0..000000000 --- a/web/react/components/command_provider.jsx +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; -import * as Client from '../utils/client.jsx'; -import Constants from '../utils/constants.jsx'; -import SuggestionStore from '../stores/suggestion_store.jsx'; - -class CommandSuggestion extends React.Component { - render() { - const {item, isSelection, onClick} = this.props; - - let className = 'command-name'; - if (isSelection) { - className += ' suggestion--selected'; - } - - return ( -
-
- {item.suggestion} -
-
- {item.description} -
-
- ); - } -} - -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('/')) { - SuggestionStore.setMatchedPretext(suggestionId, pretext); - - Client.executeCommand( - '', - pretext, - true, - (data) => { - this.handleCommandsReceived(suggestionId, pretext, data.suggestions); - } - ); - } - } - - handleCommandsReceived(suggestionId, matchedPretext, commandSuggestions) { - const terms = commandSuggestions.map(({suggestion}) => suggestion); - - AppDispatcher.handleServerAction({ - type: Constants.ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS, - id: suggestionId, - matchedPretext, - terms, - items: commandSuggestions, - component: CommandSuggestion - }); - } -} diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx index 21a938ae1..77c9e39b9 100644 --- a/web/react/components/search_bar.jsx +++ b/web/react/components/search_bar.jsx @@ -5,10 +5,10 @@ import * as client from '../utils/client.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; import SearchStore from '../stores/search_store.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; -import SuggestionBox from '../components/suggestion_box.jsx'; -import SearchChannelProvider from '../components/search_channel_provider.jsx'; -import SearchSuggestionList from '../components/search_suggestion_list.jsx'; -import SearchUserProvider from '../components/search_user_provider.jsx'; +import SuggestionBox from './suggestion/suggestion_box.jsx'; +import SearchChannelProvider from './suggestion/search_channel_provider.jsx'; +import SearchSuggestionList from './suggestion/search_suggestion_list.jsx'; +import SearchUserProvider from './suggestion/search_user_provider.jsx'; import * as utils from '../utils/utils.jsx'; import Constants from '../utils/constants.jsx'; var ActionTypes = Constants.ActionTypes; diff --git a/web/react/components/search_channel_provider.jsx b/web/react/components/search_channel_provider.jsx deleted file mode 100644 index 6b2fa2d62..000000000 --- a/web/react/components/search_channel_provider.jsx +++ /dev/null @@ -1,69 +0,0 @@ -// 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'; - -class SearchChannelSuggestion extends React.Component { - render() { - const {item, isSelection, onClick} = this.props; - - let className = 'search-autocomplete__item'; - if (isSelection) { - className += ' selected'; - } - - return ( -
- {item.name} -
- ); - } -} - -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); - } - } -} diff --git a/web/react/components/search_suggestion_list.jsx b/web/react/components/search_suggestion_list.jsx deleted file mode 100644 index 1549e90d5..000000000 --- a/web/react/components/search_suggestion_list.jsx +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import Constants from '../utils/constants.jsx'; -import SuggestionList from './suggestion_list.jsx'; -import * as Utils from '../utils/utils.jsx'; - -export default class SearchSuggestionList extends SuggestionList { - componentDidUpdate(prevProps, prevState) { - if (this.state.items.length > 0 && prevState.items.length === 0) { - this.getContent().perfectScrollbar(); - } - } - - getContent() { - return $(ReactDOM.findDOMNode(this.refs.popover)).find('.popover-content'); - } - - renderChannelDivider(type) { - let text; - if (type === Constants.OPEN_CHANNEL) { - text = 'Public ' + Utils.getChannelTerm(type) + 's'; - } else { - text = 'Private ' + Utils.getChannelTerm(type) + 's'; - } - - return ( -
- {text} -
- ); - } - - render() { - if (this.state.items.length === 0 || !this.props.show) { - 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( - - ); - } - - return ( - - {items} - - ); - } -} - -SearchSuggestionList.propTypes = { - ...SuggestionList.propTypes -}; diff --git a/web/react/components/search_user_provider.jsx b/web/react/components/search_user_provider.jsx deleted file mode 100644 index 7c1711d36..000000000 --- a/web/react/components/search_user_provider.jsx +++ /dev/null @@ -1,62 +0,0 @@ -// 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'; - -class SearchUserSuggestion extends React.Component { - render() { - const {item, isSelection, onClick} = this.props; - - let className = 'search-autocomplete__item'; - if (isSelection) { - className += ' selected'; - } - - return ( -
- ; + } else if (item.username === 'channel') { + username = 'channel'; + description = 'Notifies everyone in the channel'; + icon = ; + } else { + username = item.username; + description = Utils.getFullName(item); + icon = ( + + ); + } + + let className = 'mentions-name'; + if (isSelection) { + className += ' suggestion--selected'; + } + + return ( +
+
+ {icon} +
+
+ + {'@' + username} + + + {description} + +
+
+ ); + } +} + +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.getProfiles(); + let filtered = []; + + for (const id of Object.keys(users)) { + const user = users[id]; + + if (user.username.startsWith(usernamePrefix)) { + filtered.push(user); + } + } + + // 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); + } + } +} diff --git a/web/react/components/suggestion/command_provider.jsx b/web/react/components/suggestion/command_provider.jsx new file mode 100644 index 000000000..34de3bfd1 --- /dev/null +++ b/web/react/components/suggestion/command_provider.jsx @@ -0,0 +1,68 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../../dispatcher/app_dispatcher.jsx'; +import * as Client from '../../utils/client.jsx'; +import Constants from '../../utils/constants.jsx'; +import SuggestionStore from '../../stores/suggestion_store.jsx'; + +class CommandSuggestion extends React.Component { + render() { + const {item, isSelection, onClick} = this.props; + + let className = 'command-name'; + if (isSelection) { + className += ' suggestion--selected'; + } + + return ( +
+
+ {item.suggestion} +
+
+ {item.description} +
+
+ ); + } +} + +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('/')) { + SuggestionStore.setMatchedPretext(suggestionId, pretext); + + Client.executeCommand( + '', + pretext, + true, + (data) => { + this.handleCommandsReceived(suggestionId, pretext, data.suggestions); + } + ); + } + } + + handleCommandsReceived(suggestionId, matchedPretext, commandSuggestions) { + const terms = commandSuggestions.map(({suggestion}) => suggestion); + + AppDispatcher.handleServerAction({ + type: Constants.ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS, + id: suggestionId, + matchedPretext, + terms, + items: commandSuggestions, + component: CommandSuggestion + }); + } +} diff --git a/web/react/components/suggestion/search_channel_provider.jsx b/web/react/components/suggestion/search_channel_provider.jsx new file mode 100644 index 000000000..7547a9341 --- /dev/null +++ b/web/react/components/suggestion/search_channel_provider.jsx @@ -0,0 +1,69 @@ +// 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'; + +class SearchChannelSuggestion extends React.Component { + render() { + const {item, isSelection, onClick} = this.props; + + let className = 'search-autocomplete__item'; + if (isSelection) { + className += ' selected'; + } + + return ( +
+ {item.name} +
+ ); + } +} + +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); + } + } +} diff --git a/web/react/components/suggestion/search_suggestion_list.jsx b/web/react/components/suggestion/search_suggestion_list.jsx new file mode 100644 index 000000000..542d28ddd --- /dev/null +++ b/web/react/components/suggestion/search_suggestion_list.jsx @@ -0,0 +1,86 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Constants from '../../utils/constants.jsx'; +import SuggestionList from './suggestion_list.jsx'; +import * as Utils from '../../utils/utils.jsx'; + +export default class SearchSuggestionList extends SuggestionList { + componentDidUpdate(prevProps, prevState) { + if (this.state.items.length > 0 && prevState.items.length === 0) { + this.getContent().perfectScrollbar(); + } + } + + getContent() { + return $(ReactDOM.findDOMNode(this.refs.popover)).find('.popover-content'); + } + + renderChannelDivider(type) { + let text; + if (type === Constants.OPEN_CHANNEL) { + text = 'Public ' + Utils.getChannelTerm(type) + 's'; + } else { + text = 'Private ' + Utils.getChannelTerm(type) + 's'; + } + + return ( +
+ {text} +
+ ); + } + + render() { + if (this.state.items.length === 0 || !this.props.show) { + 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( + + ); + } + + return ( + + {items} + + ); + } +} + +SearchSuggestionList.propTypes = { + ...SuggestionList.propTypes +}; diff --git a/web/react/components/suggestion/search_user_provider.jsx b/web/react/components/suggestion/search_user_provider.jsx new file mode 100644 index 000000000..cf2953937 --- /dev/null +++ b/web/react/components/suggestion/search_user_provider.jsx @@ -0,0 +1,62 @@ +// 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'; + +class SearchUserSuggestion extends React.Component { + render() { + const {item, isSelection, onClick} = this.props; + + let className = 'search-autocomplete__item'; + if (isSelection) { + className += ' selected'; + } + + return ( +
+ 0)) { + // we can't just use blur for this because it fires and hides the children before + // their click handlers can be called + this.setState({ + focused: false + }); + } + } + + handleFocus() { + this.setState({ + focused: true + }); + + if (this.props.onFocus) { + this.props.onFocus(); + } + } + + handleChange(e) { + const textbox = ReactDOM.findDOMNode(this.refs.textbox); + const caret = Utils.getCaretPosition(textbox); + const pretext = textbox.value.substring(0, caret); + + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_PRETEXT_CHANGED, + id: 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) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_SELECT_PREVIOUS, + id: this.suggestionId + }); + e.preventDefault(); + } else if (e.which === KeyCodes.DOWN) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_SELECT_NEXT, + id: this.suggestionId + }); + e.preventDefault(); + } else if (e.which === KeyCodes.SPACE || e.which === KeyCodes.ENTER) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_COMPLETE_WORD, + id: 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, { + onFocus: this.handleFocus, + onChange: this.handleChange, + onKeyDown: this.handleKeyDown + }); + + let textbox = null; + if (this.props.type === 'input') { + textbox = ( + + ); + } else if (this.props.type === 'textarea') { + textbox = ( +