рд╕рднреА рдХреЛ рдирдорд╕реНрдХрд╛рд░!тАи
рдореЗрд░рд╛ рдирд╛рдо рдЕрд▓реЗрдХреНрдЬреЗрдВрдбрд░ рдмрд╛рд▓реНрдЯрд┐рд╡рд┐рдЪ рд╣реИ, рдореИрдВ Gem4me рдкрд░рд┐рдпреЛрдЬрдирд╛ рдХреЗ рд╡реЗрдм-рдЯреАрдо рдХреЗ рдиреЗрддреГрддреНрд╡ рдХреА рд╕реНрдерд┐рддрд┐ рдкрд░ рдХрд╛рдо рдХрд░рддрд╛ рд╣реВрдВред рдкрд░рд┐рдпреЛрдЬрдирд╛ рд╣рд░ рдХрд┐рд╕реА рдФрд░ рд╕рднреА рдХреЗ рд▓рд┐рдП рдПрдХ рдЕрднрд┐рдирд╡ рд╕рдВрджреЗрд╢рд╡рд╛рд╣рдХ рд╣реИ (рдЕрдм рддрдХ рдореЗрд░реА рдХрд▓реНрдкрдирд╛рдУрдВ рдореЗрдВ, рд▓реЗрдХрд┐рди рд╣рдо рдЗрд╕рдХреЗ рд▓рд┐рдП рдкреНрд░рдпрд╛рд╕ рдХрд░рддреЗ рд╣реИрдВ; ;-))
рд╡реЗрдм рд╕рдВрд╕реНрдХрд░рдг рд╕реНрдЯреИрдХ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рд╕рдВрдХреНрд╖реЗрдк рдореЗрдВ: ReactJS (рдЬреЛ рдЗрд╕ рдкрд░ рд╕рдВрджреЗрд╣ рдХрд░реЗрдЧрд╛) + mobX (рд╡реНрдпрдХреНрддрд┐рдЧрдд рд░реВрдк рд╕реЗ, рдореИрдВ рдмрд┐рд▓реНрдХреБрд▓ рдЙрддреНрд╕рд╛рд╣реА рдирд╣реАрдВ рд╣реВрдВ, рд▓реЗрдХрд┐рди рд╣рдо рдХрд╣реАрдВ рднреА рдорд╛рдЗрдЧреНрд░реЗрдЯ рдХрд░рдиреЗ рдХреА рдпреЛрдЬрдирд╛ рдирд╣реАрдВ рдмрдирд╛рддреЗ рд╣реИрдВ; рдпрджрд┐ рдЖрдк рд╡рд┐рд╡рд░рдг рдореЗрдВ рд░реБрдЪрд┐ рд░рдЦрддреЗ рд╣реИрдВ, рддреЛ рдЖрдк рд╡рд╛рд╕реНрддрд╡ рдореЗрдВ рдХреНрдпрд╛ рд╕реЛрдЪрддреЗ рд╣реИрдВ, рдЖрдк рдЯрд┐рдкреНрдкрдгреА рдирд╣реАрдВ рд▓рд┐рдЦрддреЗ рд╣реИрдВ - рд╢рд╛рдпрдж рдореИрдВ рдЗрд╕рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдПрдХ рдЕрд▓рдЧ рд▓реЗрдЦ рд▓рд┐рдЦреВрдВрдЧрд╛) + рд╕реНрдЯреЛрд░реАрдмреБрдХ + wdio (рд╕реНрдХреНрд░реАрдирд╢реЙрдЯ рдкрд░реАрдХреНрд╖рдг)ред
рдореИрд╕реЗрдВрдЬрд░ рдореБрдЦреНрдп рд░реВрдк рд╕реЗ рдЫреЛрдЯреЗ рд╕рдВрджреЗрд╢реЛрдВ (рдФрд░ рдлрд┐рд░ рдЗрди рд╕рднреА рдХреЙрд▓, рд╕реНрдЯрд┐рдХрд░ рдФрд░ рдЕрдиреНрдп рд╕рдореНрдореЗрд▓рдиреЛрдВ) рдХреЗ рдЖрджрд╛рди-рдкреНрд░рджрд╛рди рдХреЗ рд▓рд┐рдП рдПрдХ рдХрд╛рд░реНрдпрдХреНрд░рдо рд╣реИред рд╕рдВрджреЗрд╢ рдмреБрдирд┐рдпрд╛рджреА рдХрд╛рд░реНрдпрдХреНрд╖рдорддрд╛ рд╣реИрдВ рдЬрд┐рдиреНрд╣реЗрдВ рдЕрдЪреНрдЫреА рддрд░рд╣ рд╕реЗ рдХрд╛рдо рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдПред рдирд╣реАрдВ, рдРрд╕рд╛ рдирд╣реАрдВ рд╣реИ: рдЗрд╕реЗ рдкреВрд░реА рддрд░рд╣ рд╕реЗ рдХрд╛рдо рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдПред рдФрд░ рдХреЗрд╡рд▓ рдПрдХ рдЬрд┐рд╕рдиреЗ рдХрдо рд╕реЗ рдХрдо рдПрдХ рдмрд╛рд░ рдЗрд╕ рдХрд╛рд░реНрдпрдХреНрд╖рдорддрд╛ рдХреЗ рд╡рд┐рдХрд╛рд╕ рдХрд╛ рд╕рд╛рдордирд╛ рдХрд┐рдпрд╛, рд╡рд╣ рд╕рднреА рджрд░реНрдж рдЬрд╛рдирддрд╛ рд╣реИ рдЬрд┐рд╕реЗ рджреВрд░ рдХрд░рдирд╛ рд╣реИ рддрд╛рдХрд┐ рд╕рдм рдХреБрдЫ рд╕реБрдВрджрд░ рджрд┐рдЦреЗред рд╣рдо рд╕рднреА рдЗрд╕ рдЯреАрдо рдХреЗ рд╕рд╛рде рдЧрдПред рдФрд░ рд╣рдордиреЗ рд╕рд╛рдЭрд╛ рдХрд░рдиреЗ рдХрд╛ рдлреИрд╕рд▓рд╛ рдХрд┐рдпрд╛ред

