Mentions plugin

This plugin is only available for paid TinyMCE subscriptions.

The mentions plugin will present a list of users when a user types the "@" symbol followed by the beginnings of a username after it. It will then query your server using the mentions_fetch callback.

Interactive example

  • TinyMCE

  • HTML

  • CSS

  • JS

  • Edit on CodePen

<textarea id="mentions">
  <p>Type "<kbd>@</kbd>" followed immediately by one or more characters.</p>
  <p>For example: @a</p>
</textarea>
textarea#mentions {
  height: 350px;
}

div.card,
.tox div.card {
  width: 240px;
  background: white;
  border: 1px solid #ccc;
  border-radius: 3px;
  box-shadow: 0 4px 8px 0 rgba(34, 47, 62, .1);
  padding: 8px;
  font-size: 14px;
  font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
}

div.card::after,
.tox div.card::after {
  content: "";
  clear: both;
  display: table;
}

div.card h1,
.tox div.card h1 {
  font-size: 14px;
  font-weight: bold;
  margin: 0 0 8px;
  padding: 0;
  line-height: normal;
  font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
}

div.card p,
.tox div.card p {
  font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
}

div.card img.avatar,
.tox div.card img.avatar {
  width: 48px;
  height: 48px;
  margin-right: 8px;
  float: left;
}
/* Script to import faker.js for generating random data for demonstration purposes */
tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/faker.min.js']).then(() => {

  /*
  ** This is to simulate requesting information from a server.
  **
  ** It has 2 functions:
  ** fetchUsers() - returns a complete list of users' ids and names.
  ** fetchUser(id) - returns the full information about a single user id.
  **
  ** Both of these functions have a slight delay to simulate a server request.
  */
  const fakeServer = (() => {
    /* Some user profile images for our fake server (original source: unsplash) */
    const images = [
      'Abdullah_Hadley', 'Abriella_Bond', 'Addilynn_Dodge', 'Adolfo_Hess', 'Alejandra_Stallings', 'Alfredo_Schafer', 'Aliah_Pitts', 'Amilia_Luna', 'Andi_Lane', 'Angelina_Winn', 'Arden_Dean', 'Ariyanna_Hicks', 'Asiya_Wolff', 'Brantlee_Adair', 'Carys_Metz', 'Daniela_Dewitt', 'Della_Case', 'Dianna_Smiley', 'Eliana_Stout', 'Elliana_Palacios', 'Fischer_Garland', 'Glen_Rouse', 'Grace_Gross', 'Heath_Atwood', 'Jakoby_Roman', 'Judy_Sewell', 'Kaine_Hudson', 'Kathryn_Mcgee', 'Kayley_Dwyer', 'Korbyn_Colon', 'Lana_Steiner', 'Loren_Spears', 'Lourdes_Browning', 'Makinley_Oneill', 'Mariana_Dickey', 'Miyah_Myles', 'Moira_Baxter', 'Muhammed_Sizemore', 'Natali_Craig', 'Nevaeh_Cates', 'Oscar_Khan', 'Rodrigo_Hawkins', 'Ryu_Duke', 'Tripp_Mckay', 'Vivianna_Kiser', 'Yamilet_Booker', 'Yarely_Barr', 'Zachary_Albright', 'Zahir_Mays', 'Zechariah_Burrell'
    ];

    /* Create an array of 200 random names using faker.js */
    const userNames = [];
    for (let i = 0; i < 200; i++) {
      userNames.push(faker.name.findName());
    }

    /* This represents a database of users on the server */
    const userDb = {};
    userNames.map((fullName) => {
      const id = fullName.toLowerCase().replace(/ /g, '');
      return {
        id: id,
        name: fullName,
        fullName: fullName,
        description: faker.name.jobTitle(),
        image: '../_images/unsplash/uifaces-unsplash-portrait-' + images[Math.floor(images.length * Math.random())] + '.jpg'
      };
    }).forEach((user) => {
      userDb[user.id] = user;
    });

    /* This represents getting the complete list of users from the server with the details required for the mentions "profile" item */
    const fetchUsers = () => new Promise((resolve, _reject) => {
      /* simulate a server delay */
      setTimeout(() => {
        const users = Object.keys(userDb).map((id) => ({
          id: id,
          name: userDb[id].name,
          image: userDb[id].image,
          description: userDb[id].description
        }));
        resolve(users);
      }, 500);
    });

    /* This represents requesting all the details of a single user from the server database */
    const fetchUser = (id) => new Promise((resolve, reject) => {
      /* simulate a server delay */
      setTimeout(() => {
        if (Object.prototype.hasOwnProperty.call(userDb, id)) {
          resolve(userDb[id]);
        }
        reject('unknown user id "' + id + '"');
      }, 300);
    });

    return {
      fetchUsers: fetchUsers,
      fetchUser: fetchUser
    };
  })();

  /* These are "local" caches of the data returned from the fake server */
  let usersRequest = null;
  const userRequest = {};

  const mentions_fetch = (query, success) => {
    /* Fetch your full user list from somewhere */
    if (usersRequest === null) {
      usersRequest = fakeServer.fetchUsers();
    }
    usersRequest.then((users) => {
      /* `query.term` is the text the user typed after the '@' */
      users = users.filter((user) => user.name.indexOf(query.term.toLowerCase()) !== -1);

      users = users.slice(0, 10);

      /* Where the user object must contain the properties `id` and `name`
        but you could additionally include anything else you deem useful. */
      success(users);
    });
  };

  const mentions_menu_hover = (userInfo, success) => {
    /* Request more information about the user from the server and cache it locally */
    if (!userRequest[userInfo.id]) {
      userRequest[userInfo.id] = fakeServer.fetchUser(userInfo.id);
    }
    userRequest[userInfo.id].then((userDetail) => {
      const div = document.createElement('div');

      div.innerHTML = (
        '<div class="card">' +
          '<img class="avatar" src="' + userDetail.image + '">' +
          '<h1>' + userDetail.fullName + '</h1>' +
          '<p>' + userDetail.description + '</p>' +
        '</div>'
      );

      success(div);
    });
  };

  const mentions_menu_complete = (editor, userInfo) => {
    const span = editor.getDoc().createElement('span');
    span.className = 'mymention';
    span.setAttribute('data-mention-id', userInfo.id);
    span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name));
    return span;
  };

  const mentions_select = (mention, success) => {
    /* `mention` is the element we previously created with `mentions_menu_complete`
      in this case we have chosen to store the id as an attribute */
    const id = mention.getAttribute('data-mention-id');
    /* Request more information about the user from the server and cache it locally */
    if (!userRequest[id]) {
      userRequest[id] = fakeServer.fetchUser(id);
    }
    userRequest[id].then((userDetail) => {
      const div = document.createElement('div');
      div.innerHTML = (
        '<div class="card">' +
        '<img class="avatar" src="' + userDetail.image + '">' +
        '<h1>' + userDetail.fullName + '</h1>' +
        '<p>' + userDetail.description + '</p>' +
        '</div>'
      );
      success(div);
    });
  };

  tinymce.init({
    selector: 'textarea#mentions',
    plugins: 'mentions',
    content_style: '.mymention{ color: gray; }' +
      'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }',

    mentions_selector: '.mymention',
    mentions_fetch: mentions_fetch,
    mentions_menu_hover: mentions_menu_hover,
    mentions_menu_complete: mentions_menu_complete,
    mentions_select: mentions_select,
    mentions_item_type: 'profile'
  });
});

