Syncfusion AI Assistant

How can I help you?

Selection in EJ2 TypeScript Rich Text Editor Control

20 Nov 202524 minutes to read

Text selection

The Rich Text Editor supports character range-based text selection using the Syncfusion Slider control. This feature allows users to select a specific range of characters (e.g., 33–45) within the editor content, which is then automatically highlighted.

Adding a Slider for character range selection

The Rich Text Editor can be integrated with the Slider control to enable precise character range-based text selection. The slider is configured in range type, allowing users to select a start and end index within the editor content. When the slider values change, the corresponding text range is highlighted automatically.

This approach is particularly useful for scenarios where exact character-level selection is required for operations such as copying, formatting, or analysis.

let rangeObj: Slider = new Slider({
  value: [0, 50],
  type: 'Range',
  min: 0,
  max: 400,
  change: onChange,
});
rangeObj.appendTo('#range');

Dynamic range adjustment based on content

When the editor is created, the actual length of the text content is calculated, and the slider’s maximum value is updated dynamically to match this length. This ensures that the slider range always reflects the current content size. The editor is also focused programmatically to make the selection visible, and an initial selection is applied based on the slider’s default values.

let editor: RichTextEditor = new RichTextEditor({
  value: `<p>The Syncfusion Rich Text Editor, a WYSIWYG editor...</p>`,
  height: 400,
  created: (): void => {
    setTimeout(() => {
      const panel = editor.contentModule.getEditPanel() as HTMLElement;
      const realLength = panel.textContent?.length ?? 0;

      rangeObj.max = realLength;  // Update slider max
      rangeObj.dataBind();
      panel.focus();              // Ensure selection is visible

      onChange({ value: rangeObj.value } as SliderChangeEventArgs);
    }, 100);
  },
});
editor.appendTo('#editor');

Precise selection using DOM range

The selection logic is implemented in the change event of the slider. It retrieves the start and end positions from the slider and ensures they are within valid bounds. The code then uses a helper function, getTextNodeAtOffset(), which employs a TreeWalker to traverse text nodes and locate the exact node and offset for the given character positions.

A Range object is created using these offsets and applied to the current selection using the browser’s Selection API. This guarantees accurate highlighting even when the content spans multiple text nodes.

function onChange(args: SliderChangeEventArgs): void {
  const [start, end] = args.value as number[];
  const panel = editor.contentModule.getEditPanel() as HTMLElement;
  const maxLength = panel.textContent?.length ?? 0;

  // Ensure start and end are within valid bounds
  const safeStart = Math.min(start, maxLength);
  const safeEnd = Math.min(end, maxLength);

  // Find the text node and relative offset for both start and end
  const startInfo = getTextNodeAtOffset(panel, safeStart);
  const endInfo = getTextNodeAtOffset(panel, safeEnd);

  if (startInfo && endInfo) {
    const range = document.createRange();
    range.setStart(startInfo.node, startInfo.offset);
    range.setEnd(endInfo.node, endInfo.offset);

    const selection = window.getSelection();
    if (selection) {
      selection.removeAllRanges();
      selection.addRange(range);
    }
  }
}

Helper function for accurate offset calculation

The getTextNodeAtOffset() function uses a TreeWalker to traverse text nodes inside the editor and determine the exact node and offset for a given character index. This ensures that even complex content structures are handled correctly.

function getTextNodeAtOffset(root: Node, offset: number): { node: Text; offset: number } | null {
  let currentOffset = 0;
  const walker: TreeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);

  while (walker.nextNode()) {
    const node = walker.currentNode as Text;
    const nodeLength = node.textContent.length;

    if (currentOffset + nodeLength >= offset) {
      return { node, offset: offset - currentOffset };
    }
    currentOffset += nodeLength;
  }
  return null;
}
import { Slider, SliderChangeEventArgs } from '@syncfusion/ej2-inputs';
import {RichTextEditor, Toolbar, Link, Image, HtmlEditor, QuickToolbar, Table, Video, Audio, PasteCleanup,
} from '@syncfusion/ej2-richtexteditor';

RichTextEditor.Inject(Toolbar,Link,Image,HtmlEditor,QuickToolbar,Table,Video,Audio,PasteCleanup);

let rangeObj: Slider = new Slider({
  value: [0, 50],
  type: 'Range',
  min: 0,
  max: 400,
  change: onChange,
});
rangeObj.appendTo('#range');

let editor: RichTextEditor = new RichTextEditor({
  value: `<p>The Syncfusion Rich Text Editor, a WYSIWYG (what you see is what you get) editor, is a user interface that allows you to create, edit, and format rich text content. You can try out a demo of this editor here. Key features: Provides IFRAME and DIV modes. Bulleted and numbered lists. Handles images, hyperlinks, videos, hyperlinks, uploads, etc. Contains undo/redo manager.</p>`,
  height: 400,
  created: (): void => {
    setTimeout(() => {
      const panel = editor.contentModule.getEditPanel() as HTMLElement;
      const realLength = panel.textContent?.length ?? 0;

      // Update slider max based on actual text length
      rangeObj.max = realLength;
      rangeObj.dataBind();

      // Focus the editor to ensure selection is visible
      panel.focus();

      // Apply initial selection
      onChange({ value: rangeObj.value } as SliderChangeEventArgs);
    }, 100); // Delay to ensure DOM is ready
  },
});
editor.appendTo('#editor');