рдЖрд╡рд╢реНрдпрдХрддрд╛рдПрдБ
рдЗрдирдкреБрдЯ рдлрд╝реАрд▓реНрдб рдореЗрдВ рдХреНрдпрд╛ рд╕реБрд╡рд┐рдзрд╛рдПрдБ рдкреНрд░рджрд╛рди рдХрд░рдиреА рдЪрд╛рд╣рд┐рдП?
- рдпрд╣ рдЯреЗрдХреНрд╕реНрдЯ рдХреЛ рдкреНрд░рд┐рдВрдЯ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЯреНрд░рд╛рдЗрдЯ рд╣реИ, рдЬрд┐рд╕рдореЗрдВ рдХрдИ рд▓рд╛рдЗрдиреЛрдВ рдореЗрдВ рд▓рд┐рдЦрдиреЗ рдХреА рдХреНрд╖рдорддрд╛ рд╣реИ (рд╢рд┐рдлреНрдЯ + рдПрдВрдЯрд░ рджрдмрд╛рдХрд░)ред рд╕рдВрджреЗрд╢ рдХреЛ рдЬрд▓реНрджреА рд╕реЗ рдкреНрд░рд╛рд░реВрдкрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЙрдиреНрдирдд рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛рдУрдВ рдХреЛ рдорд╛рд░реНрдХрдбрд╛рдЙрди рдЕрдХреНрд╖рд░реЛрдВ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рднреА рдЙрдкрдпреЛрдЧ рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИред

- рдкрд╛рда рдмрдлрд░ рд╕реЗ рдЪрд┐рдкрдХрд╛рдПрдБред рдЕрдХреНрд╕рд░ рдкрд╛рда рдХреА рдкреНрд░рддрд┐рд▓рд┐рдкрд┐ рдмрдирд╛рддреЗ рд╕рдордп, рдЗрд╕ рдкрд╛рда рдХреА рдкреГрд╖реНрдарднреВрдорд┐ рдХреЛ рднреА рдХреЙрдкреА рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ (рдиреАрдЪреЗ рдЙрджрд╛рд╣рд░рдг рджреЗрдЦреЗрдВ) рдФрд░ рд╕реНрд╡рд░реВрдкрдгред рд▓реЗрдХрд┐рди рд╕реНрд╡рдЪреНрдЫ, рд╕рд╛рдл-рд╕реБрдерд░рд╛ рдкрд╛рда рдФрд░ рдЕрдзрд┐рдХ рдХреБрдЫ рднреА рдЗрдирдкреБрдЯ рдХреНрд╖реЗрддреНрд░ рдореЗрдВ рдирд╣реАрдВ рдЬрд╛рдирд╛ рдЪрд╛рд╣рд┐рдПред

- рдХреНрд▓рд┐рдкрдмреЛрд░реНрдб рд╕реЗ рдЪрд┐рддреНрд░ рдбрд╛рд▓реЗрдВред рдпрд╣ рдореБрдЭреЗ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рдХреЛрдИ рднреА рдЗрд╕ рд╕реБрд╡рд┐рдзрд╛ рдХреЗ рдмрд┐рдирд╛ рдирд╣реАрдВ рд░рд╣ рд╕рдХрддрд╛ рд╣реИред
- рд╕реНрдорд╛рдЗрд▓реА =) рд▓рдВрдмреЗ рд╕рдордп рд╕реЗ рд╕рдВрдЪрд╛рд░ рдХреА рд╕рдВрд╕реНрдХреГрддрд┐ рдХрд╛ рдПрдХ рд╣рд┐рд╕реНрд╕рд╛ рдмрди рдЧрдпрд╛ рд╣реИред рдЗрд╕рд▓рд┐рдП, рдЖрдк рдореБрд╕реНрдХреБрд░рд╛рд╣рдЯ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдирд╣реАрдВ рднреВрд▓ рд╕рдХрддреЗред рдкрд╛рда рдХреА рд╢реБрд░реБрдЖрдд рдореЗрдВ, рдмреАрдЪ рдореЗрдВ, рдПрдХ рдкрдВрдХреНрддрд┐ рдореЗрдВ рджрд╕ рдЯреБрдХрдбрд╝реЗ рдпрд╛ рдПрдХ рд╕рдордп рдореЗрдВ рдЖрдВрддрд░рд┐рдХ рдкреБрд╕реНрддрдХрд╛рд▓рдп рд╕реЗ рдбрд╛рд▓реЗ рдЧрдП рддреАрди рд╢рдмреНрджреЛрдВ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рдпрд╛ рдХрд┐рд╕реА рдЕрдиреНрдп рд╕рдВрд╕рд╛рдзрди рд╕реЗ рдХреЙрдкреА рдХрд┐рдП рдЧрдП - рдХрд┐рд╕реА рднреА рд░реВрдкрд╛рдВрддрд░ рдФрд░ рднрд┐рдиреНрдирддрд╛ рдореЗрдВ, рд╣рдореЗрд╢рд╛ рдкреНрд▓рд╕ рдХреЗ рд╕рд╛рде рдкрд╛рдВрдЪ рдореЗрдВ рдЦреАрдВрдЪрд╛ рдЬрд╛рдирд╛ рдЪрд╛рд╣рд┐рдПред
- , , ( ). тАФ , , . , , . Esc ( ).

" " ("mention") , . :

- "@" ("@T" => "T");
- тАЬ тАЭ , , "@Tes", ("@Te|s"), . ;
- , " @ ?", "@" ("| @ ?") "@" (" @ | ?"), тАЬ тАЭ ;
- , (" @ ?|") тАФ , (" @| ?") тАФ , "@" ;
- , ("@| "), " " . тАФ , . , , (тАЬ@ | тАЭ).
, .
, , , тАФ :
- , , . , , .
- . , - тАФ ( ), тАФ - . , . , .
, , тАФ , .., , , .
!
HTML
, HTML . , , , , , , input, textarea , .
input , , .
textarea тАФ " ", . , (. ). input, shift + Enter . 5 , . textarea , , .



, textarea, , , div. , , , ! contenteditable.
The contenteditable global attribute is an enumerated attribute indicating if the element should be editable by the user. If so, the browser modifies its widget to allow editing. (MDN)
, , .
:
<div
className={styles.input}
placeholder="Type a message"
contentEditable
/>
, тАФ , . input onChange. div onInput, , . addEventListener , . :
class Input extends Component {
setRef = (ref) => {
this.ref = ref;
};
saveInputValue = () => {
const text = this.ref.innerText;
this.props.onChange(text);
};
componentDidMount() {
this.ref.addEventListener('input', this.saveInputValue);
}
componentWillUnmount() {
this.ref.removeEventListener('input', this.saveInputValue);
}
render() {
return (
<div
className={styles.input}
ref={this.setRef}
placeholder="Type a message"
contentEditable
/>
);
}
}
input ref innerText . div uncontrolled, .. , .
тАФ . , Enter. Enter . . input keydown. :
onKeyDown = (event) => {
if (event.keyCode === ENTER_KEY_CODE && event.shiftKey === false) {
event.stopPropagation();
event.preventDefault();
this.props.submit();
}
};
componentDidMount() {
this.ref.addEventListener('keydown', this.onKeyDown);
...
}
event.preventDefault() Enter, event.stopPropogation() . , Shift+Enter .
-
тАФ - . - . paste.
handlePaste = (event) => {
event.preventDefault();
const text = event.clipboardData.getData('text/plain');
document.execCommand('insertText', false, text);
};
componentDidMount() {
...
this.ref.addEventListener('paste', this.handlePaste);
}
event.preventDefault() , ( event.clipboardData === DataTransfer). , тАФ . document.execCommand('insertText', false, text). , .. , .
. . fileList, тАФ , , :
handlePaste = (event) => {
event.preventDefault();
if (event.clipboardData.files.length > 0) {
// ...
}
const text = event.clipboardData.getData('text/plain');
document.execCommand('insertText', false, text);
};
, .. , . .
Emoji
emoji . unicode emoji emoji . Emoji тАФ , тАФ execComand(тАШinsertTextтАЩ). , , . ! , Emoji, , .тАитАи
, , this.ref.focus() , :
insertEmoji = (emoji) => {
if (this.ref) {
this.ref.focus();
}
document.execCommand('insertText', false, emoji);
}
, : , Emoji, тАФ . , - . , Emoji , , . .
, , .
API тАФ Selection (MDN).
A Selection object represents the range of text selected by the user or the current position of the caret. To obtain a Selection object for examination or manipulation, call window.getSelection().
window.getSelection() , , , . , , тАФ . API . , . :

