contenteditable Selection 在vue中实现自定义表达式

contenteditable 相关介绍

实现效果展示

1576141347534.gif
contenteditable 属性规定是否可编辑元素的内容

属性值

  • true 可以编辑元素内容
  • false 无法编辑元素内容
  • classname 继承父元素的 contenteditable 属性
<div class="flex" contenteditable="true"></div>

Selection属性一览

https://developer.mozilla.org/zh-CN/docs/Web/API/Selection

widnow.getSelection()

返回一个 Selection 对象,表示用户选择的文本范围或光标的当前位置

Section.getRangeAt()

返回选区开始的节点(Node)

const sel = window.getSelection();
const range = sel.getRangeAt(0);

光标所在位置追加元素代码

var sel, range;
var textContent;
//失去焦点时获取光标的位置
sel = window.getSelection();

// 先获取一下焦点
$('.flex').focus()
$('.flex').blur(function() {
  getblur()
})

function getblur() {
  sel = window.getSelection();
  range = sel.getRangeAt(0);
}

// 光标所在位置追加元素
function insertHtmlAtCaret(html) {
  if (window.getSelection) {
    // IE9 and non-IE
    if (sel.getRangeAt && sel.rangeCount) {
      var el = document.createElement("div");
      el.innerHTML = html;
      var frag = document.createDocumentFragment(),
          node,
          lastNode;
      while ((node = el.firstChild)) {
        lastNode = frag.appendChild(node);
      }
      range.insertNode(frag); // Preserve the selection
      if (lastNode) {
        range = range.cloneRange();
        range.setStartAfter(lastNode);
        range.collapse(true);
        sel.removeAllRanges();
        sel.addRange(range);
      }
    }
  } else if (document.selection && document.selection.type != "Control") {
    // IE < 9
    document.selection.createRange().pasteHTML(html);
  }
  textContent = $(".flex").html();
}

文字生成图片

/**
* js使用canvas将文字转换成图像数据base64
* @param {string}    text              文字内容  "abc"
* @param {string}    fontsize          文字大小  20
* @param {function}  fontcolor         文字颜色  "#000"
* @param {boolean}   imgBase64Data     图像数据
*/
export const textBecomeImg = (text,fontsize,fontcolor) => {
  var canvas = document.createElement('canvas');
  var $buHeight = 0;
  if(fontsize <= 32){ $buHeight = 1; }
  else if(fontsize > 32 && fontsize <= 60 ){ $buHeight = 2;}
  else if(fontsize > 60 && fontsize <= 80 ){ $buHeight = 4;}
  else if(fontsize > 80 && fontsize <= 100 ){ $buHeight = 6;}
  else if(fontsize > 100 ){ $buHeight = 10;}
  canvas.height=fontsize + $buHeight ;
  var context = canvas.getContext('2d');
  context.clearRect(0, 0, canvas.width, canvas.height);
  context.fillStyle = fontcolor;
  context.font=fontsize+"px Arial";
  context.textBaseline = 'middle';
  context.fillText(text,5,fontsize/2)

  canvas.width = context.measureText(text).width;
  context.fillStyle = fontcolor;
  context.font=14+"px Arial";
  context.textAlign="left";
  context.textBaseline = 'middle';
  context.fillText(text,5,fontsize/2)

  var dataUrl = canvas.toDataURL('image/png');
  return dataUrl;
}

剩下的就是jq了 jquery大法好

this.$nextTick(() => {
  $('.collapse-item').click(function() {
    const { name, key } = JSON.parse($(this).attr('data'))
    let htmlStr = "";
    const tempKey = "this.paramsKey." + key;
    const url = textBecomeImg(name, 20, "#fff");
    htmlStr = `<img data-key="${ tempKey }" style="background: #3296fa;margin: 0 10px 10px 0;" src="${url}">`;
    insertHtmlAtCaret(htmlStr)
  })
})

注意: 项目本身是vue工程,但是该功能需要各种操作dom,所以就在组件里见面vue与jq结合使用了

整个组件代码

方案改过一版,代码还未进行优化,因为代码里面使用了一些项目本身的数据,所以整套逻辑只能做参考了

  • html
<template>
  <el-card>
    <div class="hide-show">
      <div class="hide-show-left">
        <el-collapse v-model="activeNames" @change="handleChange">
          <el-collapse-item
            v-for="(item, index) in collapseData"
            :key="index"
            title="当前表单"
            name="1"
          >
            <p v-for="val in item.list" :key="val.key" class="collapse-item" :data="JSON.stringify(val)">{{ val.name }}</p>
          </el-collapse-item>
        </el-collapse>
      </div>
      <div class="hide-show-right">
        <div class="hide-show-right-box">
          <div class="flex" contenteditable="true"></div>
        </div>
        <div class="hide-show-right-bottom">                                                                                                           
          <p class="title">满足以上条件字段隐藏</p>
          <ul>
            <li class="li-item">请从左侧面板选择字段或选项</li>
            <li class="li-item">
              支持
              <span style="color: red;">英文</span>模式下运算符(+, -, *, /, >, &#60;, ==, !=, &#60;&#61;, >=)
            </li>
            <li class="li-item">
              <p>参考场景:</p>
              <p>总金额控件输入的值小于10000时,需要隐藏当前控件,则可将隐藏条件设置为: 总金额 &#60; 100</p>
            </li>
          </ul>
        </div>
      </div>
    </div>
  </el-card>
</template>
  • js
<script>
import { textBecomeImg } from "./utils.js";
export default {
  name: "hide-show",
  props: {
    // 当前组件dataInfo
    dataInfo: {
      type: Object,
      default: () => {}
    },

    // 画布里面已经拖拽的并且key存在的组件
    data: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      activeNames: ["1"],
      collapseData: [
        {
          title: "当前表单",
          type: 1,
          list: []
        }
      ],
      paramsKey: {}
    };
  },
  methods: {
    // 格式化数据方法
    handleFormatData() {
      const list = [];
      const params = {};
      this.data.forEach(item => {
        // 区分 栅格 标签页 子表 这几类布局组件
        if (item.type === "grid") {
          // 栅格
          item.columns.forEach(val => {
            val.list.forEach(v => {
              if (v.model && v.model != "") {
                this.$set(this.paramsKey, v.model, v.value);
                list.push({
                  name: v.name,
                  type: 1,
                  key: v.model
                });
              }
            });
          });
        } else if (item.type === "tabs") {
          // 标签页
          item.tab.forEach(val => {
            val.columns.forEach(v => {
              if (v.type === "grid") {
                v.columns.forEach(col => {
                  col.list.forEach(l => {
                    if (l.model && l.model != "") {
                      this.$set(this.paramsKey, l.model, l.value);
                      list.push({
                        name: l.name,
                        type: 1,
                        key: l.model
                      });
                    }
                  });
                });
              } else {
                if (v.model && v.model != "") {
                  this.$set(this.paramsKey, v.model, v.value);
                  list.push({
                    name: v.name,
                    type: 1,
                    key: v.model
                  });
                }
              }
            });
          });
        } else if (item.type === "childrenTable") {
          // 子表
          // 子表不能作为控制显示隐藏的条件
          console.log("item: ", item);
        } else {
          // 普通组件
          if (item.model && item.model != "") {
            this.$set(this.paramsKey, item.model, item.value);
            list.push({
              name: item.name,
              type: 1,
              key: item.model
            });
          }
        }
      });
      this.collapseData[0]["list"] = list.filter(
        val => val.key != this.dataInfo.model
      );
    },

    handleChange(val) {},

    // 清空
    handleClearExpression() {
      $(".flex").empty();
    },

    // 获取最终表达式
    handleGetExpression() {
      let m = ''
      let str = $('.flex').prop("outerHTML")
      // 谷歌浏览器如果开启翻译功能会导致生成font标签包裹文本
      str = str.replace(/<font style="vertical-align: inherit;">/g, '')
      str = str.replace(/<\/font>/g, '')
      str = str.replace(/<div contenteditable="true" type="parent" class="flex">/g, '')
      str = str.replace(/<\/div>/g, '')
      $('.flex').empty()
      $('.flex').html(str)
      Array.from($('.flex').contents()).forEach(val => {
        if ($(val).attr('data-key')) {
          m = m + $(val).attr('data-key')
        } else {
          m = m + $(val).text()
        }
      })

      try {
        eval(m);
        if (
          this.dataInfo.options &&
          typeof this.dataInfo.options !== "string"
        ) {
          this.dataInfo.options.js_content = m;
          this.dataInfo.options.htmlStr = $(".flex").prop("outerHTML");
        } else {
          this.dataInfo.options = {};
          this.dataInfo.options.js_content = m;
          this.dataInfo.options.htmlStr = $(".flex").prop("outerHTML");
        }
        this.$emit("colse-dailog");
        return str;
      } catch (err) {
        console.log(err);
        this.$message.error("配置的表达式不合法,请检查!");
      }
    }
  },
  mounted() {
    const _this = this;
    $(".hide-show-right-box").html(_this.dataInfo.options.htmlStr);
    $(".flex").keydown(function(e) {
      if (e.keyCode == 13) {
        e.preventDefault();
      }
    });
    this.handleFormatData();
    sel = window.getSelection();
    var sel, range;
    var textContent;

    $('.flex').focus()
    $('.flex').blur(function() {
      getblur()
    })

    function getblur() {
      range = sel.getRangeAt(0);
    }

    // 光标所在位置追加元素
    function insertHtmlAtCaret(html) {
      if (window.getSelection) {
        // IE9 and non-IE
        if (sel.getRangeAt && sel.rangeCount) {
          var el = document.createElement("div");
          el.innerHTML = html;
          var frag = document.createDocumentFragment(),
            node,
            lastNode;
          while ((node = el.firstChild)) {
            lastNode = frag.appendChild(node);
          }
          range.insertNode(frag); // Preserve the selection
          if (lastNode) {
            range = range.cloneRange();
            range.setStartAfter(lastNode);
            range.collapse(true);
            sel.removeAllRanges();
            sel.addRange(range);
          }
        }
      } else if (document.selection && document.selection.type != "Control") {
        // IE < 9
        document.selection.createRange().pasteHTML(html);
      }
      textContent = $(".flex").html();
    }

    this.$nextTick(() => {
      $('.collapse-item').click(function() {
        const { name, key } = JSON.parse($(this).attr('data'))
        let htmlStr = "";
        const tempKey = "this.paramsKey." + key;
        const url = textBecomeImg(name, 20, "#fff");
        // hspace=15
        htmlStr = `<img data-key="${ tempKey }" style="background: #3296fa;" src="${url}" hspace=15>`;
        insertHtmlAtCaret(htmlStr)
      })
    })
  }
};
</script>
  • css
