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.