Bootstrap

vue-textarea光标位置插入指定元素

vue-textarea光标位置插入指定元素

需求

点击插入关键字的时候把内容插入到光标所在的位置

效果图

img

实现

html

      <div class="temlate-container">
                <div class="template-content">
                  <el-input
                    ref="modelContent"
                    v-model="mould.modelContent"
                    autocomplete="off"
                    :rows="4"
                    placeholder="请输入关键字"
                    type="textarea"
                    style="width:100%"
                    :readonly="readonly"
                    @input="listenKeyword"
                  />
                  <div v-show="!readonly" class="keyword-content">
                    <span class="keyword-title">插入关键字</span>
                    <div class="keyword-select">
                      <div class="mould-key">
                        <list-code-selector
                          v-model="mouldKeyName"
                          type="mouldKey"
                          :readonly="readonly"
                          @change="handleKeyword"
                        />
                        <el-button round :loading="loading" type="primary" @click="select">插入关键字</el-button>
                      </div>
                    </div>
                  </div>
                </div>
                <div class="example">示例:{{ mould.example }}</div>
              </div>

js方法

  select() {
        if (isBlank(this.mouldKeyName)) return this.$message.warning('请选择关键字')
        const dom = this.$refs.modelContent.$refs.textarea
        console.log('this.mould.modelContent', this.mould.modelContent)
        let pos = 0
        if (dom.selectionStart || dom.selectionStart === 0) {
          // dom.selectionStart第一个被选中的字符的序号(index),从0开始
          pos = dom.selectionStart
          console.log(' dom.selectionStart', dom.selectionStart)
        } else if (document.selection) {
          // IE Support
          const selectRange = document.selection.createRange()
          selectRange.moveStart('character', -dom.value.length)
          pos = selectRange.text.length
        }
        console.log('pos', pos)
        const before = this.mould.modelContent.substr(0, pos)
        const after = this.mould.modelContent.substr(pos)
        console.log(pos, 'before', before, 'after===', after)
        this.mould.modelContent = before + '{' + this.mouldKeyName + '}' + after
        setTimeout(() => {
          if (dom.setSelectionRange) {
            // 当前元素内的文本设置备选中范围
            dom.setSelectionRange(pos, pos + this.mouldKeyName.length + 2)
            console.log('当前元素内的文本设置备选中范围====', pos, pos + this.mouldKeyName.length + 2)
          } else if (dom.createTextRange) {
            console.log('聚焦控件后把光标放到最后', dom.createTextRange)
            // IE Support
            const range = dom.createTextRange()
            range.collapse(true)
            range.moveEnd('character', pos)
            range.moveStart('character', pos)
            range.select()
          }
        }, 0)
        this.listenKeyword()//监听输入或选中内容是都括号对应及把选中的name改成example放入对应的示例下
      },
// 监听模块内容输入框
      listenKeyword() {
        const modelContent = this.mould.modelContent
        const getLocalexample = JSON.parse(localStorage.getItem('example'))
        const haveVal = /\{(.+?)\}/g // {123}|{} 花括号,大括号
        const result = modelContent.match(haveVal)
        if (modelContent.length === 0) {
          this.mould.example = ''
        }
        this.mould.example = modelContent
        if (isNotBlank(result) && this.isBracketBalance(modelContent)) {
          this.isSymmetry = false
          this.mould.example = modelContent
          result.forEach(code => {
            getLocalexample.forEach(key => {
              if (key.example) {
                if (code === key.code) {
                  this.mould.example = this.mould.example.replace(key.code, key.example)
                  // this.mould.example = this.mould.example.replace(key.code, '{' + key.example + '}')
                }
              } else {
                this.mould.example = this.mould.example.replace(key.code, '{' + this.mouldKeyName + '}')
              }
            })
          })
        } else if (!this.isBracketBalance(modelContent)) {
          this.isSymmetry = true
          this.$message.warning('缺少完整括号字符')
        }
      },
      // 判断括号是否对称
      isBracketBalance(str) {
        var leftBracketNum = 0
        var strLength = str.length
        for (var i = 0; i < strLength; i++) {
          var temp = str.charAt(i)
          if (temp === '{') {
            leftBracketNum++
          }
          if (temp === '}') {
            leftBracketNum--
          }
        }
        // 最后判断leftBracketNum,如果为0表示平衡否则不平衡
        if (leftBracketNum === 0) {
          return true
        } else {
          return false
        }
      },

