Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mention: Add text attribute #1322

Merged
merged 4 commits into from
Jun 14, 2021
Merged

Mention: Add text attribute #1322

merged 4 commits into from
Jun 14, 2021

Conversation

tomhrtly
Copy link
Contributor

This PR allows you to change the text that is to be rendered in the DOM when a selection is made from the suggestion.

This is so that you may want the data-mention attribute to use a user's ID but the user's name would be displayed instead of the ID e.g. "@johnsmith" instead of "@1".

@nadar
Copy link

nadar commented May 22, 2021

👍 looking for such an option

@nadar
Copy link

nadar commented May 22, 2021

@tomhrtly Do you know if there is any option to override the current mention render value? because i am returning an object as user. Basically instead of

renderText({ node }) {    return `${this.options.suggestion.char}${node.attrs.id}`  },

i like to return

renderText({ node }) {    return `${this.options.suggestion.char}${node.attrs.id.NANE}`  },

So its not possible to configure renderText from outside, right?

@tomhrtly
Copy link
Contributor Author

@nadar Can you not pass the specific value you need into the data attributes that already exist? I'm not sure exactly what you're trying to achieve

@nadar
Copy link

nadar commented May 23, 2021

I think i am trying to do the same as you, maintain a NAME and an ID. But then display the NAME of the user. Sorry i don't want to be off-topic in your PR.

renderText() decides what should be rendered as value inside the element, and in my case the selection returns an object with NAME (or TEXT) and ID

@nadar
Copy link

nadar commented May 26, 2021

i know its still off-topic, but i found the solution to customize the output when working with an object as mention response value. Maybe others are google for such a solution and are beginners like me. So decided to post the solution here, if it helps someone it was worth it :-)

Its not possible to override the extensionsrenderText function but of course its possible to override the renderHTML function. Therefore use something like this:

renderHTML({ node, HTMLAttributes }) {
    return [
      'span',
      mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
      `@${node.attrs.name}` // <---- 
    ]
  },

Where name is an attribute defined previously:

addAttributes() {
    return {
      name: {
        default: null,
        parseHTML: element => {
          return {
            name: element.getAttribute('data-mention-name'),
          }
        },
        renderHTML: attributes => {
          if (!attributes.name) {
            return {}
          }
          return {
            'data-mention-name': attributes.name,
          }
        },
      },
      id: {
        default: null,
        parseHTML: element => {
          return {
            id: element.getAttribute('data-mention-id'),
          }
        },
        renderHTML: attributes => {
          if (!attributes.id) {
            return {}
          }
          return {
            'data-mention-id': attributes.id,
          }
        },
      },
    }
  },
})

As people often need context, here is the full solution

const CustomMention = Mention.extend({
  renderHTML({ node, HTMLAttributes }) {
    return [
      'span',
      mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
      `@${node.attrs.name}`
    ]
  },
  addAttributes() {
    return {
      name: {
        default: null,
        parseHTML: element => {
          return {
            name: element.getAttribute('data-mention-name'),
          }
        },
        renderHTML: attributes => {
          if (!attributes.name) {
            return {}
          }
          return {
            'data-mention-name': attributes.name,
          }
        },
      },
      id: {
        default: null,
        parseHTML: element => {
          return {
            id: element.getAttribute('data-mention-id'),
          }
        },
        renderHTML: attributes => {
          if (!attributes.id) {
            return {}
          }
          return {
            'data-mention-id': attributes.id,
          }
        },
      },
    }
  },
})

and then define the editor with the custom mention extension

