<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ilya Medvedev</title>
    <description>The latest articles on DEV Community by Ilya Medvedev (@imedvedev).</description>
    <link>https://dev.to/imedvedev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F690210%2F1787a962-0e17-483d-a830-4b2e7ffd864b.png</url>
      <title>DEV Community: Ilya Medvedev</title>
      <link>https://dev.to/imedvedev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/imedvedev"/>
    <language>en</language>
    <item>
      <title>Rebuilding a Web Text Editor</title>
      <dc:creator>Ilya Medvedev</dc:creator>
      <pubDate>Tue, 16 Dec 2025 10:59:29 +0000</pubDate>
      <link>https://dev.to/readymag/rebuilding-a-web-text-editor-3g2o</link>
      <guid>https://dev.to/readymag/rebuilding-a-web-text-editor-3g2o</guid>
      <description>&lt;p&gt;&lt;em&gt;It might be easier than you think, if you learn from your mistakes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Back in 2020, I was working on &lt;a href="https://www.smashingmagazine.com/2022/02/develop-text-editor-web/" rel="noopener noreferrer"&gt;building a text editor at Readymag&lt;/a&gt;, an online design tool that helps people create websites without coding. It was a complex but rewarding journey that resulted in a tool that met all our requirements at the time. While we were pleased with many of the architectural choices, software engineering teaches us that there’s always room for improvement.&lt;/p&gt;

&lt;p&gt;As we at Readymag began planning to build our next text editor, revisiting those earlier decisions—both the successful and the limiting ones—became essential. This post is about what we found and how it’s shaping what comes next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottlenecks of the previous solution
&lt;/h2&gt;

&lt;p&gt;Readymag is a design tool for creating websites—we like to say it's like a website builder, but better, because it has no layout restrictions. It's widely used by designers for projects where visual impact matters most: both for the result, and the experience. Think portfolios, landing pages, presentations, and marketing campaigns where pixel-perfect design control is essential.&lt;/p&gt;

&lt;p&gt;I've been working at Readymag for seven years, and since publishing my previous article, I've moved from lead engineer to CTO. This shift gave me a wider perspective, the possibility, or even responsibility, to improve product functionality and approaches.&lt;/p&gt;

&lt;p&gt;Our old text editor was part of the legacy codebase, present almost since Readymag’s launch in 2013. Over time, changes became increasingly difficult to implement, and introducing new features—such as support for &lt;a href="https://blog.readymag.com/in-depth-typography-with-readymag-619795853140/" rel="noopener noreferrer"&gt;variable fonts&lt;/a&gt;—was simply out of reach.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxobsp0giccsqs4hnainc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxobsp0giccsqs4hnainc.png" width="800" height="448"&gt;&lt;/a&gt;&lt;a href="https://readymag.com/readymag/newsletter/39/" rel="noopener noreferrer"&gt;&lt;em&gt;The first text widget in Readymag (2013)&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That was the reason we decided to build a new text widget. You all know that working with text on the web is a thankless job—browsers handle text differently, users expect native behavior in web environments, and seemingly simple features like "delete a word" become complex when you consider international character sets or emoji sequences.&lt;/p&gt;

&lt;p&gt;Therefore, we wanted to choose a low-level framework that would solve most of the issues related to text input. We settled on &lt;a href="https://draftjs.org/" rel="noopener noreferrer"&gt;Draft.js&lt;/a&gt;, which was quite popular at the time (2020). All we had to do was integrate it into our current system, attach it to the data storage, and implement the ability to edit styles with our constructor—done.&lt;/p&gt;