来源:

  • https://blog.csdn.net/qq_40190624/article/details/109217790
  • https://blog.csdn.net/xuhang139/article/details/123050596

el-input textarea插入文字标签

  1. 字符串模板以“[“开始 以“]”结束 是一个元素

  2. 按下backspace退格键或者delete删除键删除字符时,如果删的是字符串模板,会删除整个字符串模板元素

  3. 选择文字元素后,会选择整个的字符串模板

  4. 字符串模板得到焦点后会将焦点移动到字符串模板后

img

<template>
    <div>
        <el-form label-width="80px">
            <el-form-item label="内容">
                <el-input
                    ref="smsInput"
                    type="textarea"
                    v-model="smsContent"
                    placeholder="请输入内容"
                    :rows="4"
                    @input.native="smsInput"
                    @blur="inputBlur"
                    @focus="focusHandler"
                    @click.native="focusHandler"
                    @keydown.up.down.left.right.native="focusHandler"
                    @select.native="selectHandler"
                />
            </el-form-item>
        </el-form>
        <el-popover style="margin-left: 10px; float: right;" placement="right-start" width="200" v-model="visible" trigger="manual">
            <div class="insert-list">
                <p class="i-title">元素列表</p>
                <div v-for="(item,index) in btns" :key="index">
                    {{ item }}
                    <el-button @click="insertStr('['+item+']')" size="mini" type="primary">插入</el-button>
                </div>
            </div>
            <el-button slot="reference" size="small" @click="visible = !visible" type="primary">插入元素</el-button>
        </el-popover>
    </div>
</template>

<script>
export default {
    data() {
        return {
            smsContent: "",
            inputFocus: null,
            visible: false,
            btns: ['姓名', '费用', '日期', '电话号码', '发件人']
        };
    },
    methods: {
        // 插入元素
        insertStr(str) {
            let before = this.smsContent.slice(0, this.inputFocus);
            let after = this.smsContent.slice(
                this.inputFocus,
                this.smsContent.length
            );
            this.inputFocus = this.inputFocus + str.length;
            this.smsContent = before + str + after;
            this.$emit("smsText", this.smsContent);
        },
        // 保存光标位置
        inputBlur(e) {
            this.inputFocus = e.target.selectionStart;
            this.visible = false;
        },
        // 删除元素剩余部分
        smsInput(e) {
            //deleteContentBackward==退格键 deleteContentForward==del键
            if (e.inputType === "deleteContentBackward" || e.inputType === "deleteContentForward") {
                let beforeIndex = 0;
                let afterIndex = 0;
                // 光标位置往前
                for (let i = e.target.selectionStart - 1; i >= 0; i--) {
                    if (this.smsContent[i] === "[") {
                        beforeIndex = i;
                        afterIndex = e.target.selectionStart;
                        break;
                    }
                    if (this.smsContent[i] === "]") {
                        break;
                    }
                }
                // 光标位置往后
                for (let i = e.target.selectionStart; i < this.smsContent.length; i++) {
                    if (this.smsContent[i] === "]") {
                        afterIndex = i + 1;
                        beforeIndex = e.target.selectionStart;
                        break;
                    }
                    if (this.smsContent[i] === "[") {
                        break;
                    }
                }
                if (beforeIndex === 0 && afterIndex === 0) {
                    return
                }
                let beforeStr = this.smsContent.slice(0, beforeIndex)
                let afterStr = this.smsContent.slice(afterIndex)
                this.smsContent = beforeStr + afterStr
                this.inputFocus = beforeStr.length
                this.$nextTick(() => {
                    this.changeFocus(e.target, this.inputFocus, this.inputFocus);
                });
            }
            this.$emit("smsText", this.smsContent);
        },
        // 选择元素剩余部分
        selectHandler(e) {
            // 光标开始位置往前
            for (let i = e.target.selectionStart - 1; i >= 0; i--) {
                if (this.smsContent[i] === "[") {
                    this.changeFocus(e.target, i, e.target.selectionEnd);
                    break;
                }
                if (this.smsContent[i] === "]") {
                    break;
                }
            }
            // 光标结束位置往后
            for (let i = e.target.selectionEnd; i < this.smsContent.length; i++) {
                if (this.smsContent[i] === "]") {
                    this.changeFocus(e.target, e.target.selectionStart, i + 1);
                    break;
                }
                if (this.smsContent[i] === "[") {
                    break;
                }
            }
        },
        // 焦点跳出元素内
        focusHandler(e) {
            setTimeout(() => {
                let selStart = e.target.selectionStart
                let beforeArrLength = this.smsContent.slice(0, selStart).split("[").length;
                let afterArrLength = this.smsContent.slice(0, selStart).split("]").length;
                //根据'['和']'生成两个数组 判断数组长度 是否相等 不相等就不成对就移动光标
                if (beforeArrLength !== afterArrLength) {
                    let pos = this.smsContent.indexOf("]", selStart) + 1
                    if (beforeArrLength > afterArrLength && e.code === 'ArrowLeft') {
                        //按下按键左箭头
                        pos = this.smsContent.lastIndexOf("[", selStart)
                    }
                    this.changeFocus(e.target, pos, pos);
                }
            }, 100);
        },
        // 修改光标位置
        changeFocus(target, start, end) {
            let range, el = target;
            if (el.setSelectionRange) {
                el.setSelectionRange(start, end);
            } else {
                range = el.createTextRange();
                range.collapse(false);
                range.select();
            }
        },
    },
};
</script>