<style lang="scss">
.hide-show {
  margin: 0 auto;
  width: 700px;
  height: 450px;
  border: 1px solid #e6e6e6;
  border-radius: 8px;
  box-sizing: border-box;
  display: flex;
  justify-content: flex-start;
  .hide-show-left {
    border-right: 1px solid #e6e6e6;
    width: 220px;
    height: 100%;
    background: #e6e6e6;
    padding: 10px;
    border-radius: 8px 0 0 8px;
    overflow: auto;
    box-sizing: border-box;
    .collapse-item {
      text-align: center;
      height: 32px;
      line-height: 32px;
      border-bottom: 1px solid #e6e6e6;
      cursor: pointer;
      margin: 0;
      padding: 0;
    }

    .collapse-item:hover {
      background: #eee;
      height: 32px;
      line-height: 32px;
    }

    .el-collapse-item__header {
      background: #3296fa;
      color: #fff;
      padding-left: 15px;
    }
    .el-collapse-item__content {
      padding-bottom: 0;
    }
  }

  .hide-show-right {
    flex: 1;
    background: #fff;
    border-radius: 8px;
    box-sizing: border-box;
    .hide-show-right-box {
      height: 250px;
      overflow: auto;
      padding: 10px;
      .flex {
        color: #666;
        padding: 20px;
        outline: none;
        line-height: 28px;
        height: 28px;
        font-size: 20px;        
      }
      .flex,  
      .flex * {  
          -webkit-user-select: auto;  
          -webkit-user-modify: read-write;  
      }
      width: 100%;
      padding: 10px;
    }
    .hide-show-right-bottom {
      * {
        margin: 0;
        padding: 0;
      }
      height: 200px;
      .title {
        background: #eed5d2;
        height: 32px;
        line-height: 32px;
        padding-left: 20px;
        margin-bottom: 15px;
        color: #fff;
      }
      ul {
        padding-left: 20px;
        li {
          line-height: 22px;
        }
      }
    }
  }
}
</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值