Vue 2 contentEditable with v-model
Vue 2 contentEditable with v-model
我正在尝试制作一个类似于 Medium 的文本编辑器。我正在使用一个内容可编辑的段落标记,并将每个项目存储在一个数组中,并使用 v-for 呈现每个项目。但是,我在使用 v-model 将文本与数组绑定时遇到了问题。似乎与 v-model 和 contenteditable 属性 有冲突。这是我的代码:
<div id="editbar">
<button class="toolbar" v-on:click.prevent="stylize('bold')">Bold</button>
</div>
<div v-for="(value, index) in content">
<p v-bind:id="'content-'+index" v-bind:ref="'content-'+index" v-model="content[index].value" v-on:keyup="emit_content($event)" v-on:keyup.delete="remove_content(index)" contenteditable></p>
</div>
在我的脚本中:
export default {
data() {
return {
content: [{ value: ''}]
}
},
methods: {
stylize(style) {
document.execCommand(style, false, null);
},
remove_content(index) {
if(this.content.length > 1 && this.content[index].value.length == 0) {
this.content.splice(index, 1);
}
}
}
}
我还没有在网上找到任何答案。
我试了一个例子,eslint-plugin-vue reported that v-model
isn't supported on p
elements. See the valid-v-model规则。
截至撰写本文时,Vue 似乎并未直接支持您想要的内容。我将介绍两个通用解决方案:
直接在可编辑元素上使用输入事件
<template>
<p
contenteditable
@input="onInput"
>
{{ content }}
</p>
</template>
<script>
export default {
data() {
return { content: 'hello world' };
},
methods: {
onInput(e) {
console.log(e.target.innerText);
},
},
};
</script>
创建可重复使用的可编辑组件
Editable.vue
<template>
<p
ref="editable"
contenteditable
v-on="listeners"
/>
</template>
<script>
export default {
props: {
value: {
type: String,
default: '',
},
},
computed: {
listeners() {
return { ...this.$listeners, input: this.onInput };
},
},
mounted() {
this.$refs.editable.innerText = this.value;
},
methods: {
onInput(e) {
this.$emit('input', e.target.innerText);
},
},
};
</script>
index.vue
<template>
<Editable v-model="content" />
</template>
<script>
import Editable from '~/components/Editable';
export default {
components: { Editable },
data() {
return { content: 'hello world' };
},
};
</script>
针对您的特定问题的自定义解决方案
经过多次迭代,我发现对于您的用例,通过 而不是 使用单独的组件更容易获得可行的解决方案。 contenteditable
元素似乎非常棘手 - 特别是在列表中呈现时。我发现我必须在删除后手动更新每个 p
的 innerText
才能使其正常工作。我还发现使用 ids 有效,但使用 refs 无效。
可能有一种方法可以在模型和内容之间实现完整的双向绑定,但我认为这需要在每次更改后操纵光标位置。
<template>
<div>
<p
v-for="(value, index) in content"
:id="`content-${index}`"
:key="index"
contenteditable
@input="event => onInput(event, index)"
@keyup.delete="onRemove(index)"
/>
</div>
</template>
<script>
export default {
data() {
return {
content: [
{ value: 'paragraph 1' },
{ value: 'paragraph 2' },
{ value: 'paragraph 3' },
],
};
},
mounted() {
this.updateAllContent();
},
methods: {
onInput(event, index) {
const value = event.target.innerText;
this.content[index].value = value;
},
onRemove(index) {
if (this.content.length > 1 && this.content[index].value.length === 0) {
this.$delete(this.content, index);
this.updateAllContent();
}
},
updateAllContent() {
this.content.forEach((c, index) => {
const el = document.getElementById(`content-${index}`);
el.innerText = c.value;
});
},
},
};
</script>
我昨天弄明白了!解决了这个解决方案。我基本上只是通过更新任何可能的事件并通过手动分配具有动态引用的相应元素重新渲染来手动跟踪 content
数组中的 innerHTML,例如content-0
, content-1
,... 效果很好:
<template>
<div id="editbar">
<button class="toolbar" v-on:click.prevent="stylize('bold')">Bold</button>
</div>
<div>
<div v-for="(value, index) in content">
<p v-bind:id="'content-'+index" class="content" v-bind:ref="'content-'+index" v-on:keydown.enter="prevent_nl($event)" v-on:keyup.enter="add_content(index)" v-on:keyup.delete="remove_content(index)" contenteditable></p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
content: [{
html: ''
}]
}
},
methods: {
add_content(index) {
//append to array
},
remove_content(index) {
//first, check some edge conditions and remove from array
//then, update innerHTML of each element by ref
for(var i = 0; i < this.content.length; i++) {
this.$refs['content-'+i][0].innerHTML = this.content[i].html;
}
},
stylize(style){
document.execCommand(style, false, null);
for(var i = 0; i < this.content.length; i++) {
this.content[i].html = this.$refs['content-'+i][0].innerHTML;
}
}
}
}
</script>
我想我可能想出了一个更简单的解决方案。请参阅下面的代码段:
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head>
<body>
<main id="app">
<div class="container-fluid">
<div class="row">
<div class="col-8 bg-light visual">
<span class="text-dark m-0" v-html="content"></span>
</div>
<div class="col-4 bg-dark form">
<button v-on:click="bold_text">Bold</button>
<span class="bg-light p-2" contenteditable @input="handleInput">Change me!</span>
</div>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
new Vue({
el: '#app',
data: {
content: 'Change me!',
},
methods: {
handleInput: function(e){
this.content = e.target.innerHTML
},
bold_text: function(){
document.execCommand('bold')
}
}
})
</script>
</body>
</html>
解释:
您可以编辑跨度,因为我已经添加了标签 contenteditable
。请注意,在 input
上,我将调用 handleInput 函数,它将内容的 innerHtml 设置为您插入到可编辑范围中的任何内容。然后,要添加 加粗 功能,您只需 select 您想要加粗的内容并单击加粗按钮即可。
额外奖励!它也适用于 cmd+b ;)
希望这对某人有所帮助!
编码愉快
请注意,我通过 CDN 引入了 bootstrap css 样式和 vue,以便它可以在代码段中运行。
您可以使用组件 v-model 在 Vue 中创建 contentEditable。
Vue.component('editable', {
template: `<p
v-bind:innerHTML.prop="value"
contentEditable="true"
@input="updateCode"
@keyup.ctrl.delete="$emit('delete-row')"
></p>`,
props: ['value'],
methods: {
updateCode: function($event) {
//below code is a hack to prevent updateDomProps
this.$vnode.child._vnode.data.domProps['innerHTML'] = $event.target.innerHTML;
this.$emit('input', $event.target.innerHTML);
}
}
});
new Vue({
el: '#app',
data: {
len: 3,
content: [{
value: 'paragraph 1'
},
{
value: 'paragraph 2'
},
{
value: 'paragraph 3'
},
]
},
methods: {
stylize: function(style, ui, value) {
var inui = false;
var ivalue = null;
if (arguments[1]) {
inui = ui;
}
if (arguments[2]) {
ivalue = value;
}
document.execCommand(style, inui, ivalue);
},
createLink: function() {
var link = prompt("Enter URL", "https://codepen.io");
document.execCommand('createLink', false, link);
},
deleteThisRow: function(index) {
this.content.splice(index, 1);
},
add: function() {
++this.len;
this.content.push({
value: 'paragraph ' + this.len
});
},
}
});
<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
<div id="app">
<button class="toolbar" v-on:click.prevent="add()">ADD PARAGRAPH</button>
<button class="toolbar" v-on:click.prevent="stylize('bold')">BOLD</button>
<button class="toolbar" v-on:click.prevent="stylize('italic')">ITALIC</button>
<button class="toolbar" v-on:click.prevent="stylize('justifyLeft')">LEFT ALIGN</button>
<button class="toolbar" v-on:click.prevent="stylize('justifyCenter')">CENTER</button>
<button class="toolbar" v-on:click.prevent="stylize('justifyRight')">RIGHT ALIGN</button>
<button class="toolbar" v-on:click.prevent="stylize('insertOrderedList')">ORDERED LIST</button>
<button class="toolbar" v-on:click.prevent="stylize('insertUnorderedList')">UNORDERED LIST</button>
<button class="toolbar" v-on:click.prevent="stylize('backColor',false,'#FFFF66')">HEIGHLIGHT</button>
<button class="toolbar" v-on:click.prevent="stylize('foreColor',false,'red')">RED TEXT</button>
<button class="toolbar" v-on:click.prevent="createLink()">CREATE LINK</button>
<button class="toolbar" v-on:click.prevent="stylize('unlink')">REMOVE LINK</button>
<button class="toolbar" v-on:click.prevent="stylize('formatBlock',false,'H1')">H1</button>
<button class="toolbar" v-on:click.prevent="stylize('underline')">UNDERLINE</button>
<button class="toolbar" v-on:click.prevent="stylize('strikeThrough')">STRIKETHROUGH</button>
<button class="toolbar" v-on:click.prevent="stylize('superscript')">SUPERSCRIPT</button>
<button class="toolbar" v-on:click.prevent="stylize('subscript')">SUBSCRIPT</button>
<button class="toolbar" v-on:click.prevent="stylize('indent')">INDENT</button>
<button class="toolbar" v-on:click.prevent="stylize('outdent')">OUTDENT</button>
<button class="toolbar" v-on:click.prevent="stylize('insertHorizontalRule')">HORIZONTAL LINE</button>
<button class="toolbar" v-on:click.prevent="stylize('insertParagraph')">INSERT PARAGRAPH</button>
<button class="toolbar" v-on:click.prevent="stylize('formatBlock',false,'BLOCKQUOTE')">BLOCK QUOTE</button>
<button class="toolbar" v-on:click.prevent="stylize('selectAll')">SELECT ALL</button>
<button class="toolbar" v-on:click.prevent="stylize('removeFormat')">REMOVE FORMAT</button>
<button class="toolbar" v-on:click.prevent="stylize('undo')">UNDO</button>
<button class="toolbar" v-on:click.prevent="stylize('redo')">REDO</button>
<editable v-for="(item, index) in content" :key="index" v-on:delete-row="deleteThisRow(index)" v-model="item.value"></editable>
<pre>
{{content}}
</pre>
</div>
您可以使用 watch 方法创建两种方式绑定 contentEditable。
Vue.component('contenteditable', {
template: `<p
contenteditable="true"
@input="update"
@focus="focus"
@blur="blur"
v-html="valueText"
@keyup.ctrl.delete="$emit('delete-row')"
></p>`,
props: {
value: {
type: String,
default: ''
},
},
data() {
return {
focusIn: false,
valueText: ''
}
},
computed: {
localValue: {
get: function() {
return this.value
},
set: function(newValue) {
this.$emit('update:value', newValue)
}
}
},
watch: {
localValue(newVal) {
if (!this.focusIn) {
this.valueText = newVal
}
}
},
created() {
this.valueText = this.value
},
methods: {
update(e) {
this.localValue = e.target.innerHTML
},
focus() {
this.focusIn = true
},
blur() {
this.focusIn = false
}
}
});
new Vue({
el: '#app',
data: {
len: 4,
val: "Test",
content: [{
"value": "<h1>Heading</h1><div><hr id=\"null\"></div>"
},
{
"value": "<span style=\"background-color: rgb(255, 255, 102);\">paragraph 1</span>"
},
{
"value": "<font color=\"#ff0000\">paragraph 2</font>"
},
{
"value": "<i><b>paragraph 3</b></i>"
},
{
"value": "<blockquote style=\"margin: 0 0 0 40px; border: none; padding: 0px;\"><b>paragraph 4</b></blockquote>"
}
]
},
methods: {
stylize: function(style, ui, value) {
var inui = false;
var ivalue = null;
if (arguments[1]) {
inui = ui;
}
if (arguments[2]) {
ivalue = value;
}
document.execCommand(style, inui, ivalue);
},
createLink: function() {
var link = prompt("Enter URL", "https://codepen.io");
document.execCommand('createLink', false, link);
},
deleteThisRow: function(index) {
this.content.splice(index, 1);
if (this.content[index]) {
this.$refs.con[index].$el.innerHTML = this.content[index].value;
}
},
add: function() {
++this.len;
this.content.push({
value: 'paragraph ' + this.len
});
},
}
});
<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
<div id="app">
<button class="toolbar" v-on:click.prevent="add()">ADD PARAGRAPH</button>
<button class="toolbar" v-on:click.prevent="stylize('bold')">BOLD</button>
<button class="toolbar" v-on:click.prevent="stylize('italic')">ITALIC</button>
<button class="toolbar" v-on:click.prevent="stylize('justifyLeft')">LEFT ALIGN</button>
<button class="toolbar" v-on:click.prevent="stylize('justifyCenter')">CENTER</button>
<button class="toolbar" v-on:click.prevent="stylize('justifyRight')">RIGHT ALIGN</button>
<button class="toolbar" v-on:click.prevent="stylize('insertOrderedList')">ORDERED LIST</button>
<button class="toolbar" v-on:click.prevent="stylize('insertUnorderedList')">UNORDERED LIST</button>
<button class="toolbar" v-on:click.prevent="stylize('backColor',false,'#FFFF66')">HEIGHLIGHT</button>
<button class="toolbar" v-on:click.prevent="stylize('foreColor',false,'red')">RED TEXT</button>
<button class="toolbar" v-on:click.prevent="createLink()">CREATE LINK</button>
<button class="toolbar" v-on:click.prevent="stylize('unlink')">REMOVE LINK</button>
<button class="toolbar" v-on:click.prevent="stylize('formatBlock',false,'H1')">H1</button>
<button class="toolbar" v-on:click.prevent="stylize('formatBlock',false,'BLOCKQUOTE')">BLOCK QUOTE</button>
<button class="toolbar" v-on:click.prevent="stylize('underline')">UNDERLINE</button>
<button class="toolbar" v-on:click.prevent="stylize('strikeThrough')">STRIKETHROUGH</button>
<button class="toolbar" v-on:click.prevent="stylize('superscript')">SUPERSCRIPT</button>
<button class="toolbar" v-on:click.prevent="stylize('subscript')">SUBSCRIPT</button>
<button class="toolbar" v-on:click.prevent="stylize('indent')">INDENT</button>
<button class="toolbar" v-on:click.prevent="stylize('outdent')">OUTDENT</button>
<button class="toolbar" v-on:click.prevent="stylize('insertHorizontalRule')">HORIZONTAL LINE</button>
<button class="toolbar" v-on:click.prevent="stylize('insertParagraph')">INSERT PARAGRAPH</button>
<button class="toolbar" v-on:click.prevent="stylize('selectAll')">SELECT ALL</button>
<button class="toolbar" v-on:click.prevent="stylize('removeFormat')">REMOVE FORMAT</button>
<button class="toolbar" v-on:click.prevent="stylize('undo')">UNDO</button>
<button class="toolbar" v-on:click.prevent="stylize('redo')">REDO</button>
<contenteditable ref="con" :key="index" v-on:delete-row="deleteThisRow(index)" v-for="(item, index) in content" :value.sync="item.value"></contenteditable>
<pre>
{{content}}
</pre>
</div>
我想我可能会做出贡献,因为我不觉得给定的解决方案是最优雅或最简洁的,无法清楚地回答需要什么,或者它们没有提供 Vue 的最佳使用。有些接近了,但最终需要一些调整才能真正有效。
首先注意,<p>
段落不支持 v-model。内容位于 innerHTML 中,仅在元素槽内使用 {{content}}
添加。该内容在插入后不会被编辑。您可以为其提供初始内容,但每次刷新内容时,内容编辑光标都会重置到前面(不是自然的打字体验)。这引出了我的最终解决方案:
...
<p class="m-0 p-3" :contenteditable="manage" @input="handleInput">
{{ content }}
</p>
...
props: {
content: {type:String,defalut:"fill content"},
manage: { type: Boolean, default: false },
...
data: function() {
return {
bioContent: this.content
...
methods: {
handleInput: function(e) {
this.bioContent = e.target.innerHTML.replace(/(?:^(?: )+)|(?:(?: )+$)/g, '');
},
...
我的建议是,将初始静态内容值放入 <p>
槽中,然后使用 @input
触发器更新第二个 active 内容从 contenteditable 动作中放入 innerHTML 的变量。您还需要 trim 结束由 <p>
元素创建的 HTML 格式 whitespace,否则如果您有一个 space。
如果有其他更有效的解决方案,我不知道,但欢迎提出建议。这就是我用于我的代码的内容,我相信它的性能将满足我的需求。
我正在尝试制作一个类似于 Medium 的文本编辑器。我正在使用一个内容可编辑的段落标记,并将每个项目存储在一个数组中,并使用 v-for 呈现每个项目。但是,我在使用 v-model 将文本与数组绑定时遇到了问题。似乎与 v-model 和 contenteditable 属性 有冲突。这是我的代码:
<div id="editbar">
<button class="toolbar" v-on:click.prevent="stylize('bold')">Bold</button>
</div>
<div v-for="(value, index) in content">
<p v-bind:id="'content-'+index" v-bind:ref="'content-'+index" v-model="content[index].value" v-on:keyup="emit_content($event)" v-on:keyup.delete="remove_content(index)" contenteditable></p>
</div>
在我的脚本中:
export default {
data() {
return {
content: [{ value: ''}]
}
},
methods: {
stylize(style) {
document.execCommand(style, false, null);
},
remove_content(index) {
if(this.content.length > 1 && this.content[index].value.length == 0) {
this.content.splice(index, 1);
}
}
}
}
我还没有在网上找到任何答案。
我试了一个例子,eslint-plugin-vue reported that v-model
isn't supported on p
elements. See the valid-v-model规则。
截至撰写本文时,Vue 似乎并未直接支持您想要的内容。我将介绍两个通用解决方案:
直接在可编辑元素上使用输入事件
<template>
<p
contenteditable
@input="onInput"
>
{{ content }}
</p>
</template>
<script>
export default {
data() {
return { content: 'hello world' };
},
methods: {
onInput(e) {
console.log(e.target.innerText);
},
},
};
</script>
创建可重复使用的可编辑组件
Editable.vue
<template>
<p
ref="editable"
contenteditable
v-on="listeners"
/>
</template>
<script>
export default {
props: {
value: {
type: String,
default: '',
},
},
computed: {
listeners() {
return { ...this.$listeners, input: this.onInput };
},
},
mounted() {
this.$refs.editable.innerText = this.value;
},
methods: {
onInput(e) {
this.$emit('input', e.target.innerText);
},
},
};
</script>
index.vue
<template>
<Editable v-model="content" />
</template>
<script>
import Editable from '~/components/Editable';
export default {
components: { Editable },
data() {
return { content: 'hello world' };
},
};
</script>
针对您的特定问题的自定义解决方案
经过多次迭代,我发现对于您的用例,通过 而不是 使用单独的组件更容易获得可行的解决方案。 contenteditable
元素似乎非常棘手 - 特别是在列表中呈现时。我发现我必须在删除后手动更新每个 p
的 innerText
才能使其正常工作。我还发现使用 ids 有效,但使用 refs 无效。
可能有一种方法可以在模型和内容之间实现完整的双向绑定,但我认为这需要在每次更改后操纵光标位置。
<template>
<div>
<p
v-for="(value, index) in content"
:id="`content-${index}`"
:key="index"
contenteditable
@input="event => onInput(event, index)"
@keyup.delete="onRemove(index)"
/>
</div>
</template>
<script>
export default {
data() {
return {
content: [
{ value: 'paragraph 1' },
{ value: 'paragraph 2' },
{ value: 'paragraph 3' },
],
};
},
mounted() {
this.updateAllContent();
},
methods: {
onInput(event, index) {
const value = event.target.innerText;
this.content[index].value = value;
},
onRemove(index) {
if (this.content.length > 1 && this.content[index].value.length === 0) {
this.$delete(this.content, index);
this.updateAllContent();
}
},
updateAllContent() {
this.content.forEach((c, index) => {
const el = document.getElementById(`content-${index}`);
el.innerText = c.value;
});
},
},
};
</script>
我昨天弄明白了!解决了这个解决方案。我基本上只是通过更新任何可能的事件并通过手动分配具有动态引用的相应元素重新渲染来手动跟踪 content
数组中的 innerHTML,例如content-0
, content-1
,... 效果很好:
<template>
<div id="editbar">
<button class="toolbar" v-on:click.prevent="stylize('bold')">Bold</button>
</div>
<div>
<div v-for="(value, index) in content">
<p v-bind:id="'content-'+index" class="content" v-bind:ref="'content-'+index" v-on:keydown.enter="prevent_nl($event)" v-on:keyup.enter="add_content(index)" v-on:keyup.delete="remove_content(index)" contenteditable></p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
content: [{
html: ''
}]
}
},
methods: {
add_content(index) {
//append to array
},
remove_content(index) {
//first, check some edge conditions and remove from array
//then, update innerHTML of each element by ref
for(var i = 0; i < this.content.length; i++) {
this.$refs['content-'+i][0].innerHTML = this.content[i].html;
}
},
stylize(style){
document.execCommand(style, false, null);
for(var i = 0; i < this.content.length; i++) {
this.content[i].html = this.$refs['content-'+i][0].innerHTML;
}
}
}
}
</script>
我想我可能想出了一个更简单的解决方案。请参阅下面的代码段:
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head>
<body>
<main id="app">
<div class="container-fluid">
<div class="row">
<div class="col-8 bg-light visual">
<span class="text-dark m-0" v-html="content"></span>
</div>
<div class="col-4 bg-dark form">
<button v-on:click="bold_text">Bold</button>
<span class="bg-light p-2" contenteditable @input="handleInput">Change me!</span>
</div>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
new Vue({
el: '#app',
data: {
content: 'Change me!',
},
methods: {
handleInput: function(e){
this.content = e.target.innerHTML
},
bold_text: function(){
document.execCommand('bold')
}
}
})
</script>
</body>
</html>
解释:
您可以编辑跨度,因为我已经添加了标签 contenteditable
。请注意,在 input
上,我将调用 handleInput 函数,它将内容的 innerHtml 设置为您插入到可编辑范围中的任何内容。然后,要添加 加粗 功能,您只需 select 您想要加粗的内容并单击加粗按钮即可。
额外奖励!它也适用于 cmd+b ;)
希望这对某人有所帮助!
编码愉快
请注意,我通过 CDN 引入了 bootstrap css 样式和 vue,以便它可以在代码段中运行。
您可以使用组件 v-model 在 Vue 中创建 contentEditable。
Vue.component('editable', {
template: `<p
v-bind:innerHTML.prop="value"
contentEditable="true"
@input="updateCode"
@keyup.ctrl.delete="$emit('delete-row')"
></p>`,
props: ['value'],
methods: {
updateCode: function($event) {
//below code is a hack to prevent updateDomProps
this.$vnode.child._vnode.data.domProps['innerHTML'] = $event.target.innerHTML;
this.$emit('input', $event.target.innerHTML);
}
}
});
new Vue({
el: '#app',
data: {
len: 3,
content: [{
value: 'paragraph 1'
},
{
value: 'paragraph 2'
},
{
value: 'paragraph 3'
},
]
},
methods: {
stylize: function(style, ui, value) {
var inui = false;
var ivalue = null;
if (arguments[1]) {
inui = ui;
}
if (arguments[2]) {
ivalue = value;
}
document.execCommand(style, inui, ivalue);
},
createLink: function() {
var link = prompt("Enter URL", "https://codepen.io");
document.execCommand('createLink', false, link);
},
deleteThisRow: function(index) {
this.content.splice(index, 1);
},
add: function() {
++this.len;
this.content.push({
value: 'paragraph ' + this.len
});
},
}
});
<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
<div id="app">
<button class="toolbar" v-on:click.prevent="add()">ADD PARAGRAPH</button>
<button class="toolbar" v-on:click.prevent="stylize('bold')">BOLD</button>
<button class="toolbar" v-on:click.prevent="stylize('italic')">ITALIC</button>
<button class="toolbar" v-on:click.prevent="stylize('justifyLeft')">LEFT ALIGN</button>
<button class="toolbar" v-on:click.prevent="stylize('justifyCenter')">CENTER</button>
<button class="toolbar" v-on:click.prevent="stylize('justifyRight')">RIGHT ALIGN</button>
<button class="toolbar" v-on:click.prevent="stylize('insertOrderedList')">ORDERED LIST</button>
<button class="toolbar" v-on:click.prevent="stylize('insertUnorderedList')">UNORDERED LIST</button>
<button class="toolbar" v-on:click.prevent="stylize('backColor',false,'#FFFF66')">HEIGHLIGHT</button>
<button class="toolbar" v-on:click.prevent="stylize('foreColor',false,'red')">RED TEXT</button>
<button class="toolbar" v-on:click.prevent="createLink()">CREATE LINK</button>
<button class="toolbar" v-on:click.prevent="stylize('unlink')">REMOVE LINK</button>
<button class="toolbar" v-on:click.prevent="stylize('formatBlock',false,'H1')">H1</button>
<button class="toolbar" v-on:click.prevent="stylize('underline')">UNDERLINE</button>
<button class="toolbar" v-on:click.prevent="stylize('strikeThrough')">STRIKETHROUGH</button>
<button class="toolbar" v-on:click.prevent="stylize('superscript')">SUPERSCRIPT</button>
<button class="toolbar" v-on:click.prevent="stylize('subscript')">SUBSCRIPT</button>
<button class="toolbar" v-on:click.prevent="stylize('indent')">INDENT</button>
<button class="toolbar" v-on:click.prevent="stylize('outdent')">OUTDENT</button>
<button class="toolbar" v-on:click.prevent="stylize('insertHorizontalRule')">HORIZONTAL LINE</button>
<button class="toolbar" v-on:click.prevent="stylize('insertParagraph')">INSERT PARAGRAPH</button>
<button class="toolbar" v-on:click.prevent="stylize('formatBlock',false,'BLOCKQUOTE')">BLOCK QUOTE</button>
<button class="toolbar" v-on:click.prevent="stylize('selectAll')">SELECT ALL</button>
<button class="toolbar" v-on:click.prevent="stylize('removeFormat')">REMOVE FORMAT</button>
<button class="toolbar" v-on:click.prevent="stylize('undo')">UNDO</button>
<button class="toolbar" v-on:click.prevent="stylize('redo')">REDO</button>
<editable v-for="(item, index) in content" :key="index" v-on:delete-row="deleteThisRow(index)" v-model="item.value"></editable>
<pre>
{{content}}
</pre>
</div>
您可以使用 watch 方法创建两种方式绑定 contentEditable。
Vue.component('contenteditable', {
template: `<p
contenteditable="true"
@input="update"
@focus="focus"
@blur="blur"
v-html="valueText"
@keyup.ctrl.delete="$emit('delete-row')"
></p>`,
props: {
value: {
type: String,
default: ''
},
},
data() {
return {
focusIn: false,
valueText: ''
}
},
computed: {
localValue: {
get: function() {
return this.value
},
set: function(newValue) {
this.$emit('update:value', newValue)
}
}
},
watch: {
localValue(newVal) {
if (!this.focusIn) {
this.valueText = newVal
}
}
},
created() {
this.valueText = this.value
},
methods: {
update(e) {
this.localValue = e.target.innerHTML
},
focus() {
this.focusIn = true
},
blur() {
this.focusIn = false
}
}
});
new Vue({
el: '#app',
data: {
len: 4,
val: "Test",
content: [{
"value": "<h1>Heading</h1><div><hr id=\"null\"></div>"
},
{
"value": "<span style=\"background-color: rgb(255, 255, 102);\">paragraph 1</span>"
},
{
"value": "<font color=\"#ff0000\">paragraph 2</font>"
},
{
"value": "<i><b>paragraph 3</b></i>"
},
{
"value": "<blockquote style=\"margin: 0 0 0 40px; border: none; padding: 0px;\"><b>paragraph 4</b></blockquote>"
}
]
},
methods: {
stylize: function(style, ui, value) {
var inui = false;
var ivalue = null;
if (arguments[1]) {
inui = ui;
}
if (arguments[2]) {
ivalue = value;
}
document.execCommand(style, inui, ivalue);
},
createLink: function() {
var link = prompt("Enter URL", "https://codepen.io");
document.execCommand('createLink', false, link);
},
deleteThisRow: function(index) {
this.content.splice(index, 1);
if (this.content[index]) {
this.$refs.con[index].$el.innerHTML = this.content[index].value;
}
},
add: function() {
++this.len;
this.content.push({
value: 'paragraph ' + this.len
});
},
}
});
<script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
<div id="app">
<button class="toolbar" v-on:click.prevent="add()">ADD PARAGRAPH</button>
<button class="toolbar" v-on:click.prevent="stylize('bold')">BOLD</button>
<button class="toolbar" v-on:click.prevent="stylize('italic')">ITALIC</button>
<button class="toolbar" v-on:click.prevent="stylize('justifyLeft')">LEFT ALIGN</button>
<button class="toolbar" v-on:click.prevent="stylize('justifyCenter')">CENTER</button>
<button class="toolbar" v-on:click.prevent="stylize('justifyRight')">RIGHT ALIGN</button>
<button class="toolbar" v-on:click.prevent="stylize('insertOrderedList')">ORDERED LIST</button>
<button class="toolbar" v-on:click.prevent="stylize('insertUnorderedList')">UNORDERED LIST</button>
<button class="toolbar" v-on:click.prevent="stylize('backColor',false,'#FFFF66')">HEIGHLIGHT</button>
<button class="toolbar" v-on:click.prevent="stylize('foreColor',false,'red')">RED TEXT</button>
<button class="toolbar" v-on:click.prevent="createLink()">CREATE LINK</button>
<button class="toolbar" v-on:click.prevent="stylize('unlink')">REMOVE LINK</button>
<button class="toolbar" v-on:click.prevent="stylize('formatBlock',false,'H1')">H1</button>
<button class="toolbar" v-on:click.prevent="stylize('formatBlock',false,'BLOCKQUOTE')">BLOCK QUOTE</button>
<button class="toolbar" v-on:click.prevent="stylize('underline')">UNDERLINE</button>
<button class="toolbar" v-on:click.prevent="stylize('strikeThrough')">STRIKETHROUGH</button>
<button class="toolbar" v-on:click.prevent="stylize('superscript')">SUPERSCRIPT</button>
<button class="toolbar" v-on:click.prevent="stylize('subscript')">SUBSCRIPT</button>
<button class="toolbar" v-on:click.prevent="stylize('indent')">INDENT</button>
<button class="toolbar" v-on:click.prevent="stylize('outdent')">OUTDENT</button>
<button class="toolbar" v-on:click.prevent="stylize('insertHorizontalRule')">HORIZONTAL LINE</button>
<button class="toolbar" v-on:click.prevent="stylize('insertParagraph')">INSERT PARAGRAPH</button>
<button class="toolbar" v-on:click.prevent="stylize('selectAll')">SELECT ALL</button>
<button class="toolbar" v-on:click.prevent="stylize('removeFormat')">REMOVE FORMAT</button>
<button class="toolbar" v-on:click.prevent="stylize('undo')">UNDO</button>
<button class="toolbar" v-on:click.prevent="stylize('redo')">REDO</button>
<contenteditable ref="con" :key="index" v-on:delete-row="deleteThisRow(index)" v-for="(item, index) in content" :value.sync="item.value"></contenteditable>
<pre>
{{content}}
</pre>
</div>
我想我可能会做出贡献,因为我不觉得给定的解决方案是最优雅或最简洁的,无法清楚地回答需要什么,或者它们没有提供 Vue 的最佳使用。有些接近了,但最终需要一些调整才能真正有效。
首先注意,<p>
段落不支持 v-model。内容位于 innerHTML 中,仅在元素槽内使用 {{content}}
添加。该内容在插入后不会被编辑。您可以为其提供初始内容,但每次刷新内容时,内容编辑光标都会重置到前面(不是自然的打字体验)。这引出了我的最终解决方案:
...
<p class="m-0 p-3" :contenteditable="manage" @input="handleInput">
{{ content }}
</p>
...
props: {
content: {type:String,defalut:"fill content"},
manage: { type: Boolean, default: false },
...
data: function() {
return {
bioContent: this.content
...
methods: {
handleInput: function(e) {
this.bioContent = e.target.innerHTML.replace(/(?:^(?: )+)|(?:(?: )+$)/g, '');
},
...
我的建议是,将初始静态内容值放入 <p>
槽中,然后使用 @input
触发器更新第二个 active 内容从 contenteditable 动作中放入 innerHTML 的变量。您还需要 trim 结束由 <p>
元素创建的 HTML 格式 whitespace,否则如果您有一个 space。
如果有其他更有效的解决方案,我不知道,但欢迎提出建议。这就是我用于我的代码的内容,我相信它的性能将满足我的需求。