&lt;p&gt;But what was wrong with that idea?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Lack of control&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At Readymag, we develop our own design tool. We have our own data structures, many in-house developments, and we need maximum control over the browser and low-level interactions. At the same time, Draft.js has had a &lt;a href="https://github.com/facebookarchive/draft-js/issues/1105" rel="noopener noreferrer"&gt;bug&lt;/a&gt; since 2017: it incorrectly handles emoji sequences. Some emoji can be longer than 1 character; for example, &lt;strong&gt;👨🏼‍🎨&lt;/strong&gt; — has a length of 7, not 1 as it appears on screen. Draft.js incorrectly processes such sequences, which can lead to errors when inserting or deleting text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flx18mmrfik6bvkv70xx9.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flx18mmrfik6bvkv70xx9.gif" alt="Draft.js Emoji Sequence" width="1166" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s where it gets interesting. We can create an issue, make our own fix and send a pull request (after all, this is the open source world, and we need to contribute), we can make a monkey patch, etc. But all these options slow down processes and complicate control over functionality. And text is the most popular widget in Readymag—this isn’t something we can neglect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Lack of confidence&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Draft.js received its last commit a couple of weeks before my previous article. Today, it’s a public archive. You can't blame anyone here—again, this is an open source world, it's normal. But this means that in addition to vendor lock, we got an uncertain future for our text widget.&lt;/p&gt;

&lt;p&gt;This uncertainty becomes particularly painful when you're building a commercial product. Your roadmap depends on features that may never come, security updates that may never arrive, and browser compatibility fixes that someone else needs to prioritize. Meanwhile, web standards continue evolving—new APIs emerge, browser behaviors change, and user expectations grow. When your foundation stops moving forward, you're essentially betting your product's future on code that's frozen in time. The technical debt accumulates not just from what you build on top, but from the growing gap between what your dependency supports and what the modern web offers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Third-party libraries&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are many pros and cons to both approaches. The common assumption is: &lt;em&gt;“Isn’t it faster and easier to just take a library and plug it in?”&lt;/em&gt;  In reality, that’s not always the case—it depends entirely on your situation. For us, it made far more sense to develop our own engine rather than reuse someone else’s, because this is a critical part of the product.&lt;/p&gt;

&lt;p&gt;If you look at the time spent on integration and the extra code added to the codebase beyond simply installing a third-party package, the numbers can be surprising—months or even years of work, and thousands of additional lines of code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Principle of least astonishment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And last but not least, an important thing—&lt;a href="https://en.wikipedia.org/wiki/Principle_of_least_astonishment" rel="noopener noreferrer"&gt;principle of least astonishment&lt;/a&gt;. We want working with text in Readymag to be as similar as possible to working with text both in browsers and in native applications. The user shouldn’t be surprised by some non-standard solutions—this is very important and fair to the user.&lt;/p&gt;

&lt;p&gt;After several years of development and experience, we began rethinking the product, and the text editor was no exception. What follows is our current approach to building a new text editor from the ground up—work that's actively in progress while I’m writing this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Principles of text editing on the web&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;They say text editing is complex. Yes, but to actually assess the complexity, you need to understand in detail what it is.&lt;/p&gt;

&lt;p&gt;What are the ways to input text? From obvious things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input" rel="noopener noreferrer"&gt;&lt;code&gt;Input&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/textarea" rel="noopener noreferrer"&gt;&lt;code&gt;Textarea&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/contenteditable" rel="noopener noreferrer"&gt;&lt;code&gt;contenteditable&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/designMode" rel="noopener noreferrer"&gt;&lt;code&gt;document.designMode&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can read more in my previous article about &lt;a href="https://www.smashingmagazine.com/2022/02/develop-text-editor-web/" rel="noopener noreferrer"&gt;text editor development&lt;/a&gt;. Here we'll focus more on &lt;code&gt;contenteditable&lt;/code&gt;. This is an attribute that turns almost any HTML element into an editable one.&lt;/p&gt;

&lt;p&gt;If you look closely at what happens when you enable this attribute, you’ll see it delivers almost everything you’d expect from a custom-built text editor: caret handling, text selection, keyboard shortcuts, and basic formatting—all out of the box.&lt;/p&gt;

&lt;p&gt;But when it comes to product development, you should start thinking about states, proprietary data types, and more. At this point, many developers decide to switch course and look for a third-party solution—and in many cases, that’s the right choice. However, for products where text editing is a core feature—design tools, content management systems, collaborative editors—having full control over the text manipulation pipeline is crucial. When users expect pixel-perfect typography, complex formatting, or real-time collaboration, you need the flexibility to implement exactly what your product vision requires, not just what a library allows.&lt;/p&gt;

