Getting Started
Overview
Installation
Theme
Forms
Basic Inputs
Custom Inputs
Select
Components
Components
78
Micro Charts
Navigation
Libraries
Content Editor
Data View
Form Builder
Headless Editor
Image Designer
Kanban Board
PDF Builder
PDF Signer
PDF Viewer
Video Player
GitHub repositoryGitHub starsGitHub forksNgStarter UI npm versionNgStarter UI npm downloads per month

Marks and Formatting

Marks annotate ranges of text: bold, links, colors, comments, mentions. A mark has a type and optional serializable attrs. Plugins decide how marks render.

Loading example…

Mark definitions

const underlinePlugin = defineNgsHeadlessEditorPlugin({
  id: 'underline',
  marks: [{ type: 'underline', tagName: 'u' }],
  commands: [toggleUnderline],
  keymap: [{ key: 'Mod-u', command: toggleUnderline }]
});

const mentionMark: NgsHeadlessEditorMarkDefinition = {
  type: 'mention',
  tagName: 'span',
  applyAttributes: (element, mark) => {
    element.classList.add('mention');
    element.dataset['userId'] = String(mark.attrs?.['userId'] ?? '');
  },
  readAttributes: element => ({ userId: element.dataset['userId'] ?? '' })
};
MemberDescription
typeUnique mark type stored in the document.
tagNameElement used to wrap marked text.
parseTagsExtra tag names recognized when browser-owned DOM (composition, drop) is read back.
applyAttributes(element, mark)Writes mark attributes to the element. Validate values here.
readAttributes(element)Reads attributes back from the DOM.

A run with several marks is rendered as nested wrappers, one per mark, in the order of mark types. Each wrapper carries data-ngs-headless-editor-mark.

Applying marks

MethodWith a selectionAt a caret
toggleMark(type, attrs?)Adds the mark to the range, or removes it if the whole range already has it.Toggles the mark for the next typed text.
setMark(type, attrs?)Applies or replaces the mark and its attributes.Arms the mark for the next typed text.
unsetMark(type)Removes the mark from the range.Disarms the mark for the next typed text.
isMarkActive(type)True when the whole range has the mark.True when the next typed text will have it.
getActiveMark(type)The mark with its attributes at the selection, for example the current link href.

Stored marks

By default, text typed at a caret inherits the marks of the text before it. Toggling a mark at a caret stores an explicit set of marks for the next insertion, exposed as the storedMarks signal. Stored marks are dropped when the caret moves, after text is inserted, and on undo or redo. Toggling bold off at the end of bold text therefore continues in plain text.

editor.toggleMark('bold');        // caret: arms bold
editor.isMarkActive('bold');      // true, toolbar shows Bold pressed
editor.storedMarks();             // [{ type: 'bold' }]
editor.insertText('Bold text');   // inserted with bold, stored marks cleared

Formatting everywhere

Formatting commands work wherever text is edited: in the document and in nested editors such as table cells. A nested editor registers itself as the editor's inlineTarget while it has focus; toggleMark(), setMark(), unsetMark(), isMarkActive(), getActiveMark(), storedMarks, selection, and insertText() of the document editor then act on it. Commands and toolbars written against the document editor need no changes.

  • Use canApplyMark(type) in a command's enabled predicate, so controls disable themselves where the mark is not allowed or the editor is read-only.
  • Use canEditBlocks() for block commands; it is false while a nested editor is active.
  • Set nested: false on a mark definition to keep it out of nested editors.
  • Nested editors can restrict marks further, for example tableEditorPlugin({ formatting: ['bold'] }).
const toggleUnderline: NgsHeadlessEditorCommand = {
  id: 'toggle-underline',
  execute: editor => editor.toggleMark('underline'),
  enabled: editor => editor.canApplyMark('underline'),   // false in cells without underline
  active: editor => editor.isMarkActive('underline')
};

const toggleHeading: NgsHeadlessEditorCommand = {
  id: 'toggle-heading',
  execute: editor => editor.toggleBlock('heading'),
  enabled: editor => editor.canEditBlocks(),             // false inside a table cell
  active: editor => editor.isBlockActive('heading')
};

editor.inlineTarget();   // nested editor that receives formatting, or null

Colors

colorEditorPlugin() adds the textColor and backgroundColor marks and four commands. Values are checked with normalizeNgsHeadlessEditorColor(), which accepts hex, named, functional (rgb, hsl, oklch, color), and var(--token) colors and rejects anything that could escape a style declaration.

provideNgsHeadlessEditor(
  withHeadlessEditorPlugin(basicTextEditorPlugin()),
  withHeadlessEditorPlugin(colorEditorPlugin())
);

editor.execute(NGS_HEADLESS_EDITOR_SET_TEXT_COLOR, 'var(--ngs-color-danger)');
editor.execute(NGS_HEADLESS_EDITOR_SET_BACKGROUND_COLOR, '#fef08a');
editor.getActiveMark(NGS_HEADLESS_EDITOR_TEXT_COLOR_MARK)?.attrs?.['color'];

normalizeNgsHeadlessEditorColor('red; display: none');  // null

Attribute safety

Mark attributes come from documents that may be stored or edited elsewhere. Validate them in applyAttributes before writing to the DOM, for example accept only http and https URLs for links, and validate again when you serialize to HTML.