<style scoped>
.insert-list p {
    text-align: center;
}

.insert-list div {
    margin: 10px 0;
    display: flex;
    justify-content: space-between;
}
</style>

来源:https://blog.csdn.net/xuhang139/article/details/123050596

自定义实现方案

效果

img

代码

customTemplateVariable.vue

<template>
    <div>
        <el-form ref="dataForm" :rules="rules" :model="dataForm" label-width="80px">
            <el-form-item label="模板内容:" prop="templateContext">
                <div ref="promptContentDiv" v-model="dataForm.templateContext" :contenteditable="true" v-html="contentHtml" style="padding: 6px;border:1px solid #c0c4cc;width:100%; height: 100px;display: inline-block;"/>
                <!--添加变量下拉菜单-->
                <el-dropdown @command="addVariable">
                    <span class="el-dropdown-link">
                        <div class="el-button el-button--text" style="cursor: default;">
                            <i style="font-size: 16px; color: #8c8a8c;" class="el-icon-circle-plus-outline cursor_pointer">添加变量</i>
                        </div>
                    </span>
                    <el-dropdown-menu class="dropdown-max" slot="dropdown">
                        <el-dropdown-item
                            v-for="(variable, index) in variableNames"
                            :key="index+'var'"
                            :command="variable.dictLabel">
                            <button type="button" class="el-button el-button--text">
                                {{ variable.dictLabel }}
                            </button>
                        </el-dropdown-item>
                    </el-dropdown-menu>
                </el-dropdown>
                <el-button :loading="submitLoading" type="primary" @click="templateSubmit">提交</el-button>
            </el-form-item>
        </el-form>
    </div>
</template>

<script>
import {parsePromptVariable, parsePromptTags} from '@/api/graph_extend'

