I needed to create a chat application for a NodeJS project, one of the main elements is a form input where users can post their initial messages. I was having issues with adding @mentions into a summernote lite text area - turned out to be a lot of work so I thought I'd post my code here in case anyone else needs a leg up.. It's all part of my little attempts to put back a little of what I've taken out from this excellent forum
This isn't really intended to be a tutorial but I'll briefly explain what the code does:
- Takes live data from an internal (JSON Array) API feed - would work on any public API data format for this one is
[{"uid":73382,"firstname":"Joe","surname":"Bloggs","image":"73382.jpg"}
- Uses Summernote hints to facilitate an @mentions type search of users in the API above
- Uses Fuse.js to provide fuzzy search, firstnames and surnames can be weighted in the search
- Includes preloaded avatars in the selection list, its very lightweight
- Includes a nifty shimmering placeholder whilst images load
- fallback avatar for when images don't load
- Changes the stored image filename on-the-fly - I have the users avatar stored in three different versions on the server, but only one filename listed in the db (DUH) so I had to change the listed filename from what is in the db
12345.jpg
to the one we plan to use that is stored in the file system12345-avatar100.jpg
- Uses a hidden txt field to send an array of selected @mentions UIDs during submit for backend processing
- Real-time preview 'Tagged Users' in a list below the textarea - this also includes the users avatar.
Full code for it is here:
<!-- Wappler include head-page="layouts/main" bootstrap5="local" is="dmx-app" id="summernoteMentionApp" appConnect="local" fontawesome_6="local" components="{dmxSummernote:{}}" jquery="cdn" -->
<meta name="ac:route" content="/test/test-basic-summernote">
<!-- Summernote Editor Section -->
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-lg">
<div class="card-header bg-primary-subtle text-primary-emphasis">
<h3 class="mb-0"><i class="fa-solid fa-comment-dots"></i> Advanced Summernote Editor with @Mentions</h3>
</div>
<div class="card-body">
<textarea id="summernote" name="summernote" dmx-bind:config="editorConfig" is="dmx-summernote"></textarea>
<button id="btnSubmit" class="btn btn-primary mt-3">Add message</button>
<input type="hidden" id="mentionUIDs" name="mentionUIDs" value="" />
<!-- Tagged Users Preview -->
<div id="mentionPreview" class="mt-4">
<h5 class="text-primary">Tagged Users:</h5>
<ul class="list-group"></ul>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.shimmer-avatar {
position: relative;
width: 24px;
height: 24px;
border-radius: 50%;
overflow: hidden;
background: #f0f0f0;
}
.shimmer-avatar::before {
content: '';
position: absolute;
top: 0;
left: -40px;
width: 40px;
height: 100%;
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.6), transparent);
animation: shimmer 1.2s infinite;
}
@keyframes shimmer {
100% {
transform: translateX(100px);
}
}
</style>
<!-- Load Fuse.js for fuzzy search -->
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2"></script>
<script>
function setupEditorConfig() {
var editorConfig = {
height: 200,
placeholder: "Type @ to mention someone in the area",
hint: {
match: /\B@([\w.-]+)$/,
search: function (keyword, callback) {
$.ajax({
url: 'https://your-website.com/api/chat/FrontEndUsers',
type: 'GET',
dataType: 'json',
success: function (data) {
const fuse = new Fuse(data.titles, {
keys: [
{ name: 'firstname', weight: 0.6 },
{ name: 'surname', weight: 0.4 }
],
threshold: 0.3,
ignoreLocation: true,
minMatchCharLength: 1
});
const results = fuse.search(keyword);
const filtered = results.map(result => {
const item = result.item;
const preloadImg = new Image();
preloadImg.src = 'https://your-website.com/uploads/avatar/' + item.image;
return {
firstname: item.firstname,
surname: item.surname,
uid: item.uid,
image: item.image
};
});
callback(filtered);
},
error: function () {
callback([]);
}
});
},
template: function (item) {
const finalImage = item.image.replace(/(\.[^.]+)$/, '-avatar100$1');
return `
<div style="display:flex; align-items:center;">
<div class="shimmer-avatar">
<img src="https://your-website.com/uploads/avatar/${finalImage}"
style="width:24px; height:24px; border-radius:50%;"
onload="this.parentNode.classList.remove('shimmer-avatar');"
onerror="this.onerror=null; this.src='https://your-website.com/uploads/avatar/default.png'; this.parentNode.classList.remove('shimmer-avatar');"
alt="avatar" />
</div>
<span style="margin-left:8px;">${item.firstname} ${item.surname}</span>
</div>
`;
},
content: function (item) {
return $('<a href="/profile/' + item.uid + '" data-uid="' + item.uid + '" data-image="' + item.image + '">@' + item.firstname + ' ' + item.surname + '</a>')[0];
}
},
toolbar: [
['style', ['style', 'cleaner']],
['font', ['bold', 'underline', 'clear']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture', 'video']],
['view', ['fullscreen', 'help']]
]
};
dmx.global.set('editorConfig', editorConfig);
// Attach live update for preview
$('#summernote').on('summernote.change', function () {
updateMentionPreview();
});
}
function updateMentionPreview() {
const content = $('#summernote').summernote('code');
const tempDiv = $('<div>').html(content);
const mentioned = [];
tempDiv.find('a[data-uid]').each(function () {
const uid = $(this).attr('data-uid');
const name = $(this).text().replace(/^@/, '').trim();
const image = $(this).attr('data-image');
mentioned.push({ uid, name, image });
});
const previewList = $('#mentionPreview ul');
previewList.empty();
if (mentioned.length === 0) {
previewList.append('<li class="list-group-item text-muted">No users tagged yet.</li>');
return;
}
mentioned.forEach(item => {
const safeImage = item.image
? item.image.replace(/(\.[^.]+)$/, '-avatar100$1')
: 'default.png';
previewList.append(`
<li class="list-group-item d-flex align-items-center">
<img src="https://your-website.com/secureuploads/User/avatar/${safeImage}"
alt="avatar" class="me-2 rounded-circle"
style="width:24px; height:24px;">
<strong>${item.name}</strong>
<small class="text-muted ms-2">(UID: ${item.uid})</small>
</li>
`);
});
}
$('#btnSubmit').on('click', function () {
const content = $('#summernote').summernote('code');
const tempDiv = $('<div>').html(content);
const uidList = [];
tempDiv.find('a[data-uid]').each(function () {
uidList.push($(this).attr('data-uid'));
});
$('#mentionUIDs').val(JSON.stringify(uidList));
});
</script>
Notes Doesn't like to work with jquery slim hence the 'full fat' version here jquery="cdn"
Its reasonably difficult to debug as some of the errors are completely silent, use your AI of choice to help you - it'll do a grand job. Wapplers internal AI is a great place to start - it's getting really good.
The search allows for seperate weighting for each search term - A threshold of 0.0
requires a perfect match (of both letters and location), a threshold of 1.0
would match anything. Theres more info on the fuzzy search here, Its great for small to reasonably large datasets.:
Hope this helps someone out there..!