import React, { Component, useRef, useState, useEffect, useImperativeHandle, forwardRef, } from "react"; import { noop } from "../utils"; import PropTypes from "prop-types"; import classNames from "classnames"; import { prefix } from "../settings"; import ContentEditable from "../ContentEditable"; import SendButton from "../Buttons/SendButton"; import AttachmentButton from "../Buttons/AttachmentButton"; import PerfectScrollbar from "../Scroll"; // Because container depends on fancyScroll // it must be wrapped in additional container function editorContainer() { class Container extends Component { render() { const { props: { fancyScroll, children, forwardedRef, ...rest }, } = this; return ( <> {fancyScroll === true && ( <PerfectScrollbar ref={(elRef) => (forwardedRef.current = elRef)} {...rest} options={{ suppressScrollX: true }} > {children} </PerfectScrollbar> )} {fancyScroll === false && ( <div ref={forwardedRef} {...rest}> {children} </div> )} </> ); } } return React.forwardRef((props, ref) => { return <Container forwardedRef={ref} {...props} />; }); } const EditorContainer = editorContainer(); const useControllableState = (value, initialValue) => { const initial = typeof value !== "undefined" ? value : initialValue; const [stateValue, setStateValue] = useState(initial); const effectiveValue = typeof value !== "undefined" ? value : stateValue; return [ effectiveValue, (newValue) => { setStateValue(newValue); }, ]; }; function MessageInputInner( { value, onSend, onChange, autoFocus, placeholder, fancyScroll, className, activateAfterChange, disabled, sendDisabled, sendOnReturnDisabled, attachDisabled, sendButton, attachButton, onAttachClick, ...rest }, ref ) { const scrollRef = useRef(); const msgRef = useRef(); const [stateValue, setStateValue] = useControllableState(value, ""); const [stateSendDisabled, setStateSendDisabled] = useControllableState( sendDisabled, true ); // Public API const focus = () => { if (typeof msgRef.current !== "undefined") { msgRef.current.focus(); } }; // Return object with public Api useImperativeHandle(ref, () => ({ focus, })); // Set focus useEffect(() => { if (autoFocus === true) { focus(); } }, []); // Update scroll useEffect(() => { if (typeof scrollRef.current.updateScroll === "function") { scrollRef.current.updateScroll(); } }); const getContent = () => { // Direct reference to contenteditable div const contentEditableRef = msgRef.current.msgRef.current; return [ contentEditableRef.textContent, contentEditableRef.innerText, contentEditableRef.cloneNode(true).childNodes, ]; }; const send = () => { if (stateValue.length > 0) { // Clear input only when it's uncontrolled mode if (value === undefined) { setStateValue(""); } // Disable send button only when it's uncontrolled mode if (typeof sendDisabled === "undefined") { setStateSendDisabled(true); } const content = getContent(); onSend(stateValue, content[0], content[1], content[2]); } }; const handleKeyPress = (evt) => { if ( evt.key === "Enter" && evt.shiftKey === false && sendOnReturnDisabled === false ) { evt.preventDefault(); send(); } }; const handleChange = (innerHTML, textContent, innerText) => { setStateValue(innerHTML); if (typeof sendDisabled === "undefined") { setStateSendDisabled(textContent.length === 0); } if (typeof scrollRef.current.updateScroll === "function") { scrollRef.current.updateScroll(); } const content = getContent(); onChange(innerHTML, textContent, innerText, content[2]); }; const cName = `${prefix}-message-input`, ph = typeof placeholder === "string" ? placeholder : ""; return ( <div {...rest} className={classNames( cName, { [`${cName}--disabled`]: disabled }, className )} > {attachButton === true && ( <div className={`${cName}__tools`}> <AttachmentButton onClick={onAttachClick} disabled={disabled === true || attachDisabled === true} /> </div> )} <div className={`${cName}__content-editor-wrapper`}> <EditorContainer fancyScroll={fancyScroll} ref={scrollRef} className={`${cName}__content-editor-container`} > <ContentEditable ref={msgRef} className={`${cName}__content-editor`} disabled={disabled} placeholder={ph} onKeyPress={handleKeyPress} onChange={handleChange} activateAfterChange={activateAfterChange} value={stateValue} /> </EditorContainer> </div> {sendButton === true && ( <div className={`${cName}__tools`}> <SendButton onClick={send} disabled={disabled === true || stateSendDisabled === true} /> </div> )} </div> ); } const MessageInput = forwardRef(MessageInputInner); MessageInput.displayName = "MessageInput"; MessageInput.propTypes = { /** Value. */ value: PropTypes.string, /** Placeholder. */ placeholder: PropTypes.string, /** A input can show it is currently unable to be interacted with. */ disabled: PropTypes.bool, /** Prevent that the input message is sent on a return press */ sendOnReturnDisabled: PropTypes.bool, /** Send button can be disabled.<br> * It's state is tracked by component, but it can be forced */ sendDisabled: PropTypes.bool, /** * Fancy scroll * This property is set in constructor, and is not changing when component update. */ fancyScroll: PropTypes.bool, /** * Sets focus element and caret at the end of input<br> * when value is changed programmatically (e.g) from button click and element is not active */ activateAfterChange: PropTypes.bool, /** Set focus after mount. */ autoFocus: PropTypes.bool, /** * onChange handler<br> * @param {String} innerHtml * @param {String} textContent * @param {String} innerText * @param {NodeList} nodes */ onChange: PropTypes.func, /** * onSend handler<br> * @param {String} innerHtml * @param {String} textContent * @param {String} innerText * @param {NodeList} nodes */ onSend: PropTypes.func, /** Additional classes. */ className: PropTypes.string, /** Show send button */ sendButton: PropTypes.bool, /** Show add attachment button */ attachButton: PropTypes.bool, /** Disable add attachment button */ attachDisabled: PropTypes.bool, /** * onAttachClick handler */ onAttachClick: PropTypes.func, }; MessageInputInner.propTypes = MessageInput.propTypes; MessageInput.defaultProps = { value: undefined, placeholder: "", disabled: false, sendOnReturnDisabled: false, fancyScroll: true, activateAfterChange: false, autoFocus: false, sendButton: true, attachButton: true, attachDisabled: false, onAttachClick: noop, onChange: noop, onSend: noop, }; MessageInputInner.defaultProps = MessageInput.defaultProps; export { MessageInput }; export default MessageInput;