Getting started with TinyMCE Mentions

Creating a Tiny Cloud account

Try the Mentions plugin and the Tiny Cloud with a free Tiny Account. New accounts receive a 14-day trial of the Tiny premium plugins, skins, and icon packs; with no credit card information or commitment required.

Get TinyMCE Mentions

The Mentions plugin is included in Tiny Custom Plans.

Options

These configuration options affect the execution of the mentions plugin. The main option that needs to be implemented is the mentions_fetch callback.

mentions_fetch

This option lets you request a list of users from your server that match a search query. The callback gets passed two parameters: one is the search query object, the other is the success callback to execute with the results. The query object has a term property that contains what the user has typed after the "@" sign. The success call should contain an array of users. For information on the user properties to pass the success callback for the available mentions item types (mentions_item_type), see: User properties.

Type: Function

Example: Using mentions_fetch

let usersRequest = null;

tinymce.init({
  selector: 'textarea',
  plugins: 'mentions',
  mentions_fetch: (query, success) => {
    // Fetch your full user list from the server and cache locally
    if (usersRequest === null) {
      usersRequest = fetch('/users');
    }
    usersRequest.then((users) => {
      // query.term is the text the user typed after the '@'
      users = users.filter((user) => {
        return user.name.indexOf(query.term.toLowerCase()) !== -1;
      });

      users = users.slice(0, 10);

      // Where the user object must contain the properties `id` and `name`
      // but you could additionally include anything else you deem useful.
      success(users);
    });
  }
});

The success callback can be passed an optional array of extra items. When clicked, the menu reloads and passes additional query parameters to the fetch function. The extra items can be used to search with different queries or show additional results, such as a full text search (which is slower to fetch). Each extra item should contain:

  • A "text" property for the content to be displayed in the menu.

  • A "meta" property for that will be passed using the fetch query parameter.

Example with extras

tinymce.init({
  selector: 'textarea',
  plugins: 'mentions',
  mentions_fetch: (query, success) => {
    // query.term is the text the user typed after the '@'
    let url = '/users?query=' + query.term;
    const isFullTextSearch = query.meta && query.meta.fullTextSearch;
    if (isFullTextSearch) {
      url += '&full=true'
    }

    // Extras are shown at the end of the list and will reload the menu
    // by passing the meta to the fetch function
    const extras = isFullTextSearch ? [ ] : [
      {
        text: 'Full user search...',
        meta: { fullTextSearch: true }
      }
    ];

    fetch(url).then((users) => {
      // Where the user object must contain the properties `id` and `name`
      // but you could additionally include anything else you deem useful.
      success(users, extras);
    });
  }
});

mentions_item_type

This option sets which user interface item type to use when displaying the list of users.

  • The name item will only display the user’s name.

  • The profile item will display the user’s name and can display an optional image and description.

For information on the properties required for the user object provided to mentions_fetch, see: User properties.

Type: String

Default value: 'name'

Possible values: 'name', 'profile'

Example: Using mentions_item_type

tinymce.init({
  selector: 'textarea',
  plugins: 'mentions',
  mentions_item_type: 'name'
});

User properties

The following table describes the properties available for user objects provided to the mentions_fetch callback. Properties may be required, optional, or not available; depending on the mentions_item_type and mentions_select options.

Name Value name profile Description

id

string

required

required

Used to identify the user mention in different callbacks

name

string

required

required

Name to display and highlight matches

image

string

not available

optional

Image source for user avatar

description

string

not available

optional

Description to display

mentions_min_chars

This option specifies the number of characters a user needs to type after the "@" symbol before the list of users will be displayed.

Type: Number

Default value: 1

Example: Using mentions_min_chars

tinymce.init({
  selector: 'textarea',
  plugins: 'mentions',
  mentions_min_chars: 1
});

mentions_menu_complete

This option overrides the default logic for inserting the mention into the editor. The callback should return an element created using the editor’s document.

Type: Function

Example: Using mentions_menu_complete

tinymce.init({
  selector: 'textarea',
  plugins: 'mentions',
  mentions_selector: 'span.mymention',
  mentions_menu_complete: (editor, userInfo) => {
    const span = editor.getDoc().createElement('span');
    span.className = 'mymention';
    // store the user id in the mention so it can be identified later
    span.setAttribute('data-mention-id', userInfo.id);
    span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name));
    return span;
  }
});

mentions_menu_hover

This option enables you to provide an element to present next to the menu item being hovered. This lets you do custom UIs for presenting user information.

Type: Function

Example: Using mentions_menu_hover

const userRequest = {};

tinymce.init({
  selector: 'textarea',
  plugins: 'mentions',
  mentions_menu_hover: (userInfo, success) => {
    // request more information about the user from the server and cache it locally
    if (!userRequest[userInfo.id]) {
      userRequest[userInfo.id] = fetch('/user?id=' + userInfo.id);
    }
    userRequest[userInfo.id].then((userDetail) => {
      const div = document.createElement('div');

      div.innerHTML = (
      '<div>' +
        '<h1>' + userDetail.fullName + '</h1>' +
        '<img src="' + userDetail.image + '" ' +
            'style="width: 50px; height: 50px; float: left;"/>' +
        '<p>' + userDetail.description + '</p>' +
      '</div>'
      );

      success(div);
    });
  }
});

