Feishu-Style @ Mention Input
An educational, self-contained demo based on the article about building an @-mention OKR input box on Gitee. Each section shows the exact code pattern, then a live result using that pattern.
1. Initialize an Editable @-Mention Input
Code:
<div id="okrInput" class="mention-editor" contenteditable="true"></div>
<script>
const people = [
{ id: "u01", name: "Ada Lovelace", role: "Engineering" },
{ id: "u02", name: "Grace Hopper", role: "Platform" },
{ id: "u03", name: "Lin Chen", role: "Design" },
{ id: "u04", name: "Maya Patel", role: "Product" }
];
createAtMentionInput(document.getElementById("okrInput"), people, {
pressEnter(editor) {
document.getElementById("submitOutput").textContent =
"Submit: " + serializeEditor(editor);
}
});
</script>
Result:
Try: type @a, use arrow keys, click a name, or press Enter.
Submit output appears here.
2. Represent an @ Mention as a Whole Token with <hr>
Code:
<style>
.at-token {
display: inline-block;
width: auto;
height: auto;
margin: 0 3px;
border: 0;
vertical-align: -0.28em;
background: transparent;
cursor: pointer;
user-select: none;
}
.at-token::before {
content: attr(data-label);
display: inline-flex;
min-height: 1.62em;
padding: 0 0.48em;
border: 1px solid rgba(15, 139, 141, 0.28);
border-radius: 999px;
background: #e8f8f7;
color: #08696b;
font-weight: 700;
}
</style>
<script>
function insertMentionToken(editor, person) {
const token = document.createElement("hr");
token.className = "at-token";
token.dataset.id = person.id;
token.dataset.label = "@" + person.name;
token.dataset.role = person.role;
editor.append(token, document.createTextNode(" "));
}
</script>
Result:
Team owners:
Each inserted name is an
<hr> element styled by ::before.
Token markup appears here.
3. Use Delegated Hover Events for the Popover
Code:
<script>
const popover = document.getElementById("personPopover");
editor.addEventListener("mouseover", (event) => {
const token = event.target.closest("hr.at-token");
if (!token || !editor.contains(token)) return;
popover.innerHTML =
"<strong>" + token.dataset.label + "</strong>" +
"<span>" + token.dataset.role + "</span>";
positionPopover(popover, token);
popover.hidden = false;
});
editor.addEventListener("mouseout", (event) => {
if (event.target.closest("hr.at-token")) {
popover.hidden = true;
}
});
</script>
Result:
Hover these owners:
The mouse listeners are attached once to the editor, not to each token.
4. Strip Rich HTML from Paste and Drop
Code:
<script>
const doStripHtml = function (event) {
const dataInput = event.clipboardData || event.dataTransfer;
const htmlOrigin = dataInput.getData("text/html");
const textOrigin = dataInput.getData("text/plain") || dataInput.getData("text");
if (htmlOrigin) {
event.preventDefault();
insertPlainTextAtCursor(textOrigin);
}
};
editor.addEventListener("paste", doStripHtml);
editor.addEventListener("drop", doStripHtml);
</script>
Result:
Only plain text is inserted.
Drag rich text: Bold OKR priority
HTML inside editor appears here.
5. Intercept Enter to Run a Custom Action
Code:
<script>
editor.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
output.textContent = "Saved: " + serializeEditor(editor);
}
});
</script>
Result:
0
Enter increases the save count instead of adding a new line.
Saved text appears here.