export default {
    /** 自定义模板变量组件 */
    name: 'customTemplateVariable',
    components: {},
    data() {
        return {
            // 数据表单
            dataForm: {},
            // 提交按钮禁用状态
            submitLoading: false,
            // Html模板内容
            contentHtml: '',
            // 变量名称列表
            variableNames: [],
            // 表单验证规则
            rules: {
                templateContext: [{required: true, message: '模板内容不能为空', trigger: 'blur'}]
            }
        }
    },
    created() {
        // 初始选择项
        this.initOptions()
    },
    /** 渲染完成后执行 */
    mounted() {
        // 添加自定义文本域监听事件
        this.addCustomTextareaListener();
    },
    methods: {
        initOptions() {
            // 模拟添加变量数据
            this.variableNames.unshift({dictLabel: '名称', dictValue: 'name'})
            this.variableNames.unshift({dictLabel: '年龄', dictValue: 'age'})
            this.variableNames.unshift({dictLabel: '性别', dictValue: 'gender'})
        },
        /**
         * 添加变量
         * @param command 命令
         */
        addVariable: function(command) {
            let sel = window.getSelection()

            let node_prompt_variable_prefix = '<span contenteditable="false" disabled="disabled" class="el-tag el-tag--info el-tag--small">'
            let node_prompt_variable_suffix = '</span>'

            // 只在当前输入框中插入
            console.log("addVariable", command);
            console.log("addVariable1", sel.focusNode);
            console.log("addVariable2", sel.focusNode ? sel.focusNode.parentNode : null);
            console.log("addVariable3", this.$refs.promptContentDiv);
            if (!sel.focusNode || (sel.focusNode.parentNode !== this.$refs.promptContentDiv && sel.focusNode !== this.$refs.promptContentDiv)) {
                return
            }
            if (sel.getRangeAt && sel.rangeCount) {
                let range = sel.getRangeAt(0)
                range.deleteContents()

                let el = document.createElement('div')
                el.innerHTML = node_prompt_variable_prefix + command + node_prompt_variable_suffix
                let node, lastNode, frag = document.createDocumentFragment()
                while ((node = el.firstChild)) {
                    lastNode = frag.appendChild(node)
                }
                range.insertNode(frag)

                if (lastNode) {
                    range = range.cloneRange()
                    range.setStartAfter(lastNode)
                    range.collapse(true)
                    sel.removeAllRanges()
                    sel.addRange(range)
                }
            }
        },
        /**
         * 添加自定义文本域监听事件
         */
        addCustomTextareaListener() {
            let self = this
            let el = this.$refs.promptContentDiv
            if (this.dataForm.templateContext) {
                // 模板内容替换为显示变量
                let tempContext = this.replaceShowVariables(this.dataForm.templateContext);
                this.$set(this.dataForm, 'templateContext', tempContext);

                // 加载后,将${}变量变为html标签
                this.contentHtml = parsePromptVariable(tempContext)
                el.innerHTML = this.contentHtml
            }
            // 添加监听事件,动态获取编辑框内容
            el.addEventListener('DOMSubtreeModified', function () {
                if (el.innerHTML !== null && el.innerHTML !== undefined) {
                    self.dataForm.templateContext = parsePromptTags(el.innerHTML)
                }
            })
        },
        /**
         * 模板内容替换为数据变量
         * @param templateContext 模板内容
         * @return {*} 替换后的模板内容
         */
        replaceDataVariables(templateContext){
            if (!templateContext) {
                return "";
            }

            for (let i in this.variableNames) {
                templateContext = templateContext.replaceAll("{" + this.variableNames[i].dictLabel + "}", "{" + this.variableNames[i].dictValue + "}");
            }
            return templateContext;
        },
        /**
         * 模板内容替换为显示变量
         * @param templateContext 模板内容
         * @return {*} 替换后的模板内容
         */
        replaceShowVariables(templateContext){
            if (!templateContext) {
                return "";
            }

            for (let i in this.variableNames) {
                templateContext = templateContext.replaceAll("{" + this.variableNames[i].dictValue + "}", "{" + this.variableNames[i].dictLabel + "}");
            }
            return templateContext;
        },
        templateSubmit(){
            this.$refs['dataForm'].validate(valid => {
                if (valid) {
                    // 提交按钮禁用状态
                    this.submitLoading = true
                    // 模板内容替换为数据变量
                    let tempContext = this.replaceDataVariables(this.dataForm.templateContext);

                    console.log("templateContext", this.dataForm.templateContext);
                    console.log("tempContext", tempContext);

                    // 提交按钮非禁用状态
                    this.submitLoading = false
                }
            })
        }
    }
}
</script>

graph_extend.js

// 播报语变量样式前缀,后缀
const node_prompt_variable_prefix = '<span contenteditable="false" disabled="disabled" class="el-tag el-tag--info el-tag--small">';
const node_prompt_variable_suffix = '</span>';

// 解析播报语变量,将 {} 标识的变量用 <span></span> 标签包裹
export function parsePromptVariable(str) {
    let reg = /\{(.*?)}/g;
    return str.replace(reg, function (result, group) {
        return node_prompt_variable_prefix + group + node_prompt_variable_suffix;
    });
}

// 获取播报语中变量,将 <span></span> 标签标识的变量用 {} 标识
export function parsePromptTags(str) {
    let reg = new RegExp(node_prompt_variable_prefix + '(.*?)' + node_prompt_variable_suffix, 'g');
    return str.replace(reg, function (result, group) {
        return "{" + group + "}";
    });
}
;