function getTextNodeAtOffset(
  root: Node,
  offset: number
): { node: Text; offset: number } | null {
  let currentOffset = 0;

  // Create a TreeWalker to traverse only text nodes inside the root
  const walker: TreeWalker = document.createTreeWalker(
    root,
    NodeFilter.SHOW_TEXT, // Only consider text nodes
    null,
    false
  );

  // Traverse each text node
  while (walker.nextNode()) {
    const node = walker.currentNode as Text;
    const nodeLength = node.textContent.length;

    // Check if the desired offset falls within this node
    if (currentOffset + nodeLength >= offset) {
      return {
        node, // The text node where the offset is located
        offset: offset - currentOffset, // Offset relative to this node
      };
    }

    // Accumulate the total offset as we move through nodes
    currentOffset += nodeLength;
  }

  // If offset is beyond total text length, return null
  return null;
}

function onChange(args: SliderChangeEventArgs): void {
  const [start, end] = args.value as number[];
  const panel = editor.contentModule.getEditPanel() as HTMLElement;
  const maxLength = panel.textContent?.length ?? 0;

  // Ensure start and end are within valid bounds
  const safeStart = Math.min(start, maxLength);
  const safeEnd = Math.min(end, maxLength);

  // Find the text node and relative offset for both start and end
  const startInfo = getTextNodeAtOffset(panel, safeStart);
  const endInfo = getTextNodeAtOffset(panel, safeEnd);

  if (startInfo && endInfo) {
    const range = document.createRange();
    range.setStart(startInfo.node, startInfo.offset);
    range.setEnd(endInfo.node, endInfo.offset);

    const selection = window.getSelection();
    if (selection) {
      selection.removeAllRanges();
      selection.addRange(range);
    }
  }
}
<!DOCTYPE html>
<html lang="en">

<head>
    <title>Essential JS 2 Rich Text Editor</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Typescript UI Controls" />
    <meta name="author" content="Syncfusion" />
    <link href="index.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-base/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-richtexteditor/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-inputs/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-lists/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-navigations/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-popups/styles/tailwind3.css" rel="stylesheet" />
     <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-buttons/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-splitbuttons/styles/tailwind3.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>
    <script src="systemjs.config.js"></script>
</head>

<body>
    <div id='loader'>Loading....</div>
    <div id="container">
    <div class="sliderwrap">
      <label class="labeltext userselect">Range Slider</label>
      <div id="range"></div>
    </div>
    <div id="editor"></div>
  </div>
</body>

</html>

Node selection

Node selection allows users to programmatically select entire HTML elements (nodes) such as paragraphs, images, or tables within the Rich Text Editor. This is useful when you want to highlight or manipulate specific content blocks without relying on manual user selection.

The following example demonstrates how to select a paragraph node programmatically using the browser’s native Range and Selection APIs.

import {RichTextEditor,Toolbar,Link,Image,HtmlEditor,QuickToolbar} from '@syncfusion/ej2-richtexteditor';
RichTextEditor.Inject(Toolbar, Link, Image, HtmlEditor, QuickToolbar);

let editor: RichTextEditor = new RichTextEditor({
  value: `<p>This is paragraph one.</p><p>This is paragraph two.</p>`,
});
editor.appendTo('#editor');

document.getElementById('btn').onclick = function () {
  const panel = editor.contentModule.getEditPanel() as HTMLElement;
  const paragraphs: NodeListOf<HTMLParagraphElement> = panel.querySelectorAll('p');

  if (paragraphs.length > 1) {
    const range: Range = document.createRange();
    range.selectNode(paragraphs[1]); // Select the second paragraph

    const selection: Selection | null = window.getSelection();
    if (selection) {
      selection.removeAllRanges();
      selection.addRange(range);
    }
  }
};
<!DOCTYPE html>
<html lang="en">

<head>
    <title>Essential JS 2 Rich Text Editor</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Typescript UI Controls" />
    <meta name="author" content="Syncfusion" />
    <link href="index.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-base/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-richtexteditor/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-inputs/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-lists/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-navigations/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-popups/styles/tailwind3.css" rel="stylesheet" />
     <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-buttons/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-splitbuttons/styles/tailwind3.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>
    <script src="systemjs.config.js"></script>
</head>

<body>
    <div id='loader'>Loading....</div>
    <div id="container">
      <div style="margin: 0 0 10px 0">
          <button id="btn" class="e-btn">Select Paragraph</button>
        </div>
        <div class="default-section">
          <div id="editor"></div>
        </div>
    </div>
</body>

</html>

Cell selection

Cell selection allows users to programmatically select specific table cells within the Rich Text Editor. This is useful for highlighting or manipulating content inside tables without requiring manual user interaction.

The following example demonstrates how to select a table cell programmatically using the browser’s native Range and Selection APIs.

import {RichTextEditor,Toolbar,Link,Image,HtmlEditor,QuickToolbar} from '@syncfusion/ej2-richtexteditor';
RichTextEditor.Inject(Toolbar, Link, Image, HtmlEditor, QuickToolbar);

let editor: RichTextEditor = new RichTextEditor({
  value: `<table style="width:100%; border-collapse: collapse;" border="1">
<thead>
  <tr>
    <th style="font-weight:bold; padding:8px;">Product</th>
    <th style="font-weight:bold; padding:8px;">Price</th>
    <th style="font-weight:bold; padding:8px;">Stock</th>
  </tr>
</thead>
<tbody>
  <tr>
    <td style="padding:8px;">Product A</td>
    <td style="padding:8px;">$25</td>
    <td style="padding:8px;">Available</td>
  </tr>
  <tr>
    <td style="padding:8px;">Product B</td>
    <td style="padding:8px;">$40</td>
    <td style="padding:8px;">Out of Stock</td>
  </tr>
</tbody>
</table>`,
});
editor.appendTo('#editor');

document.getElementById('btn').onclick = function () {
  const panel: HTMLElement = editor.contentModule.getEditPanel() as HTMLElement;
  const cells: NodeListOf<HTMLTableCellElement> = panel.querySelectorAll('td');

  if (cells.length > 0) {
    const cell: HTMLTableCellElement = cells[0]; // First cell
    const range: Range = document.createRange();
    range.selectNode(cell); // Logical selection of entire cell

    const selection: Selection | null = window.getSelection();
    if (selection) {
      selection.removeAllRanges();
      selection.addRange(range);
    }
    cell.style.backgroundColor = '#cce5ff';
    // Add Syncfusion's selection class for visual consistency
    cell.classList.add('e-cell-select');
  }
};
<!DOCTYPE html>
<html lang="en">

<head>
    <title>Essential JS 2 Rich Text Editor</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Typescript UI Controls" />
    <meta name="author" content="Syncfusion" />
    <link href="index.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-base/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-richtexteditor/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-inputs/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-lists/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-navigations/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-popups/styles/tailwind3.css" rel="stylesheet" />
     <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-buttons/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-splitbuttons/styles/tailwind3.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>
    <script src="systemjs.config.js"></script>
</head>

<body>
    <div id='loader'>Loading....</div>
    <div id="container">
      <div style="margin: 0 0 10px 0">
          <button id="btn" class="e-btn">Select Cell</button>
        </div>
        <div class="default-section">
          <div id="editor"></div>
        </div>
    </div>
</body>

</html>

Select all content

To select all content within the Rich Text Editor, use the selectAll method. This method highlights all the text and elements inside the editor, allowing users to perform actions such as formatting or deleting the entire content.

import {RichTextEditor,Toolbar,Link,Image,HtmlEditor,QuickToolbar} from '@syncfusion/ej2-richtexteditor';
RichTextEditor.Inject(Toolbar, Link, Image, HtmlEditor, QuickToolbar);

let editor: RichTextEditor = new RichTextEditor({
   value: `  <p>The Syncfusion Rich Text Editor, a WYSIWYG (what you see is what you get) editor, is a user interface that allows you to create, edit, and format rich text content. You can try out a demo of this editor here.</p><p><b>Key features:</b></p><ul><li><p>Provides &lt;IFRAME&gt; and &lt;DIV&gt; modes.</p></li><li><p>Bulleted and numbered lists.</p></li><li><p>Handles images, hyperlinks, videos, hyperlinks, uploads, etc.</p></li><li><p>Contains undo/redo manager. </p></li></ul><div style='display: inline-block; width: 60%; vertical-align: top; cursor: auto;'><img alt='Sky with sun' src='https://cdn.syncfusion.com/ej2/richtexteditor-resources/RTE-Overview.png' width='309' style='min-width: 10px; min-height: 10px; width: 309px; height: 174px;' class='e-rte-image e-imginline e-rte-drag-image' height='174' /></div>`,
});
editor.appendTo('#editor');

document.getElementById('btn').onclick = function () {
    editor.selectAll();
};
<!DOCTYPE html>
<html lang="en">

<head>
    <title>Essential JS 2 Rich Text Editor</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Typescript UI Controls" />
    <meta name="author" content="Syncfusion" />
    <link href="index.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-base/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-richtexteditor/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-inputs/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-lists/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-navigations/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-popups/styles/tailwind3.css" rel="stylesheet" />
     <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-buttons/styles/tailwind3.css" rel="stylesheet" />
    <link href="https://cdn.syncfusion.com/ej2/33.1.44/ej2-splitbuttons/styles/tailwind3.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>
    <script src="systemjs.config.js"></script>
</head>

<body>
    <div id='loader'>Loading....</div>
    <div id="container">
      <div style="margin: 0 0 10px 0">
          <button id="btn" class="e-btn">Select All </button>
        </div>
        <div class="default-section">
          <div id="editor"></div>
        </div>
    </div>
</body>

</html>