Skip to content

Commit 11015c7

Browse files
georgedautovdimodi
andcommitted
docs(Editor): add editor mentions kb
Co-authored-by: Dimo Dimov <961014+dimodi@users.noreply.github.com>
1 parent f1b8fec commit 11015c7

File tree

1 file changed

+290
-0
lines changed

1 file changed

+290
-0
lines changed

knowledge-base/editor-mentions.md

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
---
2+
title: Mentions in Editor
3+
description: Learn how to add support for mentions in the Telerik Editor component for Blazor.
4+
type: how-to
5+
page_title: Mentions in Editor
6+
slug: editor-kb-mentions
7+
position:
8+
tags: telerik, blazor, editor, mentions
9+
ticketid: 1545505
10+
res_type: kb
11+
---
12+
13+
## Environment
14+
15+
<table>
16+
<tbody>
17+
<tr>
18+
<td>Product</td>
19+
<td>Editor for Blazor</td>
20+
</tr>
21+
</tbody>
22+
</table>
23+
24+
## Description
25+
26+
How to enable or implement support for `@mentions` in the TelerikEditor for Blazor, similar to GitHub, Facebook, etc.?
27+
28+
## Solution
29+
30+
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).
31+
32+
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.
33+
34+
````RAZOR.skip-repl
35+
<TelerikEditor EditMode="EditorEditMode.Div" />
36+
````
37+
38+
### Setting up WebPack and Installing proseMirror-mentions
39+
40+
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
41+
2. Setup the Javascript project by running:
42+
````SH.skip-repl
43+
npm init -y
44+
````
45+
> 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.
46+
3. Install a JavaScript bundler. In this example we will use [webpack](https://webpack.js.org), so run:
47+
````SH.skip-repl
48+
npm install webpack webpack-cli --save-dev
49+
````
50+
4. Configure the build script in the `scripts` section of `package.json`:
51+
````JSON.skip-repl
52+
"scripts": {
53+
"build": "webpack ./src/index.js --output-path ../wwwroot/js --output-filename index.bundle.js"
54+
},
55+
````
56+
> In this example all of our Javascript files will go into `npmjs/src`
57+
5. Update the module type in `package.json`:
58+
````JSON.skip-repl
59+
"type": module"
60+
````
61+
This enables ES6 `import`/`export` syntax instead of the CommonJS require statements which will be useful later on.
62+
6. Install [proseMirror-mentions](https://github.com/joelewis/prosemirror-mentions) by running:
63+
````SH.skip-repl
64+
npm install prosemirror-mentions
65+
````
66+
7. Build the JavaScript bundle by running:
67+
````SH.skip-repl
68+
npm run build
69+
````
70+
This creates the `index.bundle.js` file in your `wwwroot/js` directory.
71+
72+
### Include the JavaScript Bundle
73+
74+
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:
75+
76+
**Global Level (App.razor):**
77+
<div class="skip-repl"></div>
78+
79+
````razor App.razor
80+
<!DOCTYPE html>
81+
<html>
82+
<head>
83+
<!-- other head content -->
84+
</head>
85+
<body>
86+
<!-- body content -->
87+
88+
<script src="js/index.bundle.js"></script>
89+
</body>
90+
</html>
91+
````
92+
93+
**Component Level (Component.razor):**
94+
If you prefer to load the script only when the component is used, include it in your component with the `defer` attribute:
95+
96+
<div class="skip-repl"></div>
97+
98+
````razor Component.razor
99+
<!-- component content -->
100+
101+
<script src="js/index.bundle.js" defer></script>
102+
````
103+
104+
> Note: The script tag does not need `type="module"` since webpack bundles all ES6 modules into a single compatible file.
105+
106+
### Integrate the Mentions Plugin
107+
108+
The following code demonstrates how to integrate the `proseMirror-mentions` plugin in the Editor.
109+
110+
<div class="skip-repl"></div>
111+
112+
````razor Component.razor
113+
@using Microsoft.Extensions.Logging.Abstractions
114+
115+
@implements IDisposable
116+
117+
@inject IJSRuntime JSRuntime
118+
@inject IServiceProvider ServiceProvider
119+
120+
<TelerikEditor Plugins="pluginsProvider"
121+
Schema="schemaProvider"
122+
EditMode="EditorEditMode.Div">
123+
</TelerikEditor>
124+
125+
@code {
126+
// Replace Component with your actual component type
127+
private DotNetObjectReference<Component>? dotNetRef;
128+
private List<Mention> Mentions { get; set; } = new List<Mention>()
129+
{
130+
new()
131+
{
132+
Id = "board",
133+
Name = "Jane Simons",
134+
Email = "jane.simons@company.com",
135+
},
136+
new()
137+
{
138+
Id = "engineering",
139+
Name = "Peter Parker",
140+
Email = "peter.parker@company.com"
141+
},
142+
new()
143+
{
144+
Id = "generalManager",
145+
Name = "Liam Turner",
146+
Email = "liam.turner@company.com"
147+
}
148+
};
149+
150+
protected override async Task OnAfterRenderAsync(bool firstRender)
151+
{
152+
if (firstRender)
153+
{
154+
dotNetRef = DotNetObjectReference.Create(this);
155+
await JSRuntime.InvokeVoidAsync("initializeMentions", dotNetRef);
156+
}
157+
}
158+
159+
[JSInvokable]
160+
public async Task<Mention[]> GetMentionSuggestionsAsync(string text)
161+
{
162+
return Mentions.Where(mention => mention.Name.ToLower().Contains(text)).ToArray();
163+
}
164+
165+
[JSInvokable]
166+
public async Task<string> GetMentionSuggestionsHTML(List<Mention> mentions)
167+
{
168+
using var htmlRenderer = new HtmlRenderer(ServiceProvider, NullLoggerFactory.Instance);
169+
var html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
170+
{
171+
var dictionary = new Dictionary<string, object?>
172+
{
173+
{ "Items", mentions }
174+
};
175+
var parameters = ParameterView.FromDictionary(dictionary);
176+
var output = await htmlRenderer.RenderComponentAsync<MentionSuggestionList>(parameters);
177+
return output.ToHtmlString();
178+
});
179+
180+
return html;
181+
}
182+
183+
public void Dispose()
184+
{
185+
dotNetRef?.Dispose();
186+
}
187+
}
188+
````
189+
````razor MentionSuggestionList.razor
190+
@*
191+
IMPORTANT: outer div's "suggestion-item-list" class is mandatory. The plugin uses this class for querying.
192+
IMPORTANT: inner div's "suggestion-item" class is mandatory too for the same reasons
193+
*@
194+
195+
<div class="suggestion-item-list">
196+
@if (Items == null || Items.Count() == 0)
197+
{
198+
<div class="suggestion-item">
199+
No suggestions
200+
</div>
201+
} else
202+
{
203+
@foreach (Mention item in Items) {
204+
<div class="suggestion-item">
205+
<div class="suggestion-item-content">
206+
<div class="suggestion-text">
207+
<div class="suggestion-name">
208+
@item.Name
209+
</div>
210+
<div class="suggestion-title">
211+
@item.Email
212+
</div>
213+
</div>
214+
</div>
215+
</div>
216+
}
217+
}
218+
</div>
219+
220+
@code {
221+
[Parameter]
222+
public IEnumerable<Mention> Items { get; set; }
223+
}
224+
````
225+
````cs Mention.cs
226+
public class Mention
227+
{
228+
public string Id { get; set; } = string.Empty;
229+
public string Name { get; set; } = string.Empty;
230+
public string Email { get; set; } = string.Empty;
231+
}
232+
````
233+
````js npmjs/src/index.js
234+
import { addMentionNodes, addTagNodes, getMentionsPlugin } from 'prosemirror-mentions';
235+
236+
let _dotnetRef;
237+
window.initializeMentions = (dotnetRef) => {
238+
_dotnetRef = dotnetRef;
239+
}
240+
241+
let mentionSuggestionsHTML = null;
242+
243+
var mentionPlugin = getMentionsPlugin({
244+
getSuggestions: (type, text, done) => {
245+
setTimeout(async () => {
246+
if (type === 'mention') {
247+
try {
248+
const suggestions = await _dotnetRef.invokeMethodAsync('GetMentionSuggestionsAsync', text);
249+
mentionSuggestionsHTML = await _dotnetRef.invokeMethodAsync('GetMentionSuggestionsHTML', suggestions);
250+
done(suggestions);
251+
} catch (error) {
252+
console.error('Error getting suggestions:', error);
253+
done([]);
254+
}
255+
}
256+
}, 0);
257+
},
258+
getSuggestionsHTML: (items, type) => {
259+
if (type === 'mention') {
260+
return mentionSuggestionsHTML;
261+
}
262+
}
263+
});
264+
265+
window.pluginsProvider = (args) => {
266+
const schema = args.getSchema();
267+
268+
return [mentionPlugin, ...args.getPlugins(schema)];
269+
}
270+
271+
window.schemaProvider = (args) => {
272+
const schema = args.getSchema();
273+
const Schema = args.ProseMirror.Schema;
274+
const nodes = addMentionNodes(schema.spec.nodes);
275+
const mentionsSchema = new Schema({
276+
nodes: nodes,
277+
marks: schema.spec.marks
278+
});
279+
280+
return mentionsSchema;
281+
}
282+
283+
````
284+
285+
## See Also
286+
287+
* [Editor Schema](slug:editor-prosemirror-schema-overview)
288+
* [Editor Plugins](slug:editor-prosemirror-plugins)
289+
* [ProseMirror Documentation](https://prosemirror.net/docs/ref)
290+
* [proseMirror-mentions](https://github.com/joelewis/prosemirror-mentions)

0 commit comments

Comments
 (0)