&lt;p&gt;So, how do you take &lt;code&gt;contenteditable&lt;/code&gt; and connect it to your own data type? For that, you need to intercept input.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How text input works&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here's what the text input lifecycle looks like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/focusin_event" rel="noopener noreferrer"&gt;&lt;code&gt;focusin&lt;/code&gt;&lt;/a&gt; — element receives focus&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/selectionchange_event" rel="noopener noreferrer"&gt;&lt;code&gt;selectionchange&lt;/code&gt;&lt;/a&gt; — cursor is set to position&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event" rel="noopener noreferrer"&gt;&lt;code&gt;keydown&lt;/code&gt;&lt;/a&gt; — physical key press&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/beforeinput_event" rel="noopener noreferrer"&gt;&lt;code&gt;beforeinput&lt;/code&gt;&lt;/a&gt; — content change is prepared&lt;/li&gt;
&lt;li&gt;DOM changes — character is added to content&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event" rel="noopener noreferrer"&gt;&lt;code&gt;input&lt;/code&gt;&lt;/a&gt; — content has already changed&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/keyup_event" rel="noopener noreferrer"&gt;&lt;code&gt;keyup&lt;/code&gt;&lt;/a&gt; — physical key release&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note that the input event isn’t &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Event/cancelable" rel="noopener noreferrer"&gt;&lt;code&gt;cancelable&lt;/code&gt;&lt;/a&gt; since the action has already been performed, but the &lt;code&gt;beforeinput&lt;/code&gt; event is &lt;code&gt;cancelable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you decide which event you should intercept, the easiest way is to start listening to keydown and try to determine what the user will enter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, you can start collecting the entered data in your own state and sending it to the persistent layer—half the job done.&lt;/p&gt;

&lt;p&gt;Or is it? Think about how many keyboard shortcuts you use for text input in daily life. Hopefully a lot—and if not, I highly recommend it; they make working with both text and code much easier. In reality, things are a bit more complicated.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Types of text manipulations&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Let's break down text manipulations into several categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Insert&lt;/li&gt;
&lt;li&gt;Delete&lt;/li&gt;
&lt;li&gt;Format&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each category will be divided into impressively large subsets. For example, you can input text from the keyboard, text can be pasted from the clipboard, text can be replaced by the spelling module, you can delete text character by character, by words, by soft lines, and also backwards and forwards. Imagine how complex it is to create a relationship of all possible text manipulations with all shortcuts. And there are also different operating systems, browsers, different types of devices—development complexity grows exponentially.&lt;/p&gt;

&lt;p&gt;Here you can pause and turn around—after all, you just looked at the &lt;code&gt;contenteditable&lt;/code&gt; block and everything worked there. How does the browser control all this?&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;InputEvent&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Let’s meet &lt;a href="https://w3c.github.io/input-events/#interface-InputEvent" rel="noopener noreferrer"&gt;&lt;code&gt;InputEvent&lt;/code&gt;&lt;/a&gt;, or to be more precise, its &lt;a href="https://w3c.github.io/input-events/#overview" rel="noopener noreferrer"&gt;&lt;code&gt;inputType&lt;/code&gt;&lt;/a&gt; property. This event can be obtained using &lt;code&gt;beforeinput&lt;/code&gt;/&lt;code&gt;input&lt;/code&gt; events. It occurs when the browser has determined what action the user is actually going to perform.&lt;/p&gt;

&lt;p&gt;Here's just a small list of input types a user can make:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;insertText&lt;/code&gt; — insert typed plain text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;insertParagraph&lt;/code&gt; — insert a paragraph break&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;insertFromDrop&lt;/code&gt; — insert content by means of drop&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deleteWordBackward&lt;/code&gt; — delete a word directly before the caret position&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deleteWordForward&lt;/code&gt; — delete a word directly after the caret position&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deleteSoftLineBackward&lt;/code&gt; — delete from the caret to the nearest visual line break before the caret position&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deleteSoftLineForward&lt;/code&gt; — delete from the caret to the nearest visual line break after the caret position&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;formatBold&lt;/code&gt; — initiate bold text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;formatItalic&lt;/code&gt; — initiate italic text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;formatUnderline&lt;/code&gt; — initiate underline text&lt;/li&gt;
&lt;li&gt;&lt;a href="https://w3c.github.io/input-events/#interface-InputEvent-Attributes" rel="noopener noreferrer"&gt;See the full list here&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser does all the work for you. It determines user intentions and categorizes actions—all you have to do is properly handle all these events.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Selection&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;That brings you to another interesting browser object: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Selection" rel="noopener noreferrer"&gt;&lt;code&gt;Selection&lt;/code&gt;&lt;/a&gt;. This class stores knowledge about selected text on screen and is necessary for full control over &lt;code&gt;contenteditable&lt;/code&gt; blocks, since you'll understand what exactly the user has selected and what exactly you need to manipulate.&lt;/p&gt;

&lt;p&gt;Using the following snippet, you can always have a fresh selection value at hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;selectionchange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// We recommend caching the selection value to ease the browser's work&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSelection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Selection comes in two types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Range/collapsed" rel="noopener noreferrer"&gt;&lt;code&gt;collapsed&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;non-collapsed&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many &lt;code&gt;inputType&lt;/code&gt; have different behavior depending on whether text is currently selected or the caret is simply in the middle of some element. For example, if you place the caret at the end of a word and press option + backspace (macOS, &lt;code&gt;deleteWordBackward&lt;/code&gt;), you'll delete the entire word. But if you have several characters selected, only the selection will be deleted. This knowledge is already enough to build an almost-full-fledged editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Building the text editor&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For the sake of experimental simplicity, let's assume that all your data is stored in the DOM. We also assume that there can only be paragraphs inside, and inside paragraphs there can only be span elements—that is, there can be no other elements, such as text nodes, in the content.&lt;/p&gt;

&lt;p&gt;Let's roughly represent this as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EditorState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**
    * This is our root `contenteditable` element
    * It can contain HTMLParagraphElement[],
    * that can in turn contain HTMLSpanElement[]
    */&lt;/span&gt;
 &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This scheme simplifies our life by allowing you to simplify knowledge about text selections. You just need to know which node is selected at the moment and which text segment is selected within this node. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SelectedNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLSpanElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;startOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;endOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you need to get selected nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSelectedNodes&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;SelectedNode&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSelection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rangeCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRangeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// No selection — return node from caret position&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isCollapsed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anchorNode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;focusNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// NOTE: anchorNode could be text node, in that case you should find closest span&lt;/span&gt;
        &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anchorNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;startOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;endOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Handle selection using common ancestor&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commonAncestor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commonAncestorContainer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;commonAncestor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Getting all commonAncestor's children&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commonAncestor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;span&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SelectedNode&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intersectsNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;HTMLSpanElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isFirst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isLast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// All nodes that are not first or last are considered fully selected spans&lt;/span&gt;
    &lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;startOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isFirst&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startOffset&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;endOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isLast&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endOffset&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, you can connect this method to the &lt;code&gt;selectionchange&lt;/code&gt; event and you’ll always have data about selected nodes and offsets of selected text within these nodes at hand.&lt;/p&gt;

&lt;p&gt;Next, you subscribe to the &lt;code&gt;beforeinput&lt;/code&gt; event, get the selection, and depending on the input type, perform one action or another. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;beforeinput&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// This is important to cancel original event, because we should control everything ourselves&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSelectedNodes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;insertText&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onInsertText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;insertLineBreak&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onInsertLineBreak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;insertParagraph&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onInsertParagraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deleteContentBackward&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onDeleteContentBackward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deleteContentForward&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onDeleteContentForward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deleteWordBackward&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onDeleteWordBackward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deleteWordForward&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onDeleteWordForward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Rabbit holes&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The knowledge from previous sections is enough to implement the foundation of a text editor. But as always when developing complex things, you can't do without rabbit holes. So let's dive in head first.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Composition Input&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The DOM &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent" rel="noopener noreferrer"&gt;CompositionEvent&lt;/a&gt; represents events that occur due to the user indirectly entering text, for example through an &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Input_method_editor" rel="noopener noreferrer"&gt;input method editor&lt;/a&gt; (IME).&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Japanese&lt;/strong&gt;: When typing "&lt;a href="https://en.wikipedia.org/wiki/Konnichiwa" rel="noopener noreferrer"&gt;konnichiwa&lt;/a&gt;" (こんにちは), the user types &lt;code&gt;k-o-n-n-i-c-h-i-w-a&lt;/code&gt; on a QWERTY keyboard. The IME shows conversion candidates like こんにちは, 今日は, etc. Only when the user selects the final option (usually by pressing Enter or Space) should the actual text be committed to the editor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chinese (Pinyin)&lt;/strong&gt;: Typing "&lt;a href="https://en.wikipedia.org/wiki/Ni_Hao" rel="noopener noreferrer"&gt;nihao&lt;/a&gt;" shows candidates like 你好, 尼好, 泥好. The user navigates through options before committing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accented characters&lt;/strong&gt;: On macOS, holding e shows options like é, è, ê, ë. The character isn't final until the user makes a selection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During composition input, &lt;code&gt;beforeinput&lt;/code&gt;/&lt;code&gt;input&lt;/code&gt; events receive all keystrokes, but the final result will only be when the user completes the composition input (for example, by pressing enter). For proper text handling, you need to control such input. For this, there are &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event" rel="noopener noreferrer"&gt;&lt;code&gt;compositionstart&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionend_event" rel="noopener noreferrer"&gt;&lt;code&gt;compositionend&lt;/code&gt;&lt;/a&gt; events.&lt;/p&gt;

&lt;p&gt;All you need to do is stop the beforeinput listener during composition input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isComposing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;beforeinput&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isComposing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;compositionstart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;isComposing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;compositionend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;isComposing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// e.data — entered text&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Text Deletion&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;"What could be complicated here?" you might ask. I would suggest diving into the wonderful world of characters, words, and lines.&lt;/p&gt;

&lt;p&gt;At the beginning of the article I mentioned the bug in Draft.js and deleting emoji sequences. Well, in the &lt;a href="https://unicode.org/reports/tr51/#Emoji_Sequences" rel="noopener noreferrer"&gt;Unicode specification&lt;/a&gt; you can learn that many emoji have non-standard length, for example &lt;code&gt;'🏳️‍🌈'.length === 6&lt;/code&gt;, not 1. If you delete one character at a time from the string, you’re likely to break the emoji. But you don't want to allow this.&lt;/p&gt;

&lt;p&gt;There are two ways you can overcome this. The first way is to use &lt;a href="https://en.wikipedia.org/wiki/Grapheme" rel="noopener noreferrer"&gt;grapheme&lt;/a&gt;—the smallest functional unit of a writing system.&lt;/p&gt;

&lt;p&gt;Today you can use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter" rel="noopener noreferrer"&gt;&lt;code&gt;Intl.Segmenter&lt;/code&gt;&lt;/a&gt; to determine graphemes and delete them directly from text. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onDeleteContentBackward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InputEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SelectedNode&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;restNodes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;restNodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TODO: we are not handling multi-span selection in this example&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Use Intl.Segmenter for proper grapheme cluster detection&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segmenter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Segmenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;granularity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grapheme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// Get segments from the string&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;segmenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
  &lt;span class="c1"&gt;// ... return the position of the required segment and delete&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But do you need to reinvent the wheel when everything is already implemented in the browser? If you look at native &lt;code&gt;contenteditable&lt;/code&gt;, everything will work perfectly there.&lt;/p&gt;

&lt;p&gt;To replicate browser behavior by reusing its methods, you can return to &lt;code&gt;Selection&lt;/code&gt; again. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Selection/modify" rel="noopener noreferrer"&gt;&lt;code&gt;Selection.modify&lt;/code&gt;&lt;/a&gt; gives you the ability to move the selection where you need it.&lt;/p&gt;

