Bootstrap

仿infobip模板功能-可通过占位符配置模板内容

模仿infobip制作的模板功能,正文可在任意位置加参数的功能。如下图所示:在正文中通过{{\d}}进行占位,在使用模板时,可在此位置自定制内容,并预览效果。
在这里插入图片描述
代码:

<template>
  <div class="template-body-wrapper">
    <div class="textarea-box">
      <el-form 
        :model="formData"
        :rules="bodyRules"
        ref="textForm"
      >
        <div class="edit-content">
          <div
            contenteditable="true" 
            @blur="handleBlur"
            ref="editor"
            class="iscroll edit-wrapper"
            @keyup="getCursor" 
            @click="getCursor"
            @input="getWordNum"
            id="contentEdit"
            placeholder="请输入消息"
          ></div>
        </div>
        <div class="textarea-bottom">
          <el-popover
            placement="top-start"
            width="480"
            ref="pop"
            trigger="click">
            <div>
              <emotion @handleEmotion="handleEmotion"></emotion>
            </div>
            <button type="button" class="button-no-style" slot="reference"><i class="hz-icon-weixiao"></i></button>
          </el-popover>
          <button type="text" class="d-value button-no-style" @click.prevent="addValue">{+}</button>
          <span 
            class="send-word-num" 
            :class="wordNum>=1014?'active_word_num':''"
          >{{wordNum}}/1024</span>
        </div>
        <el-form-item
          prop="text"
          class="hide-form-item"
        >
          <el-input v-model="formData.text" style="display: none;"></el-input>
        </el-form-item>
      </el-form>
    </div>
    <div class="tip-box">
      <div 
        class="tip-item"
        v-for="(item, index) in bodyTips" 
        :key="index"
      >{{ item }}</div>
    </div>
    <el-form
      :model="dValue"
      :rules="dValueRules"
      ref="dValueForm"
    >
      <ul v-if="hasPlaceholder" class="d-value-list">
        <li 
          v-for="(value, key, index) in dValue"
          :key="index"
          class="d-value-item"
        >
          <el-form-item
            :prop="key"
          >
            <div>
            参数<font v-pre>{{</font>{{ key }}<font>}}</font>
            <el-button type="text" @click="delDValue(key)" class="del-btn">删除</el-button>
            </div>
            <el-input 
              v-model="dValue[key]" 
              placeholder="输入样例内容"
              size="small"
              @input="initFormData"
            ></el-input>
          </el-form-item>
        </li>
      </ul>
    </el-form>
  </div>
</template>