mentions_menu_hover with predefined templates

If mentions_menu_hover is resolved with an object specifying the type and user details, a predefined hover card template will be used. To use the predefined template, set type to 'profile'. For details on the user properties required for the profile template, see: User properties.

Example: Using the 'profile' template with mentions_menu_hover
const userRequest = {};
tinymce.init({
  selector: 'textarea',
  plugins: 'mentions',
  mentions_menu_hover: (userInfo, success) => {
    // request more information about the user from the server and cache it locally
    if (!userRequest[userInfo.id]) {
      userRequest[userInfo.id] = fetch('/user?id=' + userInfo.id);
    }
    userRequest[userInfo.id].then((userDetail) => {
      success({ type: 'profile', user: userDetail });
    });
  }
});

mentions_selector

This option enables you to provide a custom CSS selector that should match the element created using mentions_menu_complete. This enables the plugin to find existing mentions. The callback takes two parameters: the editor instance and the userInfo object.

Type: Function

Example: Using mentions_selector

tinymce.init({
  selector: 'textarea',
  plugins: 'mentions',
  mentions_selector: 'span.mymention',
  mentions_menu_complete: (editor, userInfo) => {
    const span = editor.getDoc().createElement('span');
    span.className = 'mymention';
    span.setAttribute('data-mention-id', userInfo.id);
    span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name));
    return span;
  }
});

mentions_select

This option enables a hover card to be presented when a user hovers over a mention in TinyMCE. This could include details about the user. A custom hover card HTML element can be provided or a predefined template can be specified.

Type: Function

Example: Using mentions_select

const userRequest = {};

tinymce.init({
  selector: 'textarea',
  plugins: 'mentions',
  mentions_selector: 'span.mymention',
  mentions_menu_complete: (editor, userInfo) => {
    const span = editor.getDoc().createElement('span');
    span.className = 'mymention';
    span.setAttribute('data-mention-id', userInfo.id);
    span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name));
    return span;
  },
  mentions_select: (mention, success) => {
    // `mention` is the element we previously created with `mentions_menu_complete`
    // in this case we have chosen to store the id as an attribute
    const id = mention.getAttribute('data-mention-id');
    // request more information about the user from the server and cache locally
    if (!userRequest[id]) {
      userRequest[id] = fetch('/user?id=' + id);
    }
    userRequest[id].then((userDetail) => {
      const div = document.createElement('div');
      div.innerHTML = (
        '<div>' +
        '<h1>' + userDetail.fullName + '</h1>' +
        '<img src="' + userDetail.image + '" ' +
            'style="width: 50px; height: 50px; float: left;"/>' +
        '<p>' + userDetail.description + '</p>' +
        '</div>'
      );
      success(div);
    });
  }
});

mentions_select with predefined templates

If mentions_select is resolved with an object specifying the type and user details, a predefined hover card template will be used. To use the predefined template, set type to 'profile'. For details on the user properties required for the profile template, see: User properties.

Example: Using the 'profile' template with mentions_select
const userRequest = {};
tinymce.init({
  selector: 'textarea',
  plugins: 'mentions',
  mentions_selector: 'span.mymention',
  mentions_menu_complete: (editor, userInfo) => {
    const span = editor.getDoc().createElement('span');
    span.className = 'mymention';
    span.setAttribute('data-mention-id', userInfo.id);
    span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name));
    return span;
  },
  mentions_select: (mention, success) => {
    // `mention` is the element we previously created with `mentions_menu_complete`
    // in this case we have chosen to store the id as an attribute
    const id = mention.getAttribute('data-mention-id');
    // request more information about the user from the server and cache locally
    if (!userRequest[id]) {
      userRequest[id] = fetch('/user?id=' + id);
    }
    userRequest[id].then((userDetail) => {
      success({ type: 'profile', user: userDetail });
    });
  }
});