<template>
<div id="app">
<el-dialog v-dialogDrag title="参数设置" :visible.sync="dialogVisible" @close="closeDialog" width="460px">
<el-form ref="form" label-width="80px">
<el-form-item label="参数名称:">
<el-input v-model="dynamicParam" placeholder="请输入参数" style="width:250px" @input="InputData"></el-input>
</el-form-item>
</el-form>
<div style="margin-left:80px;margin-top:8px;width:308px;font-size:12px;color:#b9b9b9;line-height:21px">
key :字母、数字、下划线组成;长度1~20个半角字符;开头必须为英文字母;不能为纯数字
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="updateDynamicParam">确 定</el-button>
</span>
</el-dialog>
<div class="editable-container">
<div
ref="editor"
contenteditable="true"
@input="handleInput"
@keydown="handleKeydown"
@focus="handleFocus"
@click="handleClick"
class="editable-div"
></div>
<div class="char-counter">{{ charCount }}/{{ maxLength }}</div>
</div>
<el-button @click="openDialog">插入参数</el-button>
<el-button @click="logContent">输出内容</el-button>
</div>
</template>
<script>
export default {
name: 'copyInput',
data() {
return {
dialogVisible: false,
dynamicParam: '',
maxLength: 960,
charCount: 0,
savedRange: null,
editingButton: null,
};
},
methods: {
openDialog() {
const sel = window.getSelection();
if (sel.rangeCount > 0) {
this.savedRange = sel.getRangeAt(0);
}
this.dialogVisible = true;
},
closeDialog() {
this.dialogVisible = false;
this.dynamicParam = '';
this.editingButton = null;
},
InputData(val) {
if (val && val.length > 20) {
val = val.slice(0, 20);
}
this.dynamicParam = val.replace(/[^a-z0-9_]/g, '');
},
insertDynamicParam() {
const regT = /^[a-z][a-z0-9_]*$/g;
if (!regT.test(this.dynamicParam)) {
this.$message.error('参数名称需以字母开头,且不能为纯数字!');
return
}
const paramValue = this.dynamicParam.trim();
if (paramValue) {
const buttonHtml = `<input type="button" class="j-btn" unselectable="on" readonly="" value="\${${paramValue}}">`;
this.insertHtmlAtCursor(buttonHtml);
this.dialogVisible = false;
this.dynamicParam = '';
}
},
updateDynamicParam() {
if (this.editingButton) {
this.editingButton.value = `\${${this.dynamicParam}}`;
this.handleInput();
this.closeDialog();
} else {
this.insertDynamicParam();
}
},
insertHtmlAtCursor(html) {
const editor = this.$refs.editor;
editor.focus();
if (this.savedRange) {
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(this.savedRange);
var range = sel.getRangeAt(0);
range.deleteContents();
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
const frag = document.createDocumentFragment();
let node;
let lastNode;
while ((node = tempDiv.firstChild)) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
if (lastNode) {
range.setStartAfter(lastNode);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
this.savedRange = null;
}
this.handleInput();
},
handleInput() {
const editor = this.$refs.editor;
const textContent = this.getTextContent(editor);
this.charCount = textContent.length;
if (this.charCount > this.maxLength) {
editor.innerHTML = editor.innerHTML.slice(0, this.maxLength);
this.restoreHighlight(editor);
this.charCount = this.maxLength;
editor.focus();
document.execCommand('selectAll', false, null);
document.getSelection().collapseToEnd();
}
},
handleKeydown(event) {
if (this.charCount >= this.maxLength && event.key !== 'Backspace') {
event.preventDefault();
}
},
restoreHighlight(editor) {
const buttons = editor.querySelectorAll('.j-btn');
buttons.forEach(button => {
button.setAttribute('unselectable', 'on');
button.setAttribute('readonly', '');
button.classList.add('j-btn');
});
},
getTextContent(element) {
let text = '';
element.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
text += node.textContent;
} else if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'INPUT' && node.classList.contains('j-btn')) {
text += node.value;
} else if (node.nodeType === Node.ELEMENT_NODE) {
text += this.getTextContent(node);
}
});
return text;
},
logContent() {
const editor = this.$refs.editor;
const textContent = this.getTextContent(editor);
console.log(textContent);
},
handleFocus(event) {
const editor = this.$refs.editor;
const sel = window.getSelection();
if (sel.rangeCount === 0) {
const range = document.createRange();
range.setStart(editor, 0);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
},
handleClick(event) {
if (event.target.classList.contains('j-btn')) {
this.editingButton = event.target;
this.dynamicParam = this.editingButton.value.replace(/^\$\{|\}$/g, '');
this.dialogVisible = true;
}
},
},
};
</script>
<style>
.editable-container {
position: relative;
width: 500px;
}
.editable-div {
-webkit-user-modify: read-write-plaintext-only;
-moz-user-modify: read-write-plaintext-only;
width: 100%;
height: 200px;
overflow-y: auto;
border: 1px solid #ccc;
border-radius: 2px;
padding: 5px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
box-sizing: border-box;
}
.char-counter {
position: absolute;
bottom: 5px;
right: 10px;
font-size: 12px;
color: #999;
}
.j-btn {
color: #1a8de5;
border: none;
background-color: transparent;
cursor: pointer;
}
</style>
细节说明:
openDialog
方法在弹框打开时保存当前的选区。insertHtmlAtCursor
方法在插入HTML时恢复保存的选区,然后在选区位置插入HTML。- 插入完成后,清空保存的选区(
this.savedRange = null
),确保每次插入动态参数时都使用最新的光标位置。 charCount
数据属性:用于跟踪当前的字符数。handleInput
方法:在每次输入时更新charCount
。editable-container
类:用于包裹编辑框和字符计数器。char-counter
类:用于在编辑框右下角显示字符数。getTextContent
方法:递归遍历编辑框的子节点,提取文本节点内容和input
标签的value
值,以计算实际的字符数。-
handleFocus
方法:在focus
事件处理程序中,确保在IE浏览器中可以正确聚焦。使用window.getSelection()
和document.createRange()
来手动设置选择范围。 -
@focus 事件监听器:在
<div>
元素上添加@focus
事件监听器,以便在焦点事件发生时调用handleFocus
方法。 handleClick
方法:当用户点击动态参数按钮时,打开弹框并允许编辑按钮的value
值。editingButton
数据属性:保存当前正在编辑的动态参数按钮。updateDynamicParam
方法:如果正在编辑的按钮存在,更新其value
属性值;否则插入新的动态参数。