Instantly share code, notes, and snippets.
Last active
May 1, 2018 08:58
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save konstantin24121/a4494cf465ce91843bcda7b7df8aefd4 to your computer and use it in GitHub Desktop.
Component example from Relap-UI (Bigger, Longer & Uncut)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { PureComponent } from 'react'; | |
import PropTypes from 'prop-types'; | |
import classNames from 'classnames'; | |
/* subcomponents */ | |
import A from '../A'; | |
import DropdownFiltered from '../DropdownFiltered'; | |
import InlineSVG from '../InlineSVG'; | |
import Tooltip from '../Tooltip'; | |
/* component styles */ | |
import { styles } from './styles.scss'; | |
/** | |
* Поле для редактирования коллекции тегов | |
*/ | |
export default class TagsEditable extends PureComponent { | |
static propTypes = { | |
/** | |
* Коллекция тегов | |
*/ | |
currentTags: PropTypes.arrayOf( | |
PropTypes.shape({ | |
/** | |
* Идентификатор | |
*/ | |
id: PropTypes.oneOfType([ | |
PropTypes.string, | |
PropTypes.number, | |
]), | |
/** | |
* Заголовок | |
*/ | |
title: PropTypes.string.isRequired, | |
/** | |
* Опции для компонента Tooltip | |
*/ | |
tooltip: PropTypes.object, | |
}), | |
).isRequired, | |
/** | |
* Коллекция возможных тегов, выводится в дропдауне | |
*/ | |
suggestionTags: PropTypes.arrayOf( | |
PropTypes.shape({ | |
id: PropTypes.oneOfType([ | |
PropTypes.string, | |
PropTypes.number, | |
]).isRequired, | |
title: PropTypes.string.isRequired, | |
tooltip: PropTypes.object, | |
}), | |
), | |
/** | |
* Разделитель для тегов | |
*/ | |
delimiter: PropTypes.string, | |
/** | |
* Стиль тегов | |
*/ | |
mod: PropTypes.oneOf(['inline', 'label']), | |
/** | |
* Тип компонента. field - поле для ввода. editable - блок с кнопкой редактирования | |
*/ | |
type: PropTypes.oneOf(['field', 'editable']), | |
/** | |
* Плейсхолдер | |
*/ | |
placeholder: PropTypes.string, | |
/** | |
* Скрывать введеные теги из коллекции возможных? | |
*/ | |
hideUsedTags: PropTypes.bool, | |
/** | |
* Размер буфера компонента, для сохранения состояний тегов | |
*/ | |
bufferSize: PropTypes.number, | |
/** | |
* Кнопка редактирования выравнивается по последнему тегу? | |
*/ | |
withFloatingEditableIcon: PropTypes.bool, | |
/** | |
* Событие при сохранении | |
*/ | |
onSave: PropTypes.func, | |
/** | |
* Событие при вводе нового тега | |
*/ | |
onEnter: PropTypes.func, | |
/** | |
* Событие при выборе тега из коллекции возможных | |
*/ | |
onSuggestionChoose: PropTypes.func, | |
/** | |
* Событие при добавлении нового тега | |
*/ | |
onAdd: PropTypes.func, | |
/** | |
* Событие при изменении тегов | |
*/ | |
onChange: PropTypes.func, | |
/** | |
* Событие при удалении тега | |
*/ | |
onDelete: PropTypes.func, | |
}; | |
static defaultProps = { | |
currentTags: [], | |
suggestionTags: [], | |
delimiter: ' ', | |
mod: 'inline', | |
type: 'editable', | |
bufferSize: 3, | |
hideUsedTags: true, | |
placeholder: 'Добавить тег', | |
withFloatingEditableIcon: false, | |
/* eslint-disable no-unused-vars */ | |
onSave: (currentTags) => {}, | |
onEnter: (value) => {}, | |
onSuggestionChoose: ({ id }) => {}, | |
onAdd: ({ value, id }) => {}, | |
onChange: ({ tags }) => {}, | |
onDelete: ({ value, id, index }) => {}, | |
/* eslint-enable no-unused-vars */ | |
}; | |
constructor(props) { | |
super(props); | |
this.state = { | |
isActive: false, | |
/** | |
* Тег на котором стоит фокус | |
*/ | |
currentSelectedTagIdx: null, | |
/** | |
* Выбранные теги | |
*/ | |
selectedTagsIdx: [], | |
currentTags: props.currentTags, | |
value: '', | |
isFocused: false, | |
/** | |
* Массив предыдущих состояний. Используется при undo событии | |
*/ | |
timetravel: [], | |
}; | |
} | |
componentWillReceiveProps(nextProps) { | |
this.setState({ currentTags: nextProps.currentTags }); | |
} | |
componentDidUpdate(prevProps, prevState) { | |
{ | |
const prevIsActive = prevState.isActive; | |
const currIsActive = this.state.isActive; | |
if (currIsActive && !prevIsActive) { | |
document.addEventListener('click', this.handleBodyClick); | |
document.addEventListener('keydown', this.handleBodyKeyDown); | |
} | |
if (!currIsActive && prevIsActive) { | |
document.removeEventListener('click', this.handleBodyClick); | |
document.removeEventListener('keydown', this.handleBodyKeyDown); | |
} | |
} | |
// Tags collection are changed | |
// Так как теги могут быть только добавлены и удалены | |
// есть смысл сравнить только длинны массивов | |
if (this.state.currentTags.length !== prevState.currentTags.length && | |
this.state.timetravel.length === prevState.timetravel.length) { | |
const { bufferSize } = this.props; | |
const prevTimetravel = this.state.timetravel.slice(); | |
prevTimetravel.unshift({ | |
currentTags: prevState.currentTags, | |
}); | |
const timetravel = prevTimetravel.slice(0, bufferSize); | |
// eslint-disable-next-line react/no-did-update-set-state | |
this.setState({ timetravel }); | |
this.props.onChange({ tags: this.state.currentTags }); | |
} | |
} | |
componentWillUnmount() { | |
document.removeEventListener('click', this.handleBodyClick); | |
document.removeEventListener('keydown', this.handleBodyKeyDown); | |
} | |
/** | |
* Добавить тег в коллекцию | |
* @param {Number|String|null} id | |
* @param {String} title | |
* @param {String} newStateValue новое значение для поля ввода | |
*/ | |
addTag = (id, title, newStateValue) => { | |
const { currentTags } = this.state; | |
const trimTitle = title.trim(); | |
this.setState({ | |
currentTags: [...currentTags, { id, title: trimTitle }], | |
value: newStateValue, | |
isFocused: true, | |
}, () => { | |
this.props.onAdd({ value: trimTitle, id }); | |
this.field.focus(); | |
}); | |
}; | |
/** | |
* Сохранить теги, вызвать событие сохранения и прекратить редактирование | |
* @return {void} | |
*/ | |
saveTagsAndCloseField = () => { | |
const { type } = this.props; | |
const isField = type === 'field'; | |
const { value, currentTags } = this.state; | |
this.setState({ | |
value: '', | |
isActive: false, | |
currentSelectedTagIdx: null, | |
selectedTagsIdx: [], | |
isFocused: false, | |
currentTags: !value ? currentTags : [...currentTags, { id: null, title: value.trim() }], | |
}, () => { | |
const currentTagsIds = this.props.currentTags.map(tag => tag.id); | |
if (this.state.currentTags.every(({ id }) => currentTagsIds.includes(id))) return; | |
if (value) this.props.onAdd({ value: value.trim() }); | |
if (isField) { | |
this.field.blur(); | |
} else { | |
this.props.onSave(this.state.currentTags); | |
} | |
}); | |
}; | |
/** | |
* Сбросить выделеные теги и вернуть фокус на поле редактирования | |
* @return {void} | |
*/ | |
clearnSelected = () => { | |
this.setState({ | |
currentSelectedTagIdx: null, | |
selectedTagsIdx: [], | |
isFocused: true, | |
}, () => this.field.focus()); | |
}; | |
/** | |
* Сгенерить событие onDelete | |
* @param {Array} removedTags коллекция удаленных тегов | |
* @param {Array} selectedTags индексы удаленных тегов | |
* @return {void} | |
*/ | |
fireOnDelete = (removedTags, selectedTags) => () => { | |
removedTags.forEach((tag, index) => { | |
this.props.onDelete({ | |
value: tag.title, | |
id: tag.id, | |
index: selectedTags[index], | |
}); | |
}); | |
}; | |
/** | |
* Клик на кнопку редактирования | |
* @return {void} | |
*/ | |
handlePencilClick = () => { | |
const { isActive } = this.state; | |
if (isActive) { | |
this.saveTagsAndCloseField(); | |
} else { | |
this.setState({ isActive: true, isFocused: true }, () => this.field.focus()); | |
} | |
}; | |
/** | |
* Клик по документу | |
* @param {ClickEvent} e | |
* @return {void} | |
*/ | |
handleBodyClick = (e) => { | |
if (this.root.contains(e.target)) return; | |
this.saveTagsAndCloseField(); | |
}; | |
/** | |
* Нажатие клавиши обрабатываемое на документе | |
* Срабатывает только если компонент активен | |
* @param {KeyboardEvent} e | |
* @return {void} | |
*/ | |
handleBodyKeyDown = (e) => { | |
const { keyCode, ctrlKey, metaKey, shiftKey } = e; | |
const { currentSelectedTagIdx, currentTags, isFocused, selectedTagsIdx, value } = this.state; | |
const specialKey = ctrlKey || metaKey; | |
// Если нажата метаклавиша* | |
if (specialKey) { | |
switch (keyCode) { | |
case 65: { // meta-a | |
// Помещаем все теги в помеченые | |
const allTagsIdx = currentTags.map((item, idx) => idx); | |
this.setState({ | |
selectedTagsIdx: allTagsIdx, | |
isFocused: false, | |
}, () => this.field.blur()); | |
e.preventDefault(); | |
break; | |
} | |
case 86: { // meta-v | |
this.clearnSelected(); | |
// Событие всплывет на this.field | |
break; | |
} | |
case 88: // meta-x | |
case 67: { // meta-c | |
if (!selectedTagsIdx.length) return; | |
// Передаем управление копиру | |
this.сopier.select(); | |
break; | |
} | |
case 90: { // meta-z | |
e.preventDefault(); | |
e.stopPropagation(); | |
if (value) { | |
this.setState({ | |
value: value.substring(0, value.length - 1), | |
}); | |
break; | |
} | |
const timetravel = this.state.timetravel.slice(); | |
if (!timetravel.length) return; | |
// Возвращаем предыдущее состояние тегов | |
const prevState = timetravel.shift(); | |
this.setState({ | |
...prevState, | |
timetravel, | |
}); | |
break; | |
} | |
default: | |
e.preventDefault(); | |
return; | |
} | |
return; | |
} | |
// Без мета клавиши | |
switch (keyCode) { | |
case 37: { // left arrow | |
// Двигаем выделение по тегам влево, или выделяем несколько если зажата shift | |
// В случае если мы были в фокусе на поле ввода, сбрасываем фокус и начинаем выделять теги | |
if (isFocused) { | |
if (!currentTags.length) return; | |
const caretPosition = this.field.selectionStart; | |
if (caretPosition > 0) return; | |
const idx = currentTags.length - 1; | |
let newSelectedTagsIdx = [idx]; | |
// Если зажата клавиша shift выделяем последовательно теги | |
if (shiftKey) { | |
newSelectedTagsIdx = selectedTagsIdx.slice(); | |
newSelectedTagsIdx.push(idx); | |
} | |
this.setState({ | |
currentSelectedTagIdx: idx, | |
isFocused: false, | |
selectedTagsIdx: newSelectedTagsIdx, | |
}, () => this.field.blur()); | |
} else { | |
if (currentSelectedTagIdx === null) return; | |
if (currentSelectedTagIdx === 0) return; | |
const idx = currentSelectedTagIdx - 1; | |
let newSelectedTagsIdx = [idx]; | |
// Если зажата клавиша shift выделяем последовательно теги | |
if (shiftKey) { | |
newSelectedTagsIdx = selectedTagsIdx.slice(); | |
newSelectedTagsIdx.push(idx); | |
} | |
this.setState({ currentSelectedTagIdx: idx, selectedTagsIdx: newSelectedTagsIdx }); | |
} | |
break; | |
} | |
case 39: { // right arrow | |
// Двигаем выделение по тегам вправо, или выделяем несколько если зажата shift | |
// Если дошли до края, переводим фокус на поле для ввода тегов | |
if (currentSelectedTagIdx === null) return; | |
if (currentSelectedTagIdx === currentTags.length - 1) { | |
e.preventDefault(); | |
this.clearnSelected(); | |
} else { | |
const idx = currentSelectedTagIdx + 1; | |
let newSelectedTagsIdx = [idx]; | |
// Если зажата клавиша shift выделяем последовательно теги | |
if (shiftKey) { | |
newSelectedTagsIdx = selectedTagsIdx.slice(); | |
newSelectedTagsIdx.push(idx); | |
} | |
this.setState({ currentSelectedTagIdx: idx, selectedTagsIdx: newSelectedTagsIdx }); | |
} | |
break; | |
} | |
case 8: // del | |
case 46: { // backspace | |
// Удялаем выбранные теги, если мы находимся вне поля ввода или в поле ввода удалять нечего | |
if (isFocused) { | |
if (!currentTags.length) return; | |
const field = this.field; | |
const caretPosition = field.selectionStart; | |
if (caretPosition > 0) return; | |
const selectionText = field.value.substring(field.selectionStart, field.selectionEnd); | |
if (selectionText.length || field.value.length) return; // Если стёрли выделенное | |
e.preventDefault(); | |
const newSelectedTagIdx = currentTags.length - 1; | |
this.setState({ | |
currentSelectedTagIdx: newSelectedTagIdx, | |
selectedTagsIdx: [newSelectedTagIdx], | |
isFocused: false, | |
}, () => this.field.blur()); | |
} else { | |
if (!selectedTagsIdx.length) return; | |
if (currentTags.length === 1 || currentTags.length === selectedTagsIdx.length) { | |
e.preventDefault(); | |
const removedTags = currentTags.filter((item, idx) => | |
selectedTagsIdx.indexOf(idx) !== -1); | |
this.setState({ currentTags: [] }, this.fireOnDelete(removedTags, selectedTagsIdx)); | |
this.clearnSelected(); | |
} else { | |
const currentTagsDiff = currentTags.filter((item, idx) => | |
selectedTagsIdx.indexOf(idx) === -1); | |
const removedTags = currentTags.filter((item, idx) => | |
selectedTagsIdx.indexOf(idx) !== -1); | |
const newSelectedTagIdx = currentSelectedTagIdx > 0 | |
? currentSelectedTagIdx - 1 | |
: currentSelectedTagIdx; | |
e.preventDefault(); | |
this.setState({ | |
currentTags: currentTagsDiff, | |
selectedTagsIdx: [newSelectedTagIdx], | |
currentSelectedTagIdx: newSelectedTagIdx, | |
}, this.fireOnDelete(removedTags, selectedTagsIdx)); | |
} | |
} | |
break; | |
} | |
case 27: // esc | |
// Сбрасываем выделение тегов | |
if (currentSelectedTagIdx === null) return; | |
this.clearnSelected(); | |
break; | |
default: | |
} | |
}; | |
/** | |
* Нажатие и удерживание клавиши на области ввода нового тега | |
* @param {KeyboardEvent} e | |
* @return {void} | |
*/ | |
handleTextareaKeyDown = (e) => { | |
const { keyCode } = e; | |
const { value } = this.state; | |
switch (keyCode) { | |
case 9: // tab | |
this.saveTagsAndCloseField(); | |
break; | |
case 13: // enter | |
e.preventDefault(); | |
// Если есть значение, сохраняем его как новый тег, | |
// иначе сохрянем теги и прекращаем редактирование | |
value ? this.addTag(null, value, '') : this.saveTagsAndCloseField(); | |
break; | |
case 27: // esc | |
// Сохрянем теги и прекращаем редактирование | |
this.saveTagsAndCloseField(); | |
break; | |
default: | |
} | |
}; | |
/** | |
* Нажатие клавиши на области ввода нового тега | |
* @param {KeuboardEvent} e | |
* @return {void} | |
*/ | |
handleTextareaKeyPress = (e) => { | |
const { keyCode } = e.nativeEvent; | |
const { delimiter } = this.props; | |
const { value } = this.state; | |
const delimiterCode = delimiter.charCodeAt(); | |
switch (keyCode) { | |
// Если нажата клавиша, определенная как делимитер, сохраняем тег | |
case delimiterCode: | |
e.preventDefault(); | |
if (!value) return; | |
const caretPosition = this.field.selectionStart; | |
if (caretPosition === 0) { | |
this.addTag(null, value, ''); | |
} else { | |
this.addTag(null, value.substring(0, caretPosition), value.substring(caretPosition)); | |
} | |
break; | |
default: | |
} | |
}; | |
/** | |
* Клик по тегу | |
* @param {Number} idx | |
* @return {Function} | |
*/ | |
handleTagClick = (idx) => (e) => { | |
const { isActive } = this.state; | |
if (!isActive) return; | |
const { ctrlKey, metaKey } = e; | |
const specialKey = ctrlKey || metaKey; | |
let selectedTagsIdx = [idx]; | |
// Если зажата метаклавиша*, то добавляем тег в список выделеных | |
if (specialKey) { | |
selectedTagsIdx = this.state.selectedTagsIdx.slice(); | |
selectedTagsIdx.push(idx); | |
} | |
this.setState({ | |
currentSelectedTagIdx: idx, | |
isFocused: false, | |
selectedTagsIdx, | |
}, () => this.field.blur()); | |
}; | |
/** | |
* Событие клика по простыне над полем редактирования | |
* @param {MouseEvent} e | |
* @return {void} | |
*/ | |
handleHolderClick = (e) => { | |
const { type } = this.props; | |
if (type === 'field') { | |
this.setState( | |
{ | |
isActive: true, | |
isFocused: true, | |
}, | |
() => this.field.focus()); | |
} | |
if (this.state.isActive || e.target !== e.currentTarget) return; | |
this.clearnSelected(); | |
}; | |
/** | |
* Разбор всталяемой строки из буффера на теги по делимитеру | |
* @param {KeyboardEvent} e | |
* @return {void} | |
*/ | |
handlePaste = (e) => { | |
const { delimiter } = this.props; | |
e.stopPropagation(); | |
e.preventDefault(); | |
// Получаем данные из буфера | |
const clipboardData = e.clipboardData || window.clipboardData; | |
const bufferData = clipboardData.getData('Text'); | |
// Получаем массив из строки по делимитеру | |
let tagsFromBuffer = bufferData.split(delimiter); | |
tagsFromBuffer = tagsFromBuffer.map(item => item.trim()); // тримим | |
tagsFromBuffer = tagsFromBuffer.filter(item => item.length); // удаляем пустые | |
tagsFromBuffer = tagsFromBuffer.map(item => ({ id: null, title: item })); // формируем массив | |
this.setState((prevState) => ({ | |
currentTags: prevState.currentTags.concat(tagsFromBuffer), | |
}), () => { | |
tagsFromBuffer.forEach((tag) => { | |
this.props.onAdd({ value: tag.title, id: null }); | |
}); | |
}); | |
}; | |
/** | |
* Вырезает из поля выделеные теги | |
* @param {ClipboardEvent} e | |
* @return {void} | |
*/ | |
handleCut = (e) => { | |
const { currentTags, currentSelectedTagIdx, selectedTagsIdx } = this.state; | |
const currentTagsDiff = currentTags.filter((item, idx) => | |
selectedTagsIdx.indexOf(idx) === -1); | |
const removedTags = currentTags.filter((item, idx) => | |
selectedTagsIdx.indexOf(idx) !== -1); | |
const newSelectedTagIdx = currentSelectedTagIdx > 0 | |
? currentSelectedTagIdx - 1 | |
: currentSelectedTagIdx; | |
// Получаем данные из буфера | |
const clipboardData = e.clipboardData || window.clipboardData; | |
clipboardData.setData('text/plain', e.target.value); | |
this.setState({ | |
currentTags: currentTagsDiff, | |
selectedTagsIdx: [newSelectedTagIdx], | |
currentSelectedTagIdx: newSelectedTagIdx, | |
}, this.fireOnDelete(removedTags, selectedTagsIdx)); | |
e.stopPropagation(); | |
e.preventDefault(); | |
} | |
/** | |
* Всплывает при вводе нового тега в textarea | |
*/ | |
handleEnter = ({ target }) => { | |
const { value } = target; | |
this.setState({ value }, () => { | |
this.props.onEnter(value); | |
}); | |
}; | |
/** | |
* Всплывает при выборе тега из коллекции возможных | |
* @param {string} value | |
* @param {string} label | |
* @return {void} | |
*/ | |
handleSuggestionChoose = ({ value, label }) => { | |
this.addTag(value, label, ''); | |
this.props.onSuggestionChoose({ id: value }); | |
}; | |
/** | |
* Событие клика по полю ввода | |
* @return {void} | |
*/ | |
handleTextareaClick = () => { | |
this.setState({ | |
currentSelectedTagIdx: null, | |
isActive: true, | |
isFocused: true, | |
}); | |
} | |
/** | |
* Обработчик фокуса на поле ввода | |
* @return {void} | |
*/ | |
handleTextareaFocus = () => { | |
this.setState({ | |
isActive: true, | |
isFocused: true, | |
}, () => this.field.focus()); | |
} | |
/* | |
* Рендер плейсхолдера | |
*/ | |
renderPlaceholder = () => { | |
const { currentTags, isActive } = this.state; | |
const placeholderClass = classNames( | |
`${styles}__placeholder`, | |
{ | |
[`${styles}__placeholder--displaynone`]: currentTags.length > 0 || isActive, | |
}, | |
); | |
return ( | |
<span | |
className={placeholderClass} | |
onClick={this.handleTextareaFocus} | |
> | |
{this.props.placeholder} | |
</span> | |
); | |
}; | |
/** | |
* Рендер коллекции тегов | |
* @return {React$Element} | |
*/ | |
renderTags() { | |
const { currentTags, currentSelectedTagIdx, selectedTagsIdx } = this.state; | |
return currentTags.map(({ title, tooltip }, idx) => { | |
const isSelected = idx === currentSelectedTagIdx || selectedTagsIdx.includes(idx); | |
const tagClass = classNames( | |
`${styles}__tag`, | |
{ | |
[`${styles}__tag--selected`]: isSelected, | |
}, | |
); | |
const tag = ( | |
<span | |
className={tagClass} | |
key={idx} // eslint-disable-line react/no-array-index-key | |
onClick={this.handleTagClick(idx)} | |
onKeyPress={this.handleKeyPress} | |
> | |
{title} | |
</span> | |
); | |
if (tooltip) { | |
return ( | |
<Tooltip | |
key={idx} // eslint-disable-line react/no-array-index-key | |
{...tooltip} | |
> | |
{tag} | |
</Tooltip> | |
); | |
} | |
return tag; | |
}); | |
} | |
/** | |
* Рендер дропдауна | |
* @return {React$Element|null} | |
*/ | |
renderDropdown() { | |
const { currentTags, value } = this.state; | |
if (!value || value.length < 0) return null; | |
let suggestionTags = this.props.suggestionTags; | |
// Удалим введенные теги из возможных если установлен props | |
if (this.props.hideUsedTags) { | |
suggestionTags = suggestionTags.filter( | |
tag => { | |
const index = currentTags.findIndex( | |
currentTag => currentTag.title === tag.title, | |
); | |
return index === -1; | |
}, | |
); | |
} | |
return ( | |
<div className={`${styles}__dropdown`}> | |
<DropdownFiltered | |
options={[{ | |
items: suggestionTags.map(({ id, title }) => ({ value: id, label: title })), | |
}]} | |
filterValue={value} | |
onItemClick={this.handleSuggestionChoose} | |
/> | |
</div> | |
); | |
} | |
/** | |
* Рендер поля для ввода тега | |
* @return {React$Element} | |
*/ | |
renderField = () => { | |
const { isActive } = this.state; | |
const fieldMatClass = classNames( | |
`${styles}__field-mat`, | |
{ | |
[`${styles}__field-mat--displaynone`]: !isActive, | |
}, | |
); | |
return ( | |
<div className={fieldMatClass}> | |
<plaintext className={`${styles}__field-mat__ghost`}> | |
{this.state.value || '|'} | |
</plaintext> | |
<input | |
ref={el => (this.field = el)} | |
onClick={this.handleTextareaClick} | |
onKeyDown={this.handleTextareaKeyDown} | |
onKeyPress={this.handleTextareaKeyPress} | |
onChange={this.handleEnter} | |
onPaste={this.handlePaste} | |
onCopy={this.handleCopy} | |
onFocus={this.handleTextareaFocus} | |
className={`${styles}__field`} | |
value={this.state.value} | |
/> | |
</div> | |
); | |
}; | |
/** | |
* Рендер поля для копирования текста в буфер | |
* Это поле скрыто от пользователя, в нем записана переменная, состоящая из тегов | |
* соединенных делимитером. Когда происзодит событие связанное с записью в буфер (copy/cut) | |
* это поле фокус и перехватывая событие исполняет его, | |
* тем самым копируя в буфер нужную строку. | |
* Браузер, или даже js, запрещает напрямую в буфер пользователю записать всякое, | |
* в целях безопа-сности. Можно начать копать отсюда: | |
* https://github.com/zenorocha/clipboard.js/issues/258 | |
* @return {React$Element} | |
*/ | |
renderСopier() { | |
const { currentTags, selectedTagsIdx } = this.state; | |
const { delimiter } = this.props; | |
let tagsArray = currentTags.filter((item, idx) => selectedTagsIdx.indexOf(idx) !== -1); | |
tagsArray = tagsArray.map(item => item.title); | |
return ( | |
<input | |
className={`${styles}__copier`} | |
ref={el => (this.сopier = el)} | |
value={tagsArray.join(delimiter)} | |
onCut={this.handleCut} | |
/> | |
); | |
} | |
render() { | |
const { mod, suggestionTags, type } = this.props; | |
const { currentTags, isActive } = this.state; | |
const isField = type === 'field'; | |
const fieldIsVisible = isActive || (mod === 'label' && !currentTags.length); | |
const rootClass = classNames( | |
styles, | |
`${styles}--is-${mod}`, | |
{ | |
[`${styles}--active`]: isActive, | |
[`${styles}--is-visible`]: fieldIsVisible || isField, | |
}, | |
); | |
const holderClass = classNames( | |
`${styles}__holder`, | |
{ | |
[`${styles}__holder--active`]: fieldIsVisible, | |
[`${styles}__holder--is-inline`]: isField, | |
}, | |
); | |
const contentClass = classNames( | |
`${styles}__content`, | |
{ | |
[`${styles}__content--floating-editable-icon`]: this.props.withFloatingEditableIcon, | |
[`${styles}__content--with-icon`]: !isField, | |
}, | |
); | |
return ( | |
<div | |
className={rootClass} | |
ref={el => (this.root = el)} | |
> | |
{this.renderСopier()} | |
<div className={contentClass}> | |
<div className={holderClass} onClick={this.handleHolderClick}> | |
<span className={`${styles}__placeholder-and-pencil`}> | |
{this.renderPlaceholder()} | |
{!isField && | |
<span className={`${styles}__pencil`}> | |
<A onClick={this.handlePencilClick}> | |
<InlineSVG name="edit" colorInherit /> | |
</A> | |
</span> | |
} | |
</span> | |
{this.renderTags()} | |
{this.renderField()} | |
</div> | |
</div> | |
{suggestionTags.length !== 0 && this.renderDropdown()} | |
</div> | |
); | |
} | |
} | |
/** | |
* NOTE: *Метаклавиши - CTRL на win и unix, CMD на macOs (http://learn.javascript.ru/mouse-clicks) | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment