javascript Vue 2 contentEditable with v-model

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/53899676/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-29 10:19:05  来源:igfitidea点击:

Vue 2 contentEditable with v-model

javascriptvue.jscontenteditable

提问by Soubriquet

I'm trying to make a text editor similar to Medium. I'm using a content editable paragraph tag and store each item in an array and render each with v-for. However, I'm having problems with binding the text with the array using v-model. Seems like there's a conflict with v-model and the contenteditable property. Here's my code:

我正在尝试制作一个类似于 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>

and in my script:

在我的脚本中:

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);
         }
      }
   }
}

I haven't found any answers online for this.

我还没有在网上找到任何答案。

采纳答案by Soubriquet

I figured it out yesterday! Settled on this solution. I basically just manually keep track of the innerHTML in my contentarray by updating on any possible event and re-rendering by manually assigning the corresponding elements with dynamic refs e.g. content-0, content-1,... Works beautifully:

我昨天想通了!解决了这个解决方案。我基本上只是手动跟踪的innerHTML在我content对任何可能事件和重新渲染阵列通过更新通过手动分配与动态相应的元素裁判例如content-0content-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>

回答by David Weldon

I tried an example, and eslint-plugin-vuereported that v-modelisn't supported on pelements. See the valid-v-modelrule.

我尝试了一个例子,eslint-plugin-vue报告说元素v-model不支持p。请参阅valid-v-model规则。

As of this writing, it doesn't look like what you want is supported in Vue directly. I'll present two generic solutions:

在撰写本文时,Vue 似乎并不直接支持您想要的内容。我将提出两个通用解决方案:

Use input events directly on the editable element

直接在可编辑元素上使用输入事件

<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>


Create a reusable editable component

创建可重用的可编辑组件

Editable.vue

可编辑.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

索引.vue

<template>
  <Editable v-model="content" />
</template>

<script>
import Editable from '~/components/Editable';

export default {
  components: { Editable },
  data() {
    return { content: 'hello world' };
  },
};
</script>


Custom solution for your specific problem

针对您的特定问题的定制解决方案

After a lot of iterations, I found that for your use case it was easier to get a working solution by notusing a separate component. It seems that contenteditableelements are extremely tricky - especially when rendered in a list. I found I had to manually update the innerTextof each pafter a removal in order for it to work correctly. I also found that using ids worked, but using refs didn't.

经过多次迭代,我发现对于您的用例,通过使用单独的组件更容易获得可行的解决方案。contenteditable元素似乎非常棘手 - 特别是在列表中呈现时。我发现我必须在删除后手动更新innerText每个文件p才能正常工作。我还发现使用 ids 有效,但使用 refs 没有。

There's probably a way to get a full two-way binding between the model and the content, but I think that would require manipulating the cursor location after each change.

可能有一种方法可以在模型和内容之间获得完整的双向绑定,但我认为这需要在每次更改后操纵光标位置。

<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>

回答by AlmostPitt

I think I may have come up with an even easier solution. See snippet below:

我想我可能想出了一个更简单的解决方案。见下面的片段:

<!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>

Explanation:

解释:

You can edit the span as I have added the tag contenteditable. Notice that on input, I will call the handleInput function, which sets the innerHtml of the content to whatever you have inserted into the editable span. Then, to add the boldfunctionality, you simply select what you want to be bold and click on the bold button.

您可以编辑跨度,因为我添加了标签contenteditable。请注意,在 上input,我将调用 handleInput 函数,该函数将内容的 innerHtml 设置为您插入到可编辑范围中的任何内容。然后,要添加粗体功能,您只需选择要加粗的内容并单击粗体按钮。

Added bonus! It also works with cmd+b ;)

添加奖金!它也适用于 cmd+b ;)

Hopefully this helps someone!

希望这对某人有所帮助!

Happy coding

快乐编码

Note that I brought in bootstrap css for styling and vue via CDN so that it will function in the snippet.

请注意,我通过 CDN 引入了用于样式和 vue 的 bootstrap css,以便它可以在代码段中运行。

回答by Muthu Kumar

You can use watch method to create two way binding contentEditable.

您可以使用 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/[email protected]/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>

回答by Muthu Kumar

You can Use component v-model to create contentEditable in 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/[email protected]/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>

回答by Marcus Smith

I thought I might contribute because I don't feel that the given solutions are the most elegant or concise to clearly answer what is needed or they don't provide the best use of Vue. Some get close, but ultimately need a bit of tweaking to really be effective. First note, the <p>paragraph does not support v-model. The content is in the innerHTML and is only added using {{content}}inside the element slot. That content is not edited after inserting. You can give it initial content but every time you refresh the content, the content editing cursor gets reset to the front (not a natural typing experience). This leads to my final solution:

我想我可能会有所贡献,因为我不认为给定的解决方案是最优雅或简洁的,可以清楚地回答所需的内容,或者它们没有提供 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(/(?:^(?:&nbsp;)+)|(?:(?:&nbsp;)+$)/g, '');
    },
...

My suggestion is, put in an initial static content value into the <p>slot, then have a @inputtrigger to update a second activecontent variable with what is put into the innerHTML from the contenteditable action. You will also want to trim off the end HTML format whitespace created by the <p>element, otherwise you will get a gross string at the end if you have a space.

我的建议是,将初始静态内容值放入<p>槽中,然后使用@input触发器来更新第二个活动内容变量,其中包含从 contenteditable 操作放入 innerHTML 的内容。您还需要修剪由<p>元素创建的 HTML 格式末尾的空格,否则如果有空格,最后会得到一个粗串。

If there is another, more effective solution, I am not aware of it but I am welcome to suggestions. This is what I have used for my code and I am confident that it will be performant and suit my needs.

如果有其他更有效的解决方案,我不知道,但欢迎提出建议。这就是我在代码中使用的内容,我相信它将是高性能的并且适合我的需求。