ProseMirror Schema
The Kendo UI for Angular Editor component is based on the ProseMirror library. ProseMirror provides a set of tools and concepts for building rich text editors, using user interface inspired by what-you-see-is-what-you-get.
Concepts
ProseMirror defines its own data structure, the Node, to represent content documents. The ProseMirror document is a tree-like structure comprised of nodes. Therefore, a document is an instance of Node with children that are also instances of Node.
Each ProseMirror document conforms to a specific schema. Document schemas allow you to edit documents with a custom structure without writing your own editor from scratch.
The schema describes all nodes that may occur in the document, the way they are nested, and any marks applied to them. A mark is a piece of information that can be attached to a node, such as it being emphasized, in code font, or a link. It has a type and optionally a set of attributes that provide further information (such as the target of the link).
For more details, refer to the ProseMirror schema guide.
Editor Schema
The Editor uses a built-in schema with predefined nodes and marks, which describes some of the most common HTML tags and their basic attributes like id, class, and style.
To modify existing nodes and marks or define new ones, you can customize the default schema or create a new one that best fits your needs.
Creating a custom Editor schema enables you to:
- Define new types of
nodesandmarks. - Add new attributes to existing
nodesandmarks. - Remove existing
nodesandmarks.
To create a custom schema object, use one of the following approaches:
To replace the default schema with the custom one, bind the Editor schema option to the new schema object.
You can set a custom schema only once—upon the initialization of the Editor component. Further changes to the object to which the
schemainput is bound will not be reflected, and the Editor will continue to use the initially provided schema or, if no schema is initially provided, the built-in one.
Modifying the Default Schema
The following example demonstrates how to:
- Add a new node to the
nodesobject of the schema. - Add a new mark to the
marksobject of the schema. - Add a new attribute to an existing node.
The following step-by-step guide shows how to recreate the above example by enhancing the built-in schema through adding definitions for HTML tags and attributes which are not available out-of-the-box. The demonstrated approach relies on importing the default schema, and modifying its nodes and marks collections. The schema can store nodes and marks in a custom data type, OrderedMap. The OrderedMap API exposes utility methods for accessing specific items and updating the collection.
-
Import
schema(the default Editorschemaobject) andSchema(the ProseMirrorSchemaclass) from the@progress/kendo-angular-editorpackage:tsimport { schema, Schema } from '@progress/kendo-angular-editor'; -
Customize the existing nodes by using the relevant properties and methods from the
NodeSpecandOrderedMapAPIs. For example, to add a newdirattribute to the existingparagraphnode, follow the steps:-
Use the
OrderedMapgetmethod to access theparagraphnode.tsconst paragraph = { ...schema.spec.nodes.get('paragraph') }; -
Use the
NodeSpec attrsproperty to define the new attribute.tsparagraph.attrs['dir'] = { default: null }; -
Use the
OrderedMapupdatemethod to replace the existingparagraphnode with the customized one.tslet nodes = schema.spec.nodes.update('paragraph', paragraph);
-
-
Create new nodes and add them to the existing
nodescollection. For example, to provide support for the<iframe>tag, create and add aniframenode:-
Define the
iframenode as a configuration object which complies with theNodeSpecinterface.ts// NodeSpec interface // http://prosemirror.net/docs/ref/#model.NodeSpec const iframe = { attrs: { src: { default: null }, style: { default: null } }, group: 'block', selectable: false, parseDOM: [ { tag: 'iframe', getAttrs: (dom) => ({ src: dom.getAttribute('src'), style: dom.getAttribute('style') }) } ], toDOM: node => { const attrs = { src: node.attrs.src, style: node.attrs.style, frameborder: '0', allowfullscreen: 'true' }; return [ 'iframe', attrs ]; } }; -
Append the new
nodeto thenodescollection by using theOrderedMapaddToEndmethod.ts// Append the new node. nodes = schema.spec.nodes.addToEnd('iframe', iframe);
-
-
Create new marks and add them to the existing
markscollection. For example, to provide support for the<s>tag, create and add ansmark:-
Define the mark as a configuration object, which complies with the
MarkSpecinterface. Optionally, use a helper function that generates tags by a predefined tag name, passed as an argument.ts// Append a new mark representing the <s> formatting tag. // MarkSpec interface // https://prosemirror.net/docs/ref/#model.MarkSpec const tagMark = tag => ({ [tag]: { name: tag, inclusive: true, parseDOM: [ { tag } ], toDOM: () => [ tag, 0 ] } }); const mark = tagMark('s'); -
Append the new
markto themarkscollection by using theOrderedMapappendmethod.tslet marks = schema.spec.marks.append(mark);
-
-
Use the
Schemaconstructor to create the customized schema with the modified and newly addednodesandmarks.ts// TS Class code public mySchema = new Schema({ marks, nodes }); -
Bind the Editor
schemainput to the new schema.ts// Template <kendo-editor [schema]="mySchema" ...></kendo-editor> // TS Class code public mySchema = new Schema({ marks, nodes });
Creating a New Schema
The following example demonstrates how to create a custom Editor schema.
The following step-by-step guide shows how to create a new schema by configuring all nodes and marks which the document may contain. The demonstrated approach relies on creating new nodes and marks objects, and passing them to the Schema class constructor.
-
Define all
marksand their respective attributes so that they comply with the ProseMirrorMarkSpecinterface. Optionally, use a helper function that generates tags by a predefined tag name, passed as an argument.ts// https://prosemirror.net/docs/ref/#model.MarkSpec const tagMark = tag => { return { [tag]: { name: tag, inclusive: true, parseDOM: [ { tag: tag } ], toDOM: () => [ tag, 0 ] } }; }; const marks = { link: { attrs: { ...commonAttributes(), href: { default: null }, target: { default: null }, title: { default: null } }, inclusive: false, parseDOM: [ { tag: 'a', getAttrs: getAttributes } ], toDOM(node) { return [ 'a', getAttrs(node.attrs), 0 ]; } }, ...tagMark('strong'), ...tagMark('b'), ...tagMark('em'), ...tagMark('i'), ...tagMark('u'), ...tagMark('del'), ...tagMark('sub'), ...tagMark('sup'), ...tagMark('code'), style: { attrs: { ...commonAttributes() }, parseDOM: [ { tag: 'span', getAttrs: getAttributes } ], toDOM: node => (hasAttrs(node.attrs) ? [ 'span', getAttrs(node.attrs), 0 ] : [ 'span', 0 ]) } }; -
Define all
nodesand their respective attributes so that they comply with the ProseMirrorNodeSpecinterface.tsconst nodes = { // The top level document node. doc: { content: 'block+' }, paragraph: { content: 'inline*', group: 'block', attrs: { ...commonAttributes() }, parseDOM: [ { tag: 'p', getAttrs: getAttributes } ], toDOM: node => hasAttrs(node.attrs) ? [ 'p', getAttrs(node.attrs), 0 ] : [ 'p', 0 ] }, div: { content: 'block*', group: 'block', attrs: { ...commonAttributes() }, parseDOM: [ { tag: 'div', getAttrs: getAttributes } ], toDOM: node => hasAttrs(node.attrs) ? [ 'div', getAttrs(node.attrs), 0 ] : [ 'div', 0 ] }, // ...more nodes } -
Use the
Schemaconstructor to create the new schema with thenodesandmarksdefined in the previous steps.tsimport { Schema } from '@progress/kendo-angular-editor'; export const mySchema = new Schema({ nodes, marks }); -
Bind the Editor
schemainput to the newly created schema.tsimport { mySchema } from './custom-schema'; // Ttemplate <kendo-editor [schema]="mySchema" ...></kendo-editor> // TS Class code public mySchema = mySchema;
Custom Elements in the Editor Schema
You can create domain-specific document elements in the Kendo UI for Angular Editor by extending the ProseMirror schema. This allows you to insert and manipulate custom elements, such as specialized dynamic placeholders, or even unique entities like weather nodes.
This implementation is inspired by the ProseMirror Dino example, which demonstrates how to create custom document elements by extending the schema with site-specific elements.
-
Define the supported types of values for the custom element.
tsconst weatherTypes = ['cloudy', 'sunny', 'rainy']; -
Define the custom node specification (
NodeSpec) for the weather element.tsconst getWeatherImage = (type: string): string => { switch (type) { case 'cloudy': return 'https://demos.telerik.com/kendo-angular-ui/assets/layout/tabstrip/cloudy.png'; case 'sunny': return 'https://demos.telerik.com/kendo-angular-ui/assets/layout/tabstrip/sunny.png'; case 'rainy': return 'https://demos.telerik.com/kendo-angular-ui/assets/layout/tabstrip/rainy.png'; default: return 'https://demos.telerik.com/kendo-angular-ui/assets/layout/tabstrip/cloudy.png'; } }; export const weatherNodeSpec: NodeSpec = { attrs: { type: { default: 'cloudy' } }, inline: true, group: 'inline', draggable: true, toDOM: (node) => [ 'img', { 'weather-type': node.attrs.type, src: getWeatherImage(node.attrs.type), title: node.attrs.type, class: 'weather-icon', style: 'width: 20px; height: 20px; vertical-align: middle;', }, ], parseDOM: [ { tag: 'img[weather-type]', getAttrs: (dom) => { let type = dom.getAttribute('weather-type'); return weatherTypes.indexOf(type) > -1 ? { type } : false; }, }, ], }; -
Extend the default schema with the custom node specification.
tsimport { Schema, schema } from '@progress/kendo-angular-editor'; import { weatherNodeSpec } from './new-node'; const nodes = schema.spec.nodes.addBefore('image', 'weather', weatherNodeSpec); const marks = schema.spec.marks; export const mySchema = new Schema({ nodes, marks, }); -
Load the custom schema in the Editor component and test it with some sample weather elements.
tsimport { Component } from '@angular/core'; import { KENDO_EDITOR, Schema } from '@progress/kendo-angular-editor'; import { KENDO_TOOLBAR } from '@progress/kendo-angular-toolbar'; import { mySchema } from './custom-schema'; @Component({ selector: 'my-app', standalone: true, imports: [KENDO_EDITOR, KENDO_TOOLBAR], template: ` <kendo-editor [value]="value" [schema]="mySchema"> </kendo-editor> `, }) export class AppComponent { public value = ` <img class="weather-icon" weather-type="sunny"> <img class="weather-icon" weather-type="cloudy"> <img class="weather-icon" weather-type="rainy"> `; public mySchema: Schema = mySchema; }
The following example demonstrates how to create a custom document element in the Kendo UI for Angular Editor by extending the ProseMirror schema.
Enabling Clickable Links
By default, links in the Kendo UI for Angular Editor are non-interactive and only trigger selection behavior. To make links clickable and allow users to open them directly, you need to define a custom ProseMirror Mark and update the schema to use it. The following steps demonstrate how to achieve this:
-
Define a custom ProseMirror mark for clickable links. The mark should handle the
onclickevent and customize the rendering of anchor tags:tsexport const clickableLinkMarkSpec: MarkSpec = { attrs: { href: {}, title: { default: null }, target: { default: null }, }, inclusive: false, parseDOM: [ { tag: 'a[href]', getAttrs(dom: any) { return { href: dom.getAttribute('href'), title: dom.getAttribute('title'), target: dom.getAttribute('target'), }; }, }, ], toDOM(mark) { const { href, title, target } = mark.attrs; const attrs: any = { href, onclick: "event.stopPropagation(); window.open(this.href, this.target || '_blank'); return false;", style: 'cursor: pointer; color: #0066cc; text-decoration: underline;', }; if (title) attrs.title = title; if (target) attrs.target = target; return ['a', attrs, 0]; }, }; -
Update the schema's marks collection to use your clickable link mark instead of the default one. This is done using the
OrderedMap.updatemethod from ProseMirror, which returns a new marks collection with the updated mark:tsimport { Schema, schema } from "@progress/kendo-angular-editor"; import { clickableLinkMarkSpec } from "./clickable-link-mark"; const nodes = schema.spec.nodes; const marks = schema.spec.marks.update("link", clickableLinkMarkSpec); export const mySchema = new Schema({ nodes, marks, }); -
Load the custom schema in the Editor component by binding the Editor's
schemainput to your custom schema:tsimport { Component } from '@angular/core'; import { mySchema } from './custom-schema'; @Component({ selector: 'my-app', template: ` <kendo-editor [schema]="mySchema"></kendo-editor> ` }) export class AppComponent { public mySchema = mySchema; }
This approach parses <a> elements with href, title, and target attributes, and renders them as interactive links. Users can open links in a new tab or window directly from the Editor.
The following example demonstrates how to replace the default link mark with a custom clickable link mark in the Editor component.