xiaotiandada/blog

tribute 实现@(艾特)功能

Opened this issue · 0 comments

利用 https://github.com/zurb/tribute 实现一个前端@的功能 大概长这样。

image

因为前端是Vue(Nuxt)所以用了一个别人封装好的库 https://github.com/syropian/vue-tribute

在Nuxt里面用?

如果你不需要在Nuxt里面用就可以跳过

// import Tribute from 'tributejs'
let Tribute

if (process.client) {
  Tribute = require('tributejs')
}

const VueTribute = {
  name: 'vue-tribute',
  props: {
    options: {
      type: Object,
      required: true
    }
  },
  watch: {
    options: {
      immediate: false,
      deep: true,
      handler() {
        if (this.tribute) {
          setTimeout(() => {
            var $el = this.$slots.default[0].elm
            this.tribute.detach($el)

            setTimeout(() => {
              $el = this.$slots.default[0].elm
              this.tribute = new Tribute(this.options)
              this.tribute.attach($el)
              $el.tributeInstance = this.tribute
            }, 0)
          }, 0)
        }
      }
    }
  },
  mounted() {
    if (process.client) {
      if (typeof Tribute === 'undefined') {
        throw new Error('[vue-tribute] cannot locate tributejs!')
      }
  
      const $el = this.$slots.default[0].elm
  
      this.tribute = new Tribute(this.options)
  
      this.tribute.attach($el)
  
      $el.tributeInstance = this.tribute
  
      $el.addEventListener('tribute-replaced', e => {
        e.target.dispatchEvent(new Event('input', { bubbles: true }))
      })
    }
  },
  beforeDestroy() {
    const $el = this.$slots.default[0].elm

    if (this.tribute) {
      this.tribute.detach($el)
    }
  },
  render(h) {
    return h(
      'div',
      {
        staticClass: 'v-tribute'
      },
      this.$slots.default
    )
  }
}
if (process.client) {
  if (typeof window !== 'undefined' && window.Vue) {
    window.Vue.component(VueTribute.name, VueTribute)
  }
}

export default VueTribute

因为Nuxt的关系 我基于源码又做了一层封装(加了一些判断而已)。

使用

<client-only>
      <vue-tribute
        :options="tributeOptions"
        @tribute-no-match="noMatchFound"
        @tribute-replaced="tributeReplaced"
      >
        <div
          id="tributeShare"
          class="content-editable"
          contenteditable="true"
          placeholder="谈谈感想"
        />
      </vue-tribute>
</client-only>

搜索用户

最主要的是这个options配置,然后 tribute 有几种输入框,具体看Demo就知道了 这里说一下怎么搜索

      tributeOptions: {
        collection: [
          {
            trigger: '@',
            values: (query, cb) => {
              console.log('query', query)
              if (!query) {
                return cb([])
              }

              return this.searchUser(query, cb)
            },
            loadingItemTemplate: '<div style="padding: 16px">Loading</div>',
            lookup: 'key',
            fillAttr: 'key',
            selectTemplate: function (item) {
              console.log('item', item)
              return `<a
                  class="tribute-mention"
                  contenteditable="false"
                  href="javascript:;"
                  title="${item.original.value}"
                  data-user="${item.original.id}">@${item.original.value}</a>`
            },
          },
        ],
      },

// ...

    searchUser: debounce(async function (val, cb) {
      const list = await search(params)
        return cb(list)
      } else {
        return cb([])
      }
    }, 300),

搜索通过 values 定义的Func来执行 query 是输入的内容 利用cb设置数据

设置用户显示模版

可以自定义Temp,我这里返回了一个 a Tag,默认是 span 我为了方便改成了 a。
并且设置了 data-user 自定义数据 方便后续做渲染操作。

return `<a
class="tribute-mention"
contenteditable="false"
href="javascript:;"
title="${item.original.value}"
data-user="${item.original.id}">@${item.original.value}</a>`

Xss过滤

为了避免用户插入HTML等提交,这里用 https://github.com/leizongmin/js-xss 过滤,显示的时候也需要过滤一次 避免用户通过接口提交(也可以用其他的办法)

这里只通过a标签 因为a标签需要做用户跳转的

// 过滤分享内容
const whiteListShare = {
  a: [ 'class', 'contenteditable', 'href', 'title', 'data-user', 'target' ]
}
export const Fn = (html, whiteList = whiteListShare) => {
  return xss(html, {
    whiteList: whiteList,
    stripIgnoreTag: true,
    stripIgnoreTagBody: ['script']
  })
}

渲染

因为搜索用户的时候已经做了a标签处理只需要设置属性就可以

    ele.setAttribute('href', 'xxx')

Emoji

https://github.com/jm-david/emoji-mart-vue 可以考虑用这个 选择很多