Skip to content

Commit aba2209

Browse files
committed
docs(Editor): add editor mentions kb
1 parent f1b8fec commit aba2209

File tree

1 file changed

+253
-0
lines changed

1 file changed

+253
-0
lines changed

knowledge-base/editor-mentions.md

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
---
2+
title: Mentions in Editor
3+
description: How to support mentions in the Editor?
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+
I would like to have support for @mentions in the TelerikEditor, 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 Kendo UI for Angular Editor component. To implement the feature, you need to customize and integrate the built-in [ProseMirror Schema](slug:editor-prosemirror-schema-overview) and [ProseMirror Plugins](slug:editor-prosemirror-plugins).
31+
32+
Additionally, for proper dynamic positioning of the mentions list, you need to 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 can be positioned correctly relative to the editor content.
33+
34+
````razor.skip-repl
35+
<TelerikEditor EditMode="EditorEditMode.Div">
36+
</TelerikEditor>
37+
````
38+
39+
Setup steps:
40+
1. Create a directory at the root of your project which will contain your javascript module (e.g. `NpmJs`)
41+
2. In that directory, run
42+
> `npm init`
43+
3. Install a javascript bundler, in this example we'll be using [webpack](https://webpack.js.org), so run
44+
> `npm install webpack webpack-cli --save-dev`
45+
4. Add an `npm build` script that will use webpack to bundle our Javascript file. Modify the `scripts` section of the `package.json` file to add the following build script (note: in this example all of our Javascript files will go into `NpmJs/src`)
46+
````json.skip-repl
47+
"scripts": {
48+
"build": "webpack ./src/index.js --output-path ../wwwroot/js --output-filename index.bundle.js"
49+
},
50+
````
51+
5. Install [proseMirror-mentions](https://github.com/joelewis/prosemirror-mentions) by running
52+
> `npm install prosemirror-mentions`
53+
54+
The following code demonstrates how to integrate the `proseMirror-mentions` plugin in the Editor.
55+
56+
<div class="skip-repl"></div>
57+
58+
````razor Component
59+
@using Microsoft.Extensions.Logging.Abstractions
60+
61+
@implements IDisposable
62+
63+
<TelerikEditor Plugins="pluginsProvider"
64+
Schema="schemaProvider"
65+
EditMode="EditorEditMode.Div">
66+
</TelerikEditor>
67+
68+
@code {
69+
private DotNetObjectReference<Editor>? _dotNetRef;
70+
private List<Mention> Mentions { get; set; } = new List<Mention>()
71+
{
72+
new()
73+
{
74+
Id = "board",
75+
Name = "Jane Simons",
76+
Email = "jane.simons@company.com",
77+
},
78+
new()
79+
{
80+
Id = "engineering",
81+
Name = "Peter Parker",
82+
Email = "peter.parker@company.com"
83+
},
84+
new()
85+
{
86+
Id = "generalManager",
87+
Name = "Liam Turner",
88+
Email = "liam.turner@company.com"
89+
}
90+
};
91+
92+
[Inject]
93+
private IJSRuntime JSRuntime { get; set; }
94+
95+
[Inject]
96+
private IServiceProvider ServiceProvider { get; set; }
97+
98+
protected override async Task OnAfterRenderAsync(bool firstRender)
99+
{
100+
if (firstRender)
101+
{
102+
_dotNetRef = DotNetObjectReference.Create(this);
103+
await JSRuntime.InvokeVoidAsync("initializeMentions", _dotNetRef);
104+
}
105+
}
106+
107+
[JSInvokable]
108+
public async Task<Mention[]> GetMentionSuggestionsAsync(string text)
109+
{
110+
return Mentions.Where(mention => mention.Name.ToLower().Contains(text)).ToArray();
111+
}
112+
113+
[JSInvokable]
114+
public async Task<string> GetMentionSuggestionsHTML(List<Mention> mentions)
115+
{
116+
using var htmlRenderer = new HtmlRenderer(ServiceProvider, NullLoggerFactory.Instance);
117+
var html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
118+
{
119+
var dictionary = new Dictionary<string, object?>
120+
{
121+
{ "Items", mentions }
122+
};
123+
var parameters = ParameterView.FromDictionary(dictionary);
124+
var output = await htmlRenderer.RenderComponentAsync<MentionSuggestionList>(parameters);
125+
return output.ToHtmlString();
126+
});
127+
128+
return html;
129+
}
130+
131+
public void Dispose()
132+
{
133+
_dotNetRef?.Dispose();
134+
}
135+
}
136+
````
137+
````razor MentionSuggestionList
138+
<div class="suggestion-item-list">
139+
@if(Items == null || !Items.Any())
140+
{
141+
<div class="suggestion-item">
142+
No suggestions
143+
</div>
144+
} else
145+
{
146+
@foreach(var item in Items) {
147+
<div class="suggestion-item">
148+
<div class="suggestion-item-content">
149+
<div class="suggestion-text">
150+
<div class="suggestion-name">
151+
@item.Name
152+
</div>
153+
<div class="suggestion-title">
154+
@item.Email
155+
</div>
156+
</div>
157+
</div>
158+
</div>
159+
}
160+
}
161+
</div>
162+
163+
@code {
164+
[Parameter]
165+
public IEnumerable<Mention> Items { get; set; }
166+
}
167+
````
168+
````cs Mention.cs
169+
public class Mention
170+
{
171+
public string Id { get; set; }
172+
public string Name { get; set; }
173+
public string Email { get; set; }
174+
}
175+
````
176+
````js Javascript
177+
import { addMentionNodes, addTagNodes, getMentionsPlugin } from 'prosemirror-mentions';
178+
179+
let _dotnetRef;
180+
window.initializeMentions = (dotnetRef) => {
181+
_dotnetRef = dotnetRef;
182+
}
183+
184+
/**
185+
* IMPORTANT: outer div's "suggestion-item-list" class is mandatory. The plugin uses this class for querying.
186+
* IMPORTANT: inner div's "suggestion-item" class is mandatory too for the same reasons
187+
*/
188+
var getMentionSuggestionsHTML = items => '<div class="suggestion-item-list">' +
189+
items.map(i => '<div class="suggestion-item">' + i.name + '</div>').join('') +
190+
'</div>';
191+
192+
/**
193+
* IMPORTANT: outer div's "suggestion-item-list" class is mandatory. The plugin uses this class for querying.
194+
* IMPORTANT: inner div's "suggestion-item" class is mandatory too for the same reasons
195+
*/
196+
var getTagSuggestionsHTML = items => '<div class="suggestion-item-list">' +
197+
items.map(i => '<div class="suggestion-item">' + i.tag + '</div>').join('') +
198+
'</div>';
199+
200+
let mentionSuggestionsHTML = null;
201+
202+
var mentionPlugin = getMentionsPlugin({
203+
getSuggestions: (type, text, done) => {
204+
setTimeout(async () => {
205+
if (type === 'mention') {
206+
try {
207+
const suggestions = await _dotnetRef.invokeMethodAsync('GetMentionSuggestionsAsync', text);
208+
mentionSuggestionsHTML = await _dotnetRef.invokeMethodAsync('GetMentionSuggestionsHTML', suggestions);
209+
done(suggestions);
210+
} catch (error) {
211+
console.error('Error getting suggestions:', error);
212+
done([]);
213+
}
214+
} else {
215+
// pass dummy tag suggestions
216+
done([{ tag: 'WikiLeaks' }, { tag: 'NetNeutrality' }])
217+
}
218+
}, 0);
219+
},
220+
getSuggestionsHTML: (items, type) => {
221+
if (type === 'mention') {
222+
return mentionSuggestionsHTML;
223+
} else if (type === 'tag') {
224+
return getTagSuggestionsHTML(items)
225+
}
226+
}
227+
});
228+
229+
window.pluginsProvider = (args) => {
230+
const schema = args.getSchema();
231+
232+
return [mentionPlugin, ...args.getPlugins(schema)];
233+
}
234+
235+
window.schemaProvider = (args) => {
236+
const schema = args.getSchema();
237+
const Schema = args.ProseMirror.Schema;
238+
const nodes = addTagNodes(addMentionNodes(schema.spec.nodes));
239+
const mentionsSchema = new Schema({
240+
nodes: nodes,
241+
marks: schema.spec.marks
242+
});
243+
244+
return mentionsSchema;
245+
}
246+
247+
````
248+
249+
## See also
250+
* [Editor Schema](slug:editor-prosemirror-schema-overview)
251+
* [Editor Plugins](slug:editor-prosemirror-plugins)
252+
* [ProseMirror Documentation](https://prosemirror.net/docs/ref)
253+
* [proseMirror-mentions](https://github.com/joelewis/prosemirror-mentions)

0 commit comments

Comments
 (0)