&lt;p&gt;With it, you can select words, characters, and more—the rest is a matter of technique:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getGranularSelection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;backward&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forward&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;granularity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;word&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;character&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lineboundary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;SelectedNode&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSelection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rangeCount&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isCollapsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Cache selection&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;restoreSelection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cacheSelection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Modify selection using selected granularity&lt;/span&gt;
  &lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;modify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;extend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;granularity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSelectedNodes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Restore cached selection&lt;/span&gt;
  &lt;span class="nf"&gt;restoreSelection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;getGranularSelection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;backward&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;character&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method is also useful when deleting words. For example, a word can consist of several spans. &lt;code&gt;Selection.modify&lt;/code&gt; will handle this too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;getGranularSelection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;backward&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;character&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// or direct &lt;/span&gt;
&lt;span class="c1"&gt;// selection.modify("extend", direction, "word");&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But besides deleting characters and words, you also have the ability to delete an entire visible line. This happens, for example, when pressing command + backspace (macOS, &lt;code&gt;deleteSoftLineBackward&lt;/code&gt;). In this case, you don't just delete all content from the beginning of the paragraph to the current caret position, but you delete text to the nearest boundary—that is, you operate on visible areas.&lt;/p&gt;

&lt;p&gt;You could, of course, use the old trick—get the element width, calculate text width using measurements of the letter &lt;code&gt;M&lt;/code&gt; (the widest English letter) in the selected font—but this construction is very inaccurate, complex, fragile, and will break if you allow different fonts and text sizes within one paragraph.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Selection.modify&lt;/code&gt; comes to the rescue again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;getGranularSelection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;backward&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lineboundary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// or direct &lt;/span&gt;
&lt;span class="c1"&gt;// selection.modify("extend", direction, "lineboundary");&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can easily delegate this task to the browser, and additionally, you reduce the amount of code that needs to be maintained and whose performance needs to be optimized.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Highlighting&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Now take this case: you’ve entered text, and there's a settings panel with various inputs—font size, letter spacing, line height... What happens if you focus on such an input? You lose text selection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F44xwkhrvq6prlet4zw89.png" class="article-body-image-wrapper"&gt;&lt;img alt="Several selections on the screen" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F44xwkhrvq6prlet4zw89.png" width="800" height="452"&gt;&lt;/a&gt;Several selections on the screen&lt;/p&gt;

&lt;p&gt;There can be several selections on the screen. This is critical because, first, our code doesn't know which text needs to be changed now, and second, users lose visibility.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://www.smashingmagazine.com/2022/02/develop-text-editor-web/#text-selection-and-focus" rel="noopener noreferrer"&gt;previous article&lt;/a&gt;, I mentioned two ways to fix this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cache the caret and restore its position after each action in the input&lt;/li&gt;
&lt;li&gt;wrap the text editor in an &lt;code&gt;iframe&lt;/code&gt; (which is what we ultimately did at Readymag)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This time I'll tell you about the CSS &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API" rel="noopener noreferrer"&gt;Custom Highlight API&lt;/a&gt;. This is an API that allows you to programmatically make as many selections on screen as you want. All you have to do is connect your knowledge about selected text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;highlight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Highlight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSelectedNodes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;selectedNodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startOffset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endOffset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Register the highlight&lt;/span&gt;
&lt;span class="nx"&gt;CSS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;highlights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-selection&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;highlight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Voilà! Now users always see which part of the text is being edited.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The best solutions, not the obvious ones&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Product improvements—especially for web apps—are an ongoing process. As the web platform evolves and user expectations grow, you’ll revisit our decisions again and again. But right now, building a web text editor doesn’t have to be as daunting as it first appears. By combining the browser’s built-in capabilities with modern APIs, you can create powerful, native-feeling editors without the complexity or constraints of third-party libraries.&lt;/p&gt;

&lt;p&gt;Sometimes it’s worth taking the scenic route to build something that truly fits your needs.&lt;/p&gt;

</description>
      <category>readymag</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