<script>
import {emojiList} from '@/config/config'
import {emotionUrl} from "@/config/env";
import Emotion from '@/pages/wp/chat/component/Emotion.vue'
import { bodyTips, setCursorPosition, getCursorPosition } from "../data"
export default {
  props: {
    formData: {
      type: Object,
      default: () => ({})
    },
  },
  watch: {
    formData(val){
      let contentEdit = document.getElementById('contentEdit');
      contentEdit.innerHTML = val.text
      if(this.formData.examples){
        this.formData.examples.forEach((item, index) => {
          this.dValue[index+1+''] = item
        })
      }
    }
  },
  components: {
    Emotion
  },
  computed: {
    dValueRules(){
      let rules = {}
      Object.keys(this.dValue).forEach(key => {
        rules[key] = [
          {required: true, message: "请输入样例内容", trigger: "change"}
        ]
      })
      return rules
    },
  },
  data(){
    return {
      wordNum: 0,
      bodyTips,
      hasPlaceholder: false,
      index: 1,
      dValue: {},
      bodyRules: {
        text: [
          {required: true, message: "请输入模板内容", trigger: "change"},
          {
            validator: this.validator, trigger: "change"
          }
        ],
        examples: [
          { required: true, message: "请输入样例内容", trigger: "change" }
        ]
      },
      rules: {
        text: [
          { required: true, message: "标题文本不能为空", trigger: "change" },
          { validator: this.checkText, trigger: "change"}
        ],
      }
    }
  },
  methods: {
    handleBlur(){
      
    },
    handleEmotion(index) {
      this.focuson()
      let _index = emojiList.indexOf(index);
      let src = `${emotionUrl}${_index}.png`;
      // document.execCommand("insertImage", false, src);
      document.execCommand("insertText", false, index);
      this.getWordNum()
      this.$refs.pop.showPopper = false;
    },
    focuson() {
      let contentEdit = document.getElementById('contentEdit');
      if (contentEdit.innerHTML == '') {
        contentEdit.focus();
      }else{
        contentEdit.innerHTML = contentEdit.innerHTML
        .replace(/&nbsp;/g, ' ')
        .replace(/<div>/g,'\n')
        .replace(/<\/div>/g,'')
        .replace(/<br>/g, '\n')
        const len = contentEdit.innerHTML.length
        if(this.cursorPosition > len){
          this.cursorPosition = len
        }
        setCursorPosition( this.$refs.editor, this.cursorPosition)
      }
    },
    getWordNum(e) {
      let contentEdit = document.getElementById('contentEdit');
      let innerHtml = contentEdit.innerHTML;
      let imglist = innerHtml.match(/<img .*?src="(.*?)".*?\/?>/g);
      let text = innerHtml.replace(/<.*?>/g, "").replace(/&nbsp;/g, ' ');
      // let text = innerHtml.replace(/<.*?>/g,"");
      if (imglist) {
        this.wordNum = imglist.length + text.length;
      } else {
        this.wordNum = text.length;
      }
      let innerLength = innerHtml.length;

      if (this.wordNum > 1024) {
        this.moreInnerHtml = innerHtml.substr(0, innerLength - 1)
        e.target.innerHTML = '';
        document.execCommand('insertHTML', false, innerHtml.substr(0, innerLength - 1));
      }
      this.NoNumText = text.replace(/[0-9]/ig, "");
      this.getText()
    },
    //获取光标的位置
    getCursor () {
      this.editEle = this.$refs.editor
      this.cursorPosition = getCursorPosition(this.editEle);
    },
    addValue(){
      this.focuson()
      document.execCommand(
        'insertText', 
        false, 
        `{{${Object.keys(this.dValue).length+1}}}`
      );
      this.cursorPosition += (Object.keys(this.dValue).length+1+'').length + 4
    },
    delDValue(index){
      let contentEdit = document.getElementById('contentEdit');
      let text = contentEdit.innerHTML.replace(`{{${index}}}`, '')
      contentEdit.innerHTML = text
      this.getText()
      delete this.dValue[index]
      setCursorPosition(this.$refs.editor)
    },
    validator(rule, value, callback){
      const matchs = value.match(/\{\{(.*?)\}\}/g)
      if(matchs){
        // 顺序要对 必须是1 2 3 4... 不能是4 5 6(缺数), 不能是3 2 1 4 5(乱序)
        const indexs = matchs.map(item => {
          return item.slice(2, -2)
        })
        // 判断是否是一个从1开始递增(低增值为1)的数组
        const isValid = indexs.every((item, index) => {
          return item - index === 1
        })
        if(isValid){
          this.setPlaceHolderValue(indexs)
          callback()
          this.hasPlaceholder = true
        }else{
          this.resetPlaceHolderValue()
          callback(new Error('无效占位符顺序'))
        }
      }else{
        callback()
        this.hasPlaceholder = false
        this.dValue = {}
        this.initFormData()
      }
    },
    setPlaceHolderValue(arr){
      let obj = {}
      arr.forEach((item, index) => {
        obj[item+''] = this.dValue[item+'']
      })
      this.dValue = obj
      this.initFormData()
    },
    resetPlaceHolderValue(){
      this.hasPlaceholder = false
      this.dValue = {}
      this.initFormData()
      this.getText()
      this.formData.examples = []
    },
    // 获取formData数据
    getText(){
      const contentEdit = document.getElementById('contentEdit');
      const text = contentEdit.innerHTML
        .replace(/&nbsp;/g, ' ')
        .replace(/<div>/g,'\n')
        .replace(/<\/div>/g,'')
        .replace(/<br>/g, '\n')
      this.formData.text = text    
    },
    initFormData(){
      this.formData.examples = Object.keys(this.dValue).map(key => this.dValue[key])
    },
    validate(){
      return Promise.all([
        this.$refs.textForm.validate(),
        this.$refs.dValueForm.validate()
      ]).then(([res1, res2]) => {
        return res1 && res2
      })
    }
  },
}
</script>

