<?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>Forem: Readymag</title>
    <description>The latest articles on Forem by Readymag (@readymag).</description>
    <link>https://forem.com/readymag</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%2Forganization%2Fprofile_image%2F4700%2Fdc2982b1-d329-43b2-9716-0a60e9da5816.png</url>
      <title>Forem: Readymag</title>
      <link>https://forem.com/readymag</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/readymag"/>
    <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://forem.com/readymag/rebuilding-a-web-text-editor-3g2o</link>
      <guid>https://forem.com/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>
    <item>
      <title>No Code tools are either hype or niche — you should opt for niche</title>
      <dc:creator>Anton Vasin</dc:creator>
      <pubDate>Thu, 16 Sep 2021 10:35:18 +0000</pubDate>
      <link>https://forem.com/readymag/no-code-tools-are-either-hype-or-niche-you-should-opt-for-niche-4nh2</link>
      <guid>https://forem.com/readymag/no-code-tools-are-either-hype-or-niche-you-should-opt-for-niche-4nh2</guid>
      <description>&lt;p&gt;No-code is a broad term. It describes a vast set of products that help end-users assemble web pages and applications without hiring developers.&lt;/p&gt;

&lt;p&gt;In recent years, it has also become an ideology of sorts (praised, for example, in this &lt;a href="https://www.forbes.com/sites/johneverhard/2019/01/15/what-really-is-low-codeno-code-development/?sh=20c508132a8e"&gt;Forbes&lt;/a&gt; column): a promise to get rid of all complications that are intertwined with IT development — its proverbial high costs, unpredictability, and difficulty to scale the teams fast enough.&lt;/p&gt;

&lt;p&gt;However, I’d argue the promise is often exaggerated, as the proposed approaches are oversold and/or not particularly new. Still, niche solutions from the no-code toolbox might get your tasks in certain pipeline parts done surprisingly well.&lt;/p&gt;

&lt;p&gt;So let’s pick apart the ideology and get into what startups and businesses should consider when thinking about no-code solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  No-code is not particularly new
&lt;/h2&gt;

&lt;p&gt;Speaking of no-code, we usually think of it as a recent development, a step made in the late 2010s to emancipate the world from expensive engineers. Be it Notion, Mailchimp, Voiceflow, or Bubble, companies associated with no-code approaches are usually recently found startups. But is the approach actually that recent?&lt;/p&gt;

&lt;p&gt;In fact, no-code-like tools were there from the very beginning of the computer era. Take Microsoft Excel: it’s basically a way to embark on visual point-and-click methods to create a simple database instead of using SQL. Or any graphical operating system like Windows, Mac OS, or Ubuntu: they give users a command line functionality combined with visual means, without the need to learn code-like commands.&lt;/p&gt;

&lt;p&gt;This point also perfectly illustrates the limitations of no-code. It is no coincidence that most operating systems still have a command line-based core and give their power users access to it: some things are just intrinsically difficult to visualize.&lt;/p&gt;

&lt;p&gt;Yes, a lot of people don’t touch the Mac OS X Terminal and never will, but in most cases, somebody terminal-savvy needs to be around to perform any actions above a certain level of complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  No-code limits patterns of thought
&lt;/h2&gt;

&lt;p&gt;The visualization and simplification, these pillars of no-code, come at a price: no-code tools usually nudge a client to a limited number of patterns — in fact, that’s exactly what allows them to get rid of the code.&lt;/p&gt;

&lt;p&gt;Say, only a certain number of product management techniques go hand in hand with no-code task management tools such as, say, Trello. As a result, the idea itself might become stale. &lt;/p&gt;

&lt;p&gt;The problem with patterns is that they deny you the possibility of learning. A salesperson can’t become an expert in business only by using landing page presets. The code usually gives you almost infinite possibilities of configuring the system (open-source culture and the competition of approaches, programming languages and libraries usually guarantee it in any given field).&lt;/p&gt;

&lt;p&gt;It might not be that important for the first project, but crucial for the growth and future of any professional. There are some domains such as computations or high-load systems performance where you can not simply ‘no-code’ your way out of complexity.&lt;/p&gt;

&lt;p&gt;That’s why I and my team at &lt;a href="https://readymag.com?utm_source=dev.to&amp;amp;utm_medium=pr&amp;amp;utm_campaign=nocode_tools"&gt;Readymag&lt;/a&gt;, a browser-based design tool that helps create websites, portfolios and all kinds of online publications without coding try to avoid staleness at all costs in our solutions, never limiting our users to presets, always giving them access to a clean canvas. We also give our users tools for coding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Good tools have precise scope
&lt;/h2&gt;

&lt;p&gt;However, I firmly believe that no-code approaches are great when it comes to a narrow-scope task. Take Zapier, a tool for API integration, that we actually use in Readymag; or Airtable, a tool to automate the creation of CMS.&lt;/p&gt;

&lt;p&gt;The idea here is not to waste your time on something that can be easily automated and configured, still use the power of engineering for the necessary parts. &lt;/p&gt;

&lt;p&gt;Another example is specialized e-commerce tools such as Stripe or Ecwid. Instead of creating our own e-commerce sub-tool, we at Readymag have integrated them. We try to leave each part of the pipeline to the specialized tool, be it code or no-code. &lt;/p&gt;

&lt;p&gt;And we think of Readymag as another such tool — a web editor, great for interactive graphics and interactive UX, but possibly powered up with additional APIs or custom code for larger and more complex projects. A full-fledged no-code approach is limiting, but a specific no-code tool might significantly increase your development process. &lt;/p&gt;

&lt;p&gt;Summing it up — never buy into no-code as a mantra, but always keep an eye out for its niche practical uses.&lt;/p&gt;

