Refactor: component Tags.jsx (#4448)

* Refactor: component Tags.jsx

* Revamp Changes lint fix.

* Fixes method binding.

* Destructure Fix.

* Lint fix

* Button Lint Fix.

* Event Key Hash Change.
This commit is contained in:
RakChamp25 2019-10-20 02:36:48 +05:30 committed by Ben Halpern
parent fca98d952d
commit 5028e1a670

View file

@ -28,7 +28,6 @@ const LETTERS = /[a-z]/i;
class Tags extends Component {
constructor(props) {
super(props);
this.state = {
selectedIndex: -1,
searchResults: [],
@ -48,7 +47,8 @@ class Tags extends Component {
}
componentDidMount() {
if (this.props.listing === true) {
const { listing } = this.props;
if (listing === true) {
this.setState({
additionalTags: {
jobs: [
@ -87,148 +87,44 @@ class Tags extends Component {
componentDidUpdate() {
// stop cursor jumping if the user goes back to edit previous tags
const { cursorIdx, prevLen } = this.state;
if (
this.state.cursorIdx < this.textArea.value.length &&
this.textArea.value.length < this.state.prevLen + 1
cursorIdx < this.textArea.value.length &&
this.textArea.value.length < prevLen + 1
) {
this.textArea.selectionEnd = this.state.cursorIdx;
this.textArea.selectionEnd = cursorIdx;
this.textArea.selectionStart = this.textArea.selectionEnd;
}
}
get selected() {
return this.props.defaultValue
const { defaultValue } = this.props;
return defaultValue
.split(',')
.map(item => item !== undefined && item.trim())
.filter(item => item.length > 0);
}
get isTopOfSearchResults() {
return this.state.selectedIndex <= 0;
const { selectedIndex } = this.state;
return selectedIndex <= 0;
}
get isBottomOfSearchResults() {
return this.state.selectedIndex >= this.state.searchResults.length - 1;
const { selectedIndex, searchResults } = this.state;
return selectedIndex >= searchResults.length - 1;
}
get isSearchResultSelected() {
return this.state.selectedIndex > -1;
const { selectedIndex } = this.state;
return selectedIndex > -1;
}
render() {
let searchResultsHTML = '';
const searchResultsRows = this.state.searchResults.map((tag, index) => (
<div
tabIndex="-1"
className={`${this.props.classPrefix}__tagoptionrow ${
this.props.classPrefix
}__tagoptionrow--${
this.state.selectedIndex === index ? 'active' : 'inactive'
}`}
onClick={this.handleTagClick}
data-content={tag.name}
>
{tag.name}
{(tag.rules_html && tag.rules_html.length > 0) ? <button
type='button'
className={`${this.props.classPrefix}__tagsoptionrulesbutton`}
onClick={this.handleRulesClick}
data-content={tag.name}
>
{this.state.showingRulesForTag === tag.name ? 'Hide Rules' : 'View Rules'}
</button> : ''}
<div className={`${this.props.classPrefix}__tagrules--${this.state.showingRulesForTag === tag.name ? 'active' : 'inactive'}`} dangerouslySetInnerHTML={{ __html: tag.rules_html }} />
</div>
));
if (
this.state.searchResults.length > 0 &&
(document.activeElement.id === 'tag-input' || document.activeElement.className === 'articleform__tagsoptionrulesbutton')
) {
searchResultsHTML = (
<div className={`${this.props.classPrefix}__tagsoptions`}>
{searchResultsRows}
<div className={`${this.props.classPrefix}__tagsoptionsbottomrow`}>Some tags have rules and guidelines determined by community moderators</div>
</div>
);
}
return (
<div className={`${this.props.classPrefix}__tagswrapper`}>
{this.props.listing && <label htmlFor="Tags">Tags</label>}
<input
id="tag-input"
type="text"
ref={t => (this.textArea = t)}
className={`${this.props.classPrefix}__tags`}
placeholder={`${this.props.maxTags} tags max, comma separated, no spaces or special characters`}
autoComplete='off'
value={this.props.defaultValue}
onInput={this.handleInput}
onKeyDown={this.handleKeyDown}
onBlur={this.handleFocusChange}
onFocus={this.handleFocusChange}
/>
{searchResultsHTML}
</div>
);
}
handleRulesClick = e => {
e.preventDefault();
if (this.state.showingRulesForTag === e.target.dataset.content) {
this.setState({showingRulesForTag: null});
} else {
this.setState({showingRulesForTag: e.target.dataset.content});
}
}
handleTagClick = e => {
if (e.target.className === 'articleform__tagsoptionrulesbutton') {
return;
}
const input = document.getElementById('tag-input');
input.focus();
this.insertTag(e.target.dataset.content);
};
handleInput = e => {
let { value } = e.target;
// If we start typing immediately after a comma, add a space
// before what we typed.
// e.g. If value = "javascript," and we type a "p",
// the result should be "javascript, p".
if (e.inputType === 'insertText') {
const isTypingAfterComma =
e.target.value[e.target.selectionStart - 2] === ',';
if (isTypingAfterComma) {
value = this.insertSpace(value, e.target.selectionStart - 1);
}
}
if (e.data === ',') {
value += ' ';
}
this.props.onInput(value);
const query = this.getCurrentTagAtSelectionIndex(
e.target.value,
e.target.selectionStart - 1,
);
this.setState({
selectedIndex: 0,
cursorIdx: e.target.selectionStart,
prevLen: this.textArea.value.length,
});
return this.search(query);
};
getCurrentTagAtSelectionIndex(value, index) {
getCurrentTagAtSelectionIndex = (value, index) => {
let tagIndex = 0;
const tagByCharacterIndex = {};
value.split('').map((letter, letterIndex) => {
value.split('').forEach((letter, letterIndex) => {
if (letter === ',') {
tagIndex += 1;
} else {
@ -242,15 +138,37 @@ class Tags extends Component {
return '';
}
return tag.trim();
}
};
// Given an index of the String value, finds the range between commas.
// This is useful when we want to insert a new tag anywhere in the
// comma separated list of tags.
getRangeBetweenCommas = (value, index) => {
let start = 0;
let end = value.length;
const toPreviousComma = value
.slice(0, index)
.split('')
.reverse()
.indexOf(',');
const toNextComma = value.slice(index).indexOf(',');
if (toPreviousComma !== -1) {
start = index - toPreviousComma + 1;
}
if (toNextComma !== -1) {
end = index + toNextComma;
}
return [start, end];
};
handleKeyDown = e => {
const component = this;
if (
component.selected.length === this.props.maxTags &&
e.key === KEYS.COMMA
) {
const { maxTags } = this.props;
if (component.selected.length === maxTags && e.key === KEYS.COMMA) {
e.preventDefault();
return;
}
@ -285,60 +203,65 @@ class Tags extends Component {
) {
this.clearSelectedSearchResult();
}
} else if (
!LETTERS.test(e.key) &&
!NAVIGATION_KEYS.includes(e.key)) {
} else if (!LETTERS.test(e.key) && !NAVIGATION_KEYS.includes(e.key)) {
e.preventDefault();
}
};
insertTag(tag) {
handleRulesClick = e => {
e.preventDefault();
const { showingRulesForTag } = this.state;
if (showingRulesForTag === e.target.dataset.content) {
this.setState({ showingRulesForTag: null });
} else {
this.setState({ showingRulesForTag: e.target.dataset.content });
}
};
handleTagClick = e => {
if (e.target.className === 'articleform__tagsoptionrulesbutton') {
return;
}
const input = document.getElementById('tag-input');
input.focus();
this.insertTag(e.target.dataset.content);
};
const range = this.getRangeBetweenCommas(input.value, input.selectionStart);
const insertingAtEnd = range[1] === input.value.length;
const maxTagsWillBeReached = this.selected.length === this.props.maxTags;
if (insertingAtEnd && !maxTagsWillBeReached) {
tag += ', ';
handleInput = e => {
let { value } = e.target;
// If we start typing immediately after a comma, add a space
// before what we typed.
// e.g. If value = "javascript," and we type a "p",
// the result should be "javascript, p".
if (e.inputType === 'insertText') {
const isTypingAfterComma =
e.target.value[e.target.selectionStart - 2] === ',';
if (isTypingAfterComma) {
value = this.insertSpace(value, e.target.selectionStart - 1);
}
}
// Insert new tag between commas if there are any.
const newInput =
input.value.slice(0, range[0]) +
tag +
input.value.slice(range[1], input.value.length);
this.props.onInput(newInput);
this.resetSearchResults();
this.clearSelectedSearchResult();
}
// Given an index of the String value, finds the range between commas.
// This is useful when we want to insert a new tag anywhere in the
// comma separated list of tags.
getRangeBetweenCommas(value, index) {
let start = 0;
let end = value.length;
const toPreviousComma = value
.slice(0, index)
.split('')
.reverse()
.indexOf(',');
const toNextComma = value.slice(index).indexOf(',');
if (toPreviousComma !== -1) {
start = index - toPreviousComma + 1;
if (e.data === ',') {
value += ' ';
}
if (toNextComma !== -1) {
end = index + toNextComma;
}
/* eslint-disable-next-line react/destructuring-assignment */
this.props.onInput(value);
return [start, end];
}
const query = this.getCurrentTagAtSelectionIndex(
e.target.value,
e.target.selectionStart - 1,
);
handleFocusChange = e => {
this.setState({
selectedIndex: 0,
cursorIdx: e.target.selectionStart,
prevLen: this.textArea.value.length,
});
return this.search(query);
};
handleFocusChange = () => {
const component = this;
setTimeout(() => {
if (document.activeElement.id === 'tag-input') {
@ -348,21 +271,48 @@ class Tags extends Component {
}, 250);
};
insertSpace(value, position) {
insertSpace = (value, position) => {
return `${value.slice(0, position)} ${value.slice(position, value.length)}`;
};
handleTagEnter = e => {
if (e.key === KEYS.RETURN) {
this.handleTagClick();
}
};
insertTag(tag) {
const input = document.getElementById('tag-input');
const { maxTags } = this.props;
const range = this.getRangeBetweenCommas(input.value, input.selectionStart);
const insertingAtEnd = range[1] === input.value.length;
const maxTagsWillBeReached = this.selected.length === maxTags;
let tagValue = tag;
if (insertingAtEnd && !maxTagsWillBeReached) {
tagValue = `${tagValue}, `;
}
// Insert new tag between commas if there are any.
const newInput =
input.value.slice(0, range[0]) +
tagValue +
input.value.slice(range[1], input.value.length);
/* eslint-disable-next-line react/destructuring-assignment */
this.props.onInput(newInput);
this.resetSearchResults();
this.clearSelectedSearchResult();
}
search(query) {
if (query === '') {
return new Promise(resolve => {
setTimeout(() => {
'search query'
this.resetSearchResults();
resolve();
}, 5);
});
}
const { listing } = this.props;
return this.index
.search(query, {
hitsPerPage: 8,
@ -370,7 +320,7 @@ class Tags extends Component {
filters: 'supported:true',
})
.then(content => {
if (this.props.listing === true) {
if (listing === true) {
const { additionalTags } = this.state;
const { category } = this.props;
const additionalItems = (additionalTags[category] || []).filter(
@ -414,10 +364,92 @@ class Tags extends Component {
selectedIndex: -1,
});
}
render() {
let searchResultsHTML = '';
const { searchResults, selectedIndex, showingRulesForTag } = this.state;
const { classPrefix, defaultValue, maxTags, listing } = this.props;
const searchResultsRows = searchResults.map((tag, index) => (
<div
tabIndex="-1"
role="button"
className={`${classPrefix}__tagoptionrow ${classPrefix}__tagoptionrow--${
selectedIndex === index ? 'active' : 'inactive'
}`}
onClick={this.handleTagClick}
onKeyDown={this.handleTagEnter}
data-content={tag.name}
>
{tag.name}
{tag.rules_html && tag.rules_html.length > 0 ? (
<button
type="button"
className={`${classPrefix}__tagsoptionrulesbutton`}
onClick={this.handleRulesClick}
data-content={tag.name}
>
{showingRulesForTag === tag.name ? 'Hide Rules' : 'View Rules'}
</button>
) : (
''
)}
<div
className={`${classPrefix}__tagrules--${
showingRulesForTag === tag.name ? 'active' : 'inactive'
}`}
dangerouslySetInnerHTML={{ __html: tag.rules_html }}
/>
</div>
));
if (
searchResults.length > 0 &&
(document.activeElement.id === 'tag-input' ||
document.activeElement.className ===
'articleform__tagsoptionrulesbutton')
) {
searchResultsHTML = (
<div className={`${classPrefix}__tagsoptions`}>
{searchResultsRows}
<div className={`${classPrefix}__tagsoptionsbottomrow`}>
Some tags have rules and guidelines determined by community
moderators
</div>
</div>
);
}
return (
<div className={`${classPrefix}__tagswrapper`}>
{listing && <label htmlFor="Tags">Tags</label>}
<input
id="tag-input"
type="text"
ref={t => {
this.textArea = t;
return this.textArea;
}}
className={`${classPrefix}__tags`}
placeholder={`${maxTags} tags max, comma separated, no spaces or special characters`}
autoComplete="off"
value={defaultValue}
onInput={this.handleInput}
onKeyDown={this.handleKeyDown}
onBlur={this.handleFocusChange}
onFocus={this.handleFocusChange}
/>
{searchResultsHTML}
</div>
);
}
}
Tags.propTypes = {
defaultValue: PropTypes.string.isRequired,
onInput: PropTypes.func.isRequired,
maxTags: PropTypes.number.isRequired,
classPrefix: PropTypes.string.isRequired,
listing: PropTypes.string.isRequired,
category: PropTypes.string.isRequired,
};
export default Tags;