<style lang="scss">
.template-body-wrapper{
  .textarea-box{
    border: 1px solid #DCDFE6;
    width: 100%;
    margin-bottom: 20px;
    textarea{
      border-radius: 0;
      border: none;
      border-bottom: 1px solid #DCDFE6!important;
    }
    .el-form .hide-form-item{
      margin: 0;
    }
  }
  .el-textarea .el-input__count{
    position: absolute;
    bottom: -32px;
  }
  .edit-content {
    height: 100px;
    width: 100%;
    position: relative;
    .edit-wrapper {
      height: 100%;
      width: 100%;
      padding: 8px 12px;
      outline: none;
      overflow-y: auto;
      padding-bottom: 40px;
      line-height: 20px;
      vertical-align: top;
      font-size: 14px;
      &:empty:before {
        font-size: 14px;
        content: attr(placeholder);
        color: #bbb;
      }
      &:focus {
        content: none;
      }
      img {
        height: 30px;
        width: auto;
        display: inline-block;
        /*vertical-align: middle;*/
      }
        /*background: #ffffff;*/
    }
    .send-btn-box {
      display: inline-block;
      float: right;
      height: 30px;
      line-height: 30px;
      .send-tip {
          font-size: 12px;
          font-weight: 400;
          color: #90959E;
          line-height: 22px;
          margin-right: 8px;
      }
      .send-btn {
          margin-right: 8px;
      }
    }
  }
  .button-no-style{
    border: none;
    cursor: pointer;
    background: none;
  }
  .hz-icon-weixiao{
    font-size: 20px;
  }
  .d-value{
    font-size: 16px;
    line-height: 20px;
    vertical-align: text-bottom;
    background: none;
  }
  .textarea-bottom{
    border-top: 1px solid #DCDFE6;
  }
  .send-word-num {
    width: 42px;
    height: 30px;
    font-size: 12px;
    font-weight: 400;
    color: #90959E;
    line-height: 30px;
    margin-right: 12px;
    float: right;
    &.active_word_num {
      color: #E6A23C;
    }
  }
  .tip-item{
    color: #90959E;
    line-height: 30px;
    font-size: 12px;
  }
  .el-form-item{
    margin-bottom: 10px;
  }
  .del-btn{
    margin-left: 8px;
  }
  .edit-wrapper{
    white-space: pre-line;
  }
}
</style>

data/index.js

export const setCursorPosition = (element, cursorPosition) => {
  const range = document.createRange()
  const lastChild = element.lastChild
  if(lastChild.innerHTML){
    if(cursorPosition === undefined){
      range.setStart(lastChild.firstChild, lastChild.innerHTML.length)
      range.setEnd(lastChild.firstChild, lastChild.innerHTML.length)
    }else{
      range.setStart(lastChild.firstChild, cursorPosition)
      range.setEnd(lastChild.firstChild, cursorPosition)
    }
  }else{
    if(cursorPosition === undefined){
      range.setStart(lastChild, lastChild.length)
      range.setEnd(lastChild, lastChild.length)
    }else{
      range.setStart(lastChild, cursorPosition)
      range.setEnd(lastChild, cursorPosition)
    }
  }
  const sel = window.getSelection()
  sel.removeAllRanges()
  sel.addRange(range)
}


export const getCursorPosition = (element) => {
  let caretOffset = 0
  const doc = element.ownerDocument || element.document
  const win = doc.defaultView || doc.parentWindow
  const sel = win.getSelection()
  if (sel.rangeCount > 0) {
    const range = win.getSelection().getRangeAt(0)
    const preCaretRange = range.cloneRange()
    preCaretRange.selectNodeContents(element)
    preCaretRange.setEnd(range.endContainer, range.endOffset)
    const divs = preCaretRange.commonAncestorContainer.innerHTML.match(/<div>/g)
    const len = divs ? divs.length : 0
    caretOffset = preCaretRange.toString().length + len
  }
  return caretOffset
}
;