I was recently forced into the world of custom text editors again, after a fairly long hiatus. Text (especially rich text) has always been one of those areas that tools fall on either end of the 'too simple' to 'too complex' spectrum. Where simple tools don't offer what you need and complex ones offer too much. There is also the phenomena of 'it works but' where a requirement isn't ticked and you question whether you can convince stakeholders to drop it. My recent project was a simple input, that had a non-trivial view, but it had to work flawlessly. It looks like this:
It allows the user to type free text and displays an emoji above each character as a secret 'code'. What I knew early on is it had to work like a real text input, it needed to support copy/paste and select, and line breaks and all that good stuff. The early ideas looked at content editable tags, and capturing keyboard events and doing all sorts of mad complicated stuff. In the end what I looked into was 'boosting' an input, a textarea
in this case. By 'boosting' I mean taking the natural implementation and hooking things off of it to give it additional functionality. In this case, there is a textarea
that isn't visible and the component controls focus
as well as onChange
and other events. Starting off with the usual:
const textareaRef = createRef();
const [value, setValue] = useState('');
It then boils down to two functions that take care of keeping track of the value, and keeping track of the selection and focus.
const onTextareaChange = (evt) => {
onChange(evt.target.value);
setSelection();
};
const setSelection = () => {
setStart(textareaRef.current.selectionStart);
setEnd(textareaRef.current.selectionEnd);
setSelected(textareaRef.current === document.activeElement);
};
These are meant to be as general purpose as possible. The onTextareaChange
is what you have all probably written dozens of times, but the setSelection
is reasonably elegant. It sets three state values start
, end
and selected
. Where start
and end
are the range of characters currently selected (also used to work out the position of my fake caret), and selected
being a boolean showing focus. From here the rest of the component can do whatever it wants to show the 'output'. In my case I actually used click events on each 'letter' that it shows to change the selection:
const onSelect = (idx = 0) => {
if (onChange && !disabled) {
textareaRef.current.focus();
textareaRef.current.selectionStart = idx;
textareaRef.current.selectionEnd = idx;
setSelection();
}
};
This will be updated to support drag selections eventually, but it simply expects an index as in the value
index clicked. This will then ensure the textarea
has focus and sets the selection back to trigger any other logic.
Generally this is pretty clean, and even with some additional animation is quite snappy in my particular use case.
I also used the same technique for an input view, where the user has to decode the secret message. Which used individual input
tags and some focus change magic:
I'll clean up the code and share if people are interested, and you can go check out the finished app and write your own messages at mojimess, an app built for 6 year olds 😎
Top comments (0)