</description>
      <category>readymag</category>
      <category>nocode</category>
      <category>webdesign</category>
    </item>
    <item>
      <title>What makes Feature Flags a path to faster and safer development</title>
      <dc:creator>Anton Vasin</dc:creator>
      <pubDate>Fri, 20 Aug 2021 12:07:22 +0000</pubDate>
      <link>https://forem.com/readymag/what-makes-feature-flags-a-path-to-faster-and-safer-development-obd</link>
      <guid>https://forem.com/readymag/what-makes-feature-flags-a-path-to-faster-and-safer-development-obd</guid>
      <description>&lt;p&gt;&lt;a href="https://martinfowler.com/articles/feature-toggles.html"&gt;Feature Flags&lt;/a&gt; (or Feature Toggles) is a way of working with code that allows teams to modify system behavior, without changing existing or deploying new code. It allows teams to handle deployments and new releases separately,  and iterate rapidly.&lt;/p&gt;

&lt;p&gt;Feature Flags vary by usage and context. Here we’ll look into three of the most powerful techniques that you can leverage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trunk-based development&lt;/li&gt;
&lt;li&gt;Uncoupling deployments and releases&lt;/li&gt;
&lt;li&gt;Living on your product (aka &lt;a href="https://en.wikipedia.org/wiki/Eating_your_own_dog_food"&gt;dogfooding&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When getting started with this approach, your product doesn't necessarily have to be complex. Also the solution you choose doesn't have to cover every possible use case, at least not from day one. Teams can use popular SaaS solutions, open-source projects or just start with a simple JSON file in their repo, so don't be intimidated by how much is possible. The most important thing is to change your thinking when it comes to writing and releasing new code.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://trunkbaseddevelopment.com"&gt;Trunk-based development&lt;/a&gt; enables engineers to check unfinished features into the main branch and safely release them into production at any time. When working this way, teams develop code in short-lived branches that enable developers to merge and deploy safely. This technique helps cut the time it takes to merge new code by avoiding the big, complex conflicts usually associated with long-lived feature branches. Checking in code regularly guarantees that teams can move quickly, without obstacles. To do this, teams use feature flags to disable unfinished code until it's time for release. When the feature is ready, simply enable the flag and release the new feature into the world. This allows teams to separate releases from the act of merging code. It also means moving from long development cycles to a series of short ones. Instead of big changes being merged at once, there are now lots of small iterative changes that are merged and deployed often, preferably many times a day. This helps teams move faster and safer.&lt;/p&gt;

&lt;p&gt;Being able to tie a particular feature to a particular user, or a group of users, allows you to verify your ideas cheaply and safely. Аlmost every new feature in &lt;a href="https://readymag.com?utm_source=dev.to&amp;amp;utm_medium=pr&amp;amp;utm_campaign=feature_flags"&gt;Readymag&lt;/a&gt; is released for internal users first — so our design team can test them thoroughly. It is especially powerful if you use your own product in your daily work, because your beta testers are right there working alongside you. Get feedback fast and test your ideas before you release a feature for the general public.&lt;/p&gt;

&lt;p&gt;Feature Flags allow easy experimentation. Often there is some idea, maybe a concept for a new feature or a smart way of improving existing code, that needs to be tested. Using Feature Flags allows developers to explore these ideas in a real production environment without fear of breaking something. Try things out and see what's working, and what's not, then decide how to act on it later. Combine this way of working with monitoring and product analytics, and you'll get a really powerful combo!&lt;/p&gt;

&lt;p&gt;So let's illustrate all of this with a real world example. Recently we launched new subscription plans for Readymag. To implement them, we needed to adjust some parts of our application that control available features and what limits are set for each plan. This is where Feature Flags help immensely. First, we added a new Feature Flag to our system. From there we wrote new code and adjusted the existing behaviour behind the flag. We did this over the course of many iterations, continuously deploying new changes into production. For example, the front end looked like this:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Here, we conditionally render a new part of the UI. Existing code works as usual until the flag is enabled.&lt;br&gt;
Since we have two different components in this example, the team working on this can safely work on a new interface without disrupting the existing flow.&lt;br&gt;
Elsewhere in our system we would have code like this, implementing another part of this update:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Of course it is always a good idea to test your code, especially when using something as dynamic as Feature Flags. So we did just that:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;When we were ready to launch the update, we just enabled this flag and the feature was rolled out. Not only did this approach allow us to merge and deploy code continuously, it also made it possible to enable new code branches in several services at once without coordinated deployments. &lt;/p&gt;

&lt;p&gt;Here are some best practices around Feature Flags that we use in Readymag:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feature Flags should have a short life cycle—they are used only before a feature is released, not to control long-term product behaviour.&lt;/li&gt;
&lt;li&gt;Always ask yourself if it would be easy to delete a particular flag later.&lt;/li&gt;
&lt;li&gt;Each Feature Flag is responsible for a single feature. It should have a name that is commonly understood.&lt;/li&gt;
&lt;li&gt;All flags should have similar semantics: all flags enable something, they don’t disable features.&lt;/li&gt;
&lt;li&gt;Sometimes it is better to duplicate code to ensure clean removal of the flag when the feature is released.&lt;/li&gt;
&lt;li&gt;Flags can and should be used for experimentation and to verify hypotheses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, the next time you're battling with some exhausting merge conflict, or you find your team holding on to some code because it’s too early for it to be released, or you wish that your team could try out some idea but it’s too dangerous,  try using Feature Flags. Solve your problem at hand, and hopefully change your whole product development cycle for the better.&lt;/p&gt;

</description>
      <category>featureflags</category>
      <category>readymag</category>
      <category>experimentation</category>
      <category>trunkbaseddevelopment</category>
    </item>
  </channel>
</rss>
