Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
290 changes: 290 additions & 0 deletions knowledge-base/editor-mentions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
---
title: Mentions in Editor
description: Learn how to add support for mentions in the Telerik Editor component for Blazor.
type: how-to
page_title: Mentions in Editor
slug: editor-kb-mentions
position:
tags: telerik, blazor, editor, mentions
ticketid: 1545505
res_type: kb
---

## Environment

<table>
<tbody>
<tr>
<td>Product</td>
<td>Editor for Blazor</td>
</tr>
</tbody>
</table>

## Description

How to enable or implement support for `@mentions` in the TelerikEditor for Blazor, similar to GitHub, Facebook, etc.?

## Solution

You can use the [proseMirror-mentions](https://github.com/joelewis/prosemirror-mentions) plugin to provide a `@mentions` and `#hashtags` functionality for the Telerik Blazor Editor component. To implement the feature, customize the built-in [ProseMirror schema](slug:editor-prosemirror-schema-overview) and [integrate a ProseMirror plugin](slug:editor-prosemirror-plugins).

For dynamic positioning of the mentions list, set the [`EditMode`](slug:Telerik.Blazor.Components.TelerikEditor#telerik_blazor_components_telerikeditor_editmode) property of the Editor to `EditorEditorMode.Div`. This ensures that the mentions dropdown position is correct relative to the Editor content.

````RAZOR.skip-repl
<TelerikEditor EditMode="EditorEditMode.Div" />
````

### Setting up WebPack and Installing proseMirror-mentions

1. Create a directory at the root of your project which will contain your JavaScript module (e.g. `npmjs`). All of the next steps will be done inside that directory
2. Setup the Javascript project by running:
````SH.skip-repl
npm init -y
````
> This command creates a `package.json` file containing the project's configuration. The `-y` flag accepts all defaults for simplicity. In a real world application, consider running `npm init` without the flag to configure settings interactively.
3. Install a JavaScript bundler. In this example we will use [webpack](https://webpack.js.org), so run:
````SH.skip-repl
npm install webpack webpack-cli --save-dev
````
4. Configure the build script in the `scripts` section of `package.json`:
````JSON.skip-repl
"scripts": {
"build": "webpack ./src/index.js --output-path ../wwwroot/js --output-filename index.bundle.js"
},
````
> In this example all of our Javascript files will go into `npmjs/src`
5. Update the module type in `package.json`:
````JSON.skip-repl
"type": module"
````
This enables ES6 `import`/`export` syntax instead of the CommonJS require statements which will be useful later on.
6. Install [proseMirror-mentions](https://github.com/joelewis/prosemirror-mentions) by running:
````SH.skip-repl
npm install prosemirror-mentions
````
7. Build the JavaScript bundle by running:
````SH.skip-repl
npm run build
````
This creates the `index.bundle.js` file in your `wwwroot/js` directory.

### Include the JavaScript Bundle

After building the JavaScript bundle, you need to include it in your Blazor application. You can do this at either the global or component level:

**Global Level (App.razor):**
<div class="skip-repl"></div>

````razor App.razor
<!DOCTYPE html>
<html>
<head>
<!-- other head content -->
</head>
<body>
<!-- body content -->

<script src="js/index.bundle.js"></script>
</body>
</html>
````

**Component Level (Component.razor):**
If you prefer to load the script only when the component is used, include it in your component with the `defer` attribute:

<div class="skip-repl"></div>

````razor Component.razor
<!-- component content -->

<script src="js/index.bundle.js" defer></script>
````

> Note: The script tag does not need `type="module"` since webpack bundles all ES6 modules into a single compatible file.

### Integrate the Mentions Plugin

The following code demonstrates how to integrate the `proseMirror-mentions` plugin in the Editor.

<div class="skip-repl"></div>

````razor Component.razor
@using Microsoft.Extensions.Logging.Abstractions

@implements IDisposable

@inject IJSRuntime JSRuntime
@inject IServiceProvider ServiceProvider

<TelerikEditor Plugins="pluginsProvider"
Schema="schemaProvider"
EditMode="EditorEditMode.Div">
</TelerikEditor>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add theme-independent styles for the popup using theme variables.

@code {
// Replace Component with your actual component type
private DotNetObjectReference<Component>? dotNetRef;
private List<Mention> Mentions { get; set; } = new List<Mention>()
{
new()
{
Id = "board",
Name = "Jane Simons",
Email = "jane.simons@company.com",
},
new()
{
Id = "engineering",
Name = "Peter Parker",
Email = "peter.parker@company.com"
},
new()
{
Id = "generalManager",
Name = "Liam Turner",
Email = "liam.turner@company.com"
}
};

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
dotNetRef = DotNetObjectReference.Create(this);
await JSRuntime.InvokeVoidAsync("initializeMentions", dotNetRef);
}
}

[JSInvokable]
public async Task<Mention[]> GetMentionSuggestionsAsync(string text)
{
return Mentions.Where(mention => mention.Name.ToLower().Contains(text)).ToArray();
}

[JSInvokable]
public async Task<string> GetMentionSuggestionsHTML(List<Mention> mentions)
{
using var htmlRenderer = new HtmlRenderer(ServiceProvider, NullLoggerFactory.Instance);
var html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
var dictionary = new Dictionary<string, object?>
{
{ "Items", mentions }
};
var parameters = ParameterView.FromDictionary(dictionary);
var output = await htmlRenderer.RenderComponentAsync<MentionSuggestionList>(parameters);
return output.ToHtmlString();
});

return html;
}

public void Dispose()
{
dotNetRef?.Dispose();
}
}
````
````razor MentionSuggestionList.razor
@*
IMPORTANT: outer div's "suggestion-item-list" class is mandatory. The plugin uses this class for querying.
IMPORTANT: inner div's "suggestion-item" class is mandatory too for the same reasons
*@

<div class="suggestion-item-list">
@if (Items == null || Items.Count() == 0)
{
<div class="suggestion-item">
No suggestions
</div>
} else
{
@foreach (Mention item in Items) {
<div class="suggestion-item">
<div class="suggestion-item-content">
<div class="suggestion-text">
<div class="suggestion-name">
@item.Name
</div>
<div class="suggestion-title">
@item.Email
</div>
</div>
</div>
</div>
}
}
</div>

@code {
[Parameter]
public IEnumerable<Mention> Items { get; set; }
}
````
````cs Mention.cs
public class Mention
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
````
````js npmjs/src/index.js
import { addMentionNodes, addTagNodes, getMentionsPlugin } from 'prosemirror-mentions';

let _dotnetRef;
window.initializeMentions = (dotnetRef) => {
_dotnetRef = dotnetRef;
}

let mentionSuggestionsHTML = null;

var mentionPlugin = getMentionsPlugin({
getSuggestions: (type, text, done) => {
setTimeout(async () => {
if (type === 'mention') {
try {
const suggestions = await _dotnetRef.invokeMethodAsync('GetMentionSuggestionsAsync', text);
mentionSuggestionsHTML = await _dotnetRef.invokeMethodAsync('GetMentionSuggestionsHTML', suggestions);
done(suggestions);
} catch (error) {
console.error('Error getting suggestions:', error);
done([]);
}
}
}, 0);
},
getSuggestionsHTML: (items, type) => {
if (type === 'mention') {
return mentionSuggestionsHTML;
}
}
});

window.pluginsProvider = (args) => {
const schema = args.getSchema();

return [mentionPlugin, ...args.getPlugins(schema)];
}

window.schemaProvider = (args) => {
const schema = args.getSchema();
const Schema = args.ProseMirror.Schema;
const nodes = addMentionNodes(schema.spec.nodes);
const mentionsSchema = new Schema({
nodes: nodes,
marks: schema.spec.marks
});

return mentionsSchema;
}

````

## See Also

* [Editor Schema](slug:editor-prosemirror-schema-overview)
* [Editor Plugins](slug:editor-prosemirror-plugins)
* [ProseMirror Documentation](https://prosemirror.net/docs/ref)
* [proseMirror-mentions](https://github.com/joelewis/prosemirror-mentions)