gem4me, "m" "e". selection selection.anchorNode ( ) selection.anchorOffset (5). , selection.anchorNode , , , . , , тАФ . :
updateCaretPosition = () => {
const { node, cursorPosition } = this.state;
const selection = window.getSelection();
const newNode = selection.anchorNode;
const newCursorPosition = selection.anchorOffset;
if ( node === newNode && cursorPosition === newCursorPosition) {
return;
}
this.setState({ node, cursorPosition });
}
, . , :
тАФ onInput, , . paste , .
onInput. , тАФ . тАФ , keyup ( keydown тАФ , , ). , 2 ( , input keyup). state, . , .. , =).
. , ? click .
. -!
Emoji
, , Emoji. API Range, Selection.
The Range interface represents a fragment of a document that can contain nodes and parts of text nodes
, . :
const { node, cursorPosition } = this.state;
const range = document.createRange();
range.setStart(node, cursorPosition);
range.setEnd(node, cursorPosition);
тАи , . тАФ . :
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
selection, , . this.ref.focus() ! , , :
document.execCommand('insertText', false, emoji);
:
customFocus = () => {
const { node, cursorPosition } = this.state;
const range = document.createRange();
range.setStart(node, cursorPosition);
range.setEnd(node, cursorPosition);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
insertEmoji = (emoji) => {
this.customFocus();
document.execCommand('insertText', false, emoji);
};
, , . :
, , ( ). тАФ , , . , , . Esc ( ).
, . keydown. , . :
if (
event.keyCode === UP_ARROW_KEY_CODE &&
this.props.isTextEmpty &&
this.props.mode === INPUT_CONTEXT_TYPES.STANDARD
) {
тАж
}
, , . :
if (
event.keyCode === UP_ARROW_KEY_CODE &&
this.props.isTextEmpty &&
this.props.mode === INPUT_CONTEXT_TYPES.STANDARD
) {
event.preventDefault();
event.stopPropagation();
this.props.setMode('edit');
document.execCommand('insertText', false, this.props.lastMessage);
}
document.execCommand('insertText') , .
Esc . keydown . this.ref.textContent = "".
, , . тАФ , .. , . , "@". updateCaretPosition:
updateAndProcessCaretPosition = () => {
const { node, cursorPosition } = this.state;
const selection = window.getSelection();
const newNode = selection.anchorNode;
const newCursorPosition = selection.anchorOffset;
if (node === newNode && cursorPosition === newCursorPosition) {
return;
}
if (this.props.isAvailableMention) {
this.props.parseMention(node.textContent, cursorPosition);
};
this.setState({ node, cursorPosition });
}
isAvailableMention , . , true. :
parseMention = (text, cursorPosition) => {
if (text && cursorPosition && text.includes('@')) {
const lastWord = text
.substring(0, cursorPosition)
.split(' ')
.pop();
if (lastWord[0] === '@') {
this.setFilter(lastWord.substring(1, cursorPosition));
return;
}
}
this.clearFilter();
};
, "@" . тАФ , , "@", . , .
insertMention = (insertRestPieceOfMention) => {
this.customFocus();
document.execCommand('insertText', false, insertRestPieceOfMention + ' ');
};
, document.execCommand('insertText') : , , .
, , , , , 20+ , , . , - . тАФ , , (, , , , , - ), , , , . тАФ , .
, Gem4me