new Editor({
      extensions: [
        StarterKit,
        CustomMention.configure({
          HTMLAttributes: {
            class: 'mention',
          },
          suggestion: {
            items: async function(query) {
              const response = await this.$axios('users?query=' + query)
              var values = []
              response.data.forEach(user => {
                values.push({id: userr.user_id, name: userr.user.firstname})
              })

              return values
            }.bind(this),
//.... here goes the rest from the example in the docs

I could send a PR for the docs, if you are interested in such extensive guide examples @hanspagel

@philippkuehn
Copy link
Contributor

BTW: renderText is only used for the text representation when copying mentions into plain text fields/editors. by default atom nodes would be rendered as an empty string.

@tomhrtly
Copy link
Contributor Author

@philippkuehn What changes do you need me to make to get this PR approved? What's the best way forward?

@mmachatschek
Copy link
Contributor

Issue #1316 also mentions this

Copy link
Contributor

@philippkuehn philippkuehn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I think I would like to merge that, however I would want to rename "text" to "label". Can you change this?

@tomhrtly
Copy link
Contributor Author

@philippkuehn All done

@philippkuehn
Copy link
Contributor

Great!

Another thing I’m not quite sure about: Currently the id is stored via the attribute data-mention. Does it make sense to change this to data-id? data-mention would still be necessary to parse the node.

@tomhrtly
Copy link
Contributor Author

@philippkuehn In my use case it would, but other developers using this may not be passing the ID but a username instead so data-mention covers different types of data that could be sent (username, ID, full name)

@hanspagel
Copy link
Contributor

hanspagel commented May 31, 2021

id = identifier = the thing that identifies it, so +1 for data-id from me 🙃

@tomhrtly
Copy link
Contributor Author

tomhrtly commented Jun 1, 2021

Happy to change to data-id if others think the same @philippkuehn

@shadow-light
Copy link
Contributor

shadow-light commented Jun 8, 2021

Just a suggestion but would it be worth making the label dynamic by not storing it as an attribute? If the above use cases are similar to mine, I prefer to have the label get updated when the data does. By storing it as an attribute it is similar to hard-coding it with the value it first had when chosen. Where as if a user's name changes, this won't be reflected in the HTML.

If going down this route, perhaps it would be best to just supply a new label config option that takes a function and the id as an argument, and let the user lookup the value.

@shadow-light
Copy link
Contributor

shadow-light commented Jun 8, 2021

Mention.configure({
    label: id => MY_LABELS[id],
}).extend({
    renderHTML({node, HTMLAttributes}){
        return [
            'span',
            mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
            this.options.label(node.attrs.id),
        ]
    },
}),

@philippkuehn philippkuehn merged commit c3afe88 into ueberdosis:main Jun 14, 2021
@philippkuehn
Copy link
Contributor

@tomhrtly I’ve added data-id 👍

@philippkuehn
Copy link
Contributor

@shadow-light Yeah you are right. I’ve learned that rendering mentions is very individual. But I like your idea and added a renderLabel option to overwrite the default way (3b78af4).

So right now there are some ways to render mentions.

  1. use id
  2. use id + label
  3. use id + add a custom node view to render names asynchronously (fetched by API for example)
  4. use id + custom renderLabel method if names are available synchronously anywhere in your app

@SF-Simon
Copy link

@shadow-light Yeah you are right. I’ve learned that rendering mentions is very individual. But I like your idea and added a renderLabel option to overwrite the default way (3b78af4).

So right now there are some ways to render mentions.

  1. use id
  2. use id + label
  3. use id + add a custom node view to render names asynchronously (fetched by API for example)
  4. use id + custom renderLabel method if names are available synchronously anywhere in your app

Why didn't I get the right variables?

suggestion: {
            items: function (query) {
              return [
                { id: 1, label: "aaa" },
                { id: 11, label: "aaa1" },
                { id: 12, label: "aaa2" },
                { id: 13, label: "aaa3" },
              ];
            },

But here's the variable:

{id: Proxy, label: null}
  id: Proxy
    [[Handler]]: Object
    [[Target]]: Object
    [[IsRevoked]]: false
  label: null

Everything else is default.

"name": "@tiptap/extension-mention",
"description": "mention extension for tiptap",
"version": "2.0.0-beta.61",

@nadar

@villageseo
Copy link

@HeebeLee did you find out how to win this ?

@SF-Simon
Copy link

SF-Simon commented Jul 20, 2021 via email

@villageseo
Copy link

@HeebeLee I've won this by minor changing in our custom MentionList.vue component:

selectItem(index) { const item = this.items[index]; if (item) { this.command({ id: item.id, label: item.label, avatar: item.avatar }); } },

Then it works like a charm. It wasn't very obvious at first..

@SF-Simon
Copy link

@HeebeLee I've won this by minor changing in our custom MentionList.vue component:

selectItem(index) { const item = this.items[index]; if (item) { this.command({ id: item.id, label: item.label, avatar: item.avatar }); } },

Then it works like a charm. It wasn't very obvious at first..

selectItem(index) {
      const item = this.items[index];
      // object
      if (item && item instanceof Object) {
        this.command(item);
      } else {
        // string or number
        this.command({ id: item });
      }
    },
<template>
  <div class="items">
    <button
      class="item"
      :class="{ 'is-selected': index === selectedIndex }"
      v-for="(item, index) in items"
      :key="index"
      @click="selectItem(index)"
    >
      {{ item.label ?? item }}
    </button>
  </div>
</template>

This is our code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants