Selection in Angular Rich Text Editor Component

18 Nov 201822 minutes to read

Text selection

The Rich Text Editor supports character range-based text selection using the Syncfusion Slider component. 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 component 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.

@Component({
  imports: [
    RichTextEditorAllModule, SliderModule
  ],
  standalone: true,
  selector: 'app-root',
  template: ` <ejs-slider #rangeSlider [type]="'Range'" [value]="sliderValue" [min]="0" [max]="maxLength"></ejs-slider>`,
  providers: [ToolbarService, LinkService, ImageService, HtmlEditorService]
})

export class AppComponent {
    public sliderValue: number[] = [0, 50];
    public maxLength: number = 400;
}

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.

@Component({
  imports: [
    RichTextEditorAllModule, SliderModule
  ],
  standalone: true,
  selector: 'app-root',
  template: `<ejs-richtexteditor #rte [value]="rteContent" (created)="onEditorCreated()" ></ejs-richtexteditor>`,
  providers: [ToolbarService, LinkService, ImageService, HtmlEditorService]
})

export class AppComponent {
    public sliderValue: number[] = [0, 50];
    public maxLength: number = 400;
    public rteContent: string = `<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>`;
    
    onEditorCreated(): void {
        setTimeout(() => {
            const panel = this.rte.contentModule.getEditPanel() as HTMLElement;
            const realLength = panel.textContent?.length ?? 0;
            this.maxLength = realLength;
            this.rangeSlider.max = realLength;
            this.rangeSlider.dataBind();
            panel.focus();
            this.onChange({ value: this.rangeSlider.value } as ChangeEventArgs);
        }, 100);
    }
}

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.

@Component({
  imports: [
    RichTextEditorAllModule, SliderModule
  ],
  standalone: true,
  selector: 'app-root',
  template: ` <ejs-slider #rangeSlider [type]="'Range'" [value]="sliderValue" [min]="0" [max]="maxLength" (change)="onChange($event)"></ejs-slider>`,
  providers: [ToolbarService, LinkService, ImageService, HtmlEditorService]
})

export class AppComponent {
    public sliderValue: number[] = [0, 50];
    public maxLength: number = 400;

    onChange(args: ChangeEventArgs): void {
        const [start, end] = args.value as any;
        const panel = this.rte.contentModule.getEditPanel() as HTMLElement;
        const maxLength = panel.textContent?.length ?? 0;

        const safeStart = Math.min(start, maxLength);
        const safeEnd = Math.min(end, maxLength);

        const startInfo = this.getTextNodeAtOffset(panel, safeStart);
        const endInfo = this.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.

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

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

        if (currentOffset + nodeLength >= offset) {
            return {
            node,
            offset: offset - currentOffset,
            };
        }

        currentOffset += nodeLength;
        }

        return null;
    }

}
import { Component, ViewChild } from '@angular/core';
import { SliderComponent, ChangeEventArgs, SliderModule } from '@syncfusion/ej2-angular-inputs';
import { RichTextEditorComponent, RichTextEditorModule, ToolbarService, LinkService, ImageService, HtmlEditorService, QuickToolbarService, TableService, VideoService, AudioService, PasteCleanupService } from '@syncfusion/ej2-angular-richtexteditor';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RichTextEditorModule, SliderModule],
  template: `
    <div id="container">
      <div class="sliderwrap">
        <label class="labeltext userselect">Range Slider</label>
        <ejs-slider
          #rangeSlider
          [type]="'Range'"
          [value]="sliderValue"
          [min]="0"
          [max]="maxLength"
          (change)="onChange($event)"
        ></ejs-slider>
      </div>

      <ejs-richtexteditor
        #rte
        [value]="rteContent"
        (created)="onEditorCreated()"
      ></ejs-richtexteditor>
    </div>
  `,
  providers: [
    ToolbarService,
    LinkService,
    ImageService,
    HtmlEditorService,
    QuickToolbarService,
    TableService,
    VideoService,
    AudioService,
    PasteCleanupService,
  ],
})
export class AppComponent {
  @ViewChild('rangeSlider') rangeSlider!: SliderComponent;
  @ViewChild('rte') rte!: RichTextEditorComponent;

  sliderValue: number[] = [0, 50];
  maxLength: number = 400;

  rteContent: string = `<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>`;

  onEditorCreated(): void {
    setTimeout(() => {
      const panel = this.rte?.[
        'contentModule'
      ]?.getEditPanel?.() as HTMLElement;
      const realLength = panel.textContent?.length ?? 0;

      this.maxLength = realLength;
      this.rangeSlider.max = realLength;
      this.rangeSlider.dataBind();

      panel.focus();

      this.onChange({ value: this.rangeSlider.value } as ChangeEventArgs);
    }, 100);
  }

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

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

      if (currentOffset + nodeLength >= offset) {
        return {
          node,
          offset: offset - currentOffset,
        };
      }

