Vue 2内容可通过v模型编辑

时间:2018-12-22 22:18:22

标签: javascript vue.js contenteditable

我正在尝试制作类似于Medium的文本编辑器。我正在使用内容可编辑的段落标签,并将每个项目存储在数组中,并使用v-for呈现每个项目。但是,我在使用v-model将文本与数组绑定时遇到问题。似乎与v模型和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);
         }
      }
   }
}

我没有在线找到任何答案。

6 个答案:

答案 0 :(得分:4)

我尝试了一个示例,eslint-plugin-vue报告说v-model元素不支持p。请参阅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元素似乎非常棘手-尤其是在列表中呈现时。我发现删除后必须手动更新每个innerText的{​​{1}}才能正常工作。我还发现使用id是有效的,但使用ref却没有。

可能存在一种在模型和内容之间进行完全双向绑定的方法,但是我认为这将需要在每次更改后操纵光标位置。

p

答案 1 :(得分:4)

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

<!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设置为您在可编辑范围中插入的内容。然后,要添加粗体功能,只需选择要粗体的内容,然后单击粗体按钮即可。

增加了奖励!它也适用于cmd + b;)

希望这对某人有帮助!

快乐编码

请注意,我通过CDN引入了用于样式和vue的引导CSS,以便它可以在代码段中起作用。

答案 2 :(得分:1)

我昨天知道了!着眼于这种解决方案。我基本上只是通过更新任何可能的事件来手动跟踪content数组中的innerHTML,并通过使用动态引用手动分配相应的元素(例如)来重新呈现。 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>

答案 3 :(得分:1)

我想我可能会做出贡献,因为我不认为给定的解决方案最清晰明了地回答需要的解决方案,或者它们不能提供Vue的最佳使用。有些接近,但最终需要一些调整才能真正生效。 首先请注意,<p>段不支持v模型。内容在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, '');
    },
...

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

如果有另一个更有效的解决方案,我不知道它,但欢迎提出建议。这就是我在代码中使用的内容,并且我相信它会表现出色并满足我的需求。

答案 4 :(得分:0)

您可以使用组件v模型在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>

答案 5 :(得分:0)

您可以使用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>