Mentions

It is now possible for users to send notifications to other users using the Mentions plugin through an @username in the document. Using @username for notifications are incredibly commonplace in social software such as Twitter and Facebook along with social enterprise applications such as IBM’s Connections suite.

Users can communicate every day using @username on social networks and can expect to be able to use them when creating content too.

There are two deployment options. One is with Tiny Cloud, the other is the more traditional Self-hosted route, requiring an additional server-side component to be installed and configured.

Please note that this feature requires a backend integration with a system to handle listing of users, as well as sending the notifications. Refer to this example code for more information. There are two integration points: getting a user list when @abc... is typed, and on submitting the form, sending a notification to the @username in the document.

Mentions cloud setup

Mentions is available for download through Tiny Cloud. Include the mentions parameter in the tinymce.init script. Refer to the Mentions documentation, for more information on setup and configuration.

Try our Mentions plugin demo

  • 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'], function() {

  /*
  ** 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.
  */
  var fakeServer = (function () {
    /* Use TinyMCE's Promise shim */
    var Promise = tinymce.util.Promise;

    /* Some user profile images for our fake server (original source: unsplash) */
    var 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 */
    var userNames = [];
    for (var i = 0; i < 200; i++) {
      userNames.push(faker.name.findName());
    };

    /* This represents a database of users on the server */
    var userDb = {};
    userNames.map(function (fullName) {
      var id = fullName.toLowerCase().replace(/ /g, '');
      return {
        id: id,
        name: fullName,
        fullName: fullName,
        description: faker.name.jobTitle(),
        image: '/unsplash/uifaces-unsplash-portrait-' + images[Math.floor(images.length * Math.random())] + '.jpg'
      };
    }).forEach(function(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 */
    var fetchUsers = function() {
      return new Promise(function(resolve, _reject) {
        /* simulate a server delay */
        setTimeout(function() {
          var users = Object.keys(userDb).map(function(id) {
            return {
              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 */
    var fetchUser = function(id) {
      return new Promise(function(resolve, reject) {
        /* simulate a server delay */
        setTimeout(function() {
          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 */
  var usersRequest = null;
  var userRequest = {};

  var mentions_fetch = function (query, success) {
    /* Fetch your full user list from somewhere */
    if (usersRequest === null) {
      usersRequest = fakeServer.fetchUsers();
    }
    usersRequest.then(function(users) {
      /* `query.term` is the text the user typed after the '@' */
      users = users.filter(function (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);
    });
  };

  var mentions_menu_hover = function (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(function(userDetail) {
      var 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);
    });
  };

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

  var mentions_select = function (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 */
    var 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(function(userDetail) {
      var 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:14px }',

    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 Mentions

Creating an account

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

Get Mentions

The Mentions plugin is included in Tiny Custom Plans.

Further information

For information on: