仿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(/ /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(/ /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(/ /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
}