      currentOffset += nodeLength;
    }

    return null;
  }

  onChange(args: ChangeEventArgs): void {
    const [start, end] = args.value as any;
    const panel = this.rte?.['contentModule']?.getEditPanel?.() as HTMLElement;
    const maxLength = panel.textContent?.length ?? 0;

    const safeStart = Math.min(start, maxLength);
    const safeEnd = Math.min(end, maxLength);

    const startInfo = this.getTextNodeAtOffset(panel, safeStart);
    const endInfo = this.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);
      }
    }
  }
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));

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 { Component, ViewChild } from '@angular/core';
import { RichTextEditorModule, RichTextEditorComponent, ToolbarService, LinkService, ImageService, HtmlEditorService, QuickToolbarService, TableService, PasteCleanupService } from '@syncfusion/ej2-angular-richtexteditor';

@Component({
  imports: [
    RichTextEditorModule
  ],
  standalone: true,
  selector: 'app-root',
  template: `<button id="btn" style="margin:0 0 20px 0" class="e-btn" (click)="selectParagraph()">Select Paragraph</button>
    <ejs-richtexteditor #rte id="editor" [value]="value"></ejs-richtexteditor>`,
  providers: [ToolbarService, LinkService, ImageService, HtmlEditorService, QuickToolbarService, TableService, PasteCleanupService]
})

export class AppComponent {
  @ViewChild('rte') editor!: RichTextEditorComponent;

  public value: string = `<p>This is paragraph one.</p><p>This is paragraph two.</p>`;

  selectParagraph(): void {
    const panel = (this.editor as any).contentModule.getEditPanel();
    const paragraphs = panel.querySelectorAll('p');

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

      const selection = window.getSelection();
      if (selection) {
        selection.removeAllRanges();
        selection.addRange(range);
      }
    }
  }
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));

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 { Component, ViewChild } from '@angular/core';
import { RichTextEditorModule, RichTextEditorComponent, ToolbarService, LinkService, ImageService, HtmlEditorService, QuickToolbarService, TableService, PasteCleanupService } from '@syncfusion/ej2-angular-richtexteditor';

@Component({
    imports: [
      RichTextEditorModule
    ],
    standalone: true,
    selector: 'app-root',
    template: `<button id="btn" class="e-btn" style="margin-bottom: 20px;" (click)="selectCell()">Select Cell</button>
      <ejs-richtexteditor #rte [height]="300" [value]="value"></ejs-richtexteditor>`,
    providers: [ToolbarService, LinkService, ImageService, HtmlEditorService, QuickToolbarService, TableService, PasteCleanupService]
})

export class AppComponent {
  @ViewChild('rte') rte!: RichTextEditorComponent;

  public value: string = `
    <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>
  `;

  selectCell(): void {
    const panel = (this.rte as any).contentModule.getEditPanel();
    const cells = panel.querySelectorAll('td');

    if (cells.length > 2) {
      const cell = cells[2]; // Third cell
      const range = document.createRange();
      range.selectNode(cell);

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

      cell.style.backgroundColor = '#cce5ff';
      cell.classList.add('e-cell-select');
    }
  }
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));

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 { Component, ViewChild } from '@angular/core';
import { RichTextEditorModule, RichTextEditorComponent, ToolbarService, LinkService, ImageService, HtmlEditorService, QuickToolbarService, TableService, PasteCleanupService } from '@syncfusion/ej2-angular-richtexteditor';

@Component({
  imports: [
    RichTextEditorModule
  ],
  standalone: true,
  selector: 'app-root',
  template: `<button id="btn" style="margin:0 0 20px 0" class="e-btn" (click)="selectAllContent()">Select All</button><ejs-richtexteditor  #rte id='editor' [value]='value'></ejs-richtexteditor>`,
  providers: [ToolbarService, LinkService, ImageService, HtmlEditorService, QuickToolbarService, TableService, PasteCleanupService]
})

export class AppComponent {
  @ViewChild('rte') editor!: RichTextEditorComponent;
  public value: string =
    '<p>The Rich Text Editor component is WYSIWYG ("what you see is what you get") editor that provides the best user experience to create and update the content. Users can format their content using standard toolbar commands.</p><p><b>Key features:</b></p><ul><li><p>Provides &lt;IFRAME&gt; and &lt;DIV&gt; modes</p></li><li><p>Capable of handling markdown editing.</p></li><li><p>Contains a modular library to load the necessary functionality on demand.</p></li><li><p>Provides a fully customizable toolbar.</p></li><li><p>Provides HTML view to edit the source directly for developers.</p></li><li><p>Supports third-party library integration.</p></li><li><p>Allows preview of modified content before saving it.</p></li><li><p>Handles images, hyperlinks, video, hyperlinks, uploads, etc.</p></li><li><p>Contains undo/redo manager.</p></li><li><p>Creates bulleted and numbered lists.</p></li></ul>';

  selectAllContent(): void {
    this.editor.selectAll();
  }
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));