uniapp富文本editor输入二次扩展兼容微信小程序

简介: uniapp富文本editor输入二次扩展兼容微信小程序

在uni-app中开发富文本输入功能,并使其兼容微信小程序,需要注意一些特定的限制和解决方案。由于微信小程序本身对HTML的支持有限,直接在小程序中实现像Web那样完整的富文本编辑功能(如使用CKEditor、Quill等)是不可能的。但你可以通过一些方法来实现基本的富文本输入或近似功能。

富文本编辑器,可以对图片、文字格式进行编辑和混排。

在web开发时,可以使用contenteditable来实现内容编辑。但这是一个dom API,在非H5平台无法使用。于是微信小程序和uni-app的App-vue提供了editor组件来实现这个功能,并且在uni-app的H5平台也提供了兼容。从技术本质来讲,这个组件仍然运行在视图层webview中,利用的也是浏览器的contenteditable功能。


编辑器导出内容支持带标签的 html和纯文本的 text,编辑器内部采用 delta 格式进行存储。


通过setContents接口设置内容时,解析插入的 html 可能会由于一些非法标签导致解析错误,建议开发者在应用内使用时通过 delta 进行插入。

组件扩展

<template>
  <view class="diygw-col-24">
    <view :style="{height:height}" class='flex  flex-direction-column wrapper'>
      <view class='toolbar' @tap="format">
        <view v-if="tools.indexOf('undo')>-1" class="iconfont icon-undo" @tap="undo"></view>
        <view v-if="tools.indexOf('redo')>-1" class="iconfont icon-redo" @tap="redo"></view>
        <view v-if="tools.indexOf('bold')>-1" :class="formats.bold ? 'ql-active' : ''" class="iconfont icon-zitijiacu" data-name="bold"></view>
        <view v-if="tools.indexOf('italic')>-1" :class="formats.italic ? 'ql-active' : ''" class="iconfont icon-zitixieti" data-name="italic"></view>
        <view v-if="tools.indexOf('underline')>-1" :class="formats.underline ? 'ql-active' : ''" class="iconfont icon-zitixiahuaxian" data-name="underline"></view>
        <view v-if="tools.indexOf('strike')>-1" :class="formats.strike ? 'ql-active' : ''" class="iconfont icon-zitishanchuxian" data-name="strike"></view>
        <view v-if="tools.indexOf('align-left')>-1" :class="formats.align === 'left' ? 'ql-active' : ''" class="iconfont icon-zuoduiqi" data-name="align" data-value="left"></view>
        <view v-if="tools.indexOf('align-center')>-1" :class="formats.align === 'center' ? 'ql-active' : ''" class="iconfont icon-juzhongduiqi" data-name="align" data-value="center"></view>
        <view v-if="tools.indexOf('align-right')>-1" :class="formats.align === 'right' ? 'ql-active' : ''" class="iconfont icon-youduiqi" data-name="align" data-value="right"></view>
        <view v-if="tools.indexOf('align-justify')>-1" :class="formats.align === 'justify' ? 'ql-active' : ''" class="iconfont icon-zuoyouduiqi" data-name="align" data-value="justify"></view>
        <view v-if="tools.indexOf('lineHeight')>-1" :class="formats.lineHeight ? 'ql-active' : ''" class="iconfont icon-line-height" data-name="lineHeight" data-value="2"></view>
        <view v-if="tools.indexOf('letterSpacing')>-1" :class="formats.letterSpacing ? 'ql-active' : ''" class="iconfont icon-Character-Spacing" data-name="letterSpacing" data-value="2em"></view>
        <view v-if="tools.indexOf('marginTop')>-1" :class="formats.marginTop ? 'ql-active' : ''" class="iconfont icon-722bianjiqi_duanqianju" data-name="marginTop" data-value="20px"></view>
        <view v-if="tools.indexOf('previewarginBottom')>-1" :class="formats.previewarginBottom ? 'ql-active' : ''" class="iconfont icon-723bianjiqi_duanhouju" data-name="marginBottom" data-value="20px"></view>
        <view v-if="tools.indexOf('removeFormat')>-1" class="iconfont icon-clearedformat" @tap="removeFormat"></view>
        <view v-if="tools.indexOf('fontFamily')>-1" :class="formats.fontFamily ? 'ql-active' : ''" class="iconfont icon-font" data-name="fontFamily" data-value="仿宋, 仿宋_GB2312"></view>
      
        <!-- <picker v-if="tools.indexOf('fontSize')>-1" :range="fontSizelist" @change="formatsChange" @tap.stop="formatsChange" data-name="size" class="iconfont icon-fontsize" :class="formats.size? ' ql-active' : ''"></picker> -->
        <picker v-if="tools.indexOf('fontSize')>-1" range-key="name" :range="fontSizelist" @change="formatsChange" @tap.stop="formatsChange" data-name="fontSize" class="iconfont icon-fontsize" :class="formats.fontSize? ' ql-active' : ''"></picker>
        <!-- <view v-if="tools.indexOf('fontSize')>-1" :class="formats.fontSize === '24px' ? 'ql-active' : ''" class="iconfont icon-fontsize" data-name="fontSize" data-value="24px"></view> -->
        <view v-if="tools.indexOf('color')>-1" :style="(formats.color != '#FFFFFF'&&formats.color != '#fff'&&formats.color != '#ffffff')? 'color:' + formats.color : ''" class="iconfont icon-text_color" data-name="color" @tap.stop="openColor"></view>
        <view v-if="tools.indexOf('backgroundColor')>-1" :style="(formats.backgroundColor != '#FFFFFF'&&formats.backgroundColor != '#fff'&&formats.backgroundColor != '#ffffff') ? 'color:' + formats.backgroundColor : ''" class="iconfont icon-fontbgcolor" data-name="backgroundColor" @tap.stop="openColor"></view>
 
        <view v-if="tools.indexOf('insertDate')>-1" class="iconfont icon-date" @tap="insertDate"></view>
        <view v-if="tools.indexOf('list')>-1" class="iconfont icon--checklist" data-name="list" data-value="check"></view>
        <view v-if="tools.indexOf('ordered')>-1" :class="formats.list === 'ordered' ? 'ql-active' : ''" class="iconfont icon-youxupailie" data-name="list" data-value="ordered"></view>
        <view v-if="tools.indexOf('bullet')>-1" :class="formats.list === 'bullet' ? 'ql-active' : ''" class="iconfont icon-wuxupailie" data-name="list" data-value="bullet"></view>
        
        <view v-if="tools.indexOf('indent-reduce')>-1" class="iconfont icon-outdent" data-name="indent" data-value="-1"></view>
        <view v-if="tools.indexOf('indent-add')>-1" class="iconfont icon-indent" data-name="indent" data-value="+1"></view>
        <view v-if="tools.indexOf('insert-divider')>-1" class="iconfont icon-fengexian" @tap="insertDivider"></view>
        <view v-if="tools.indexOf('insert-image')>-1" class="iconfont icon-charutupian" @tap="selectImage"></view>
        <picker v-if="tools.indexOf('header')>-1" :range="headerlist" @change="formatsChange" @tap.stop="formatsChange" data-name="header" :class="'iconfont icon-format-header-'+(headerindex==0?1:headerindex)+(formats.header? ' ql-active' : '')"></picker>
        <!-- <view v-if="tools.indexOf('header')>-1" :class="formats.header === 1 ? 'ql-active' : ''" class="iconfont icon-format-header-1" data-name="header" :data-value="3"></view> -->
        <view v-if="tools.indexOf('script-sub')>-1" :class="formats.script === 'sub' ? 'ql-active' : ''" class="iconfont icon-zitixiabiao" data-name="script" data-value="sub"></view>
        <view v-if="tools.indexOf('script-super')>-1" :class="formats.script === 'super' ? 'ql-active' : ''" class="iconfont icon-zitishangbiao" data-name="script" data-value="super"></view>
        <view v-if="tools.indexOf('direction')>-1" :class="formats.direction === 'rtl' ? 'ql-active' : ''" class="iconfont icon-direction-rtl" data-name="direction" data-value="rtl"></view>
        <view v-if="tools.indexOf('clear')>-1" class="iconfont icon-shanchu" @tap="clear"></view>
      
      </view>
      <view class="flex-sub editor-wrapper">
        <editor id="editor" class="ql-container" :placeholder="placeholder" showImgSize showImgToolbar showImgResize @statuschange="onStatusChange" :read-only="readOnly" @ready="onEditorReady" @input="editorChange">
        </editor>
      </view>
    </view>
    <block v-if="modal.show">
      <view class="mask" />
      <view class="modal">
        <view class="modal_title">{{modal.title}}</view>
        <input type="text" class="modal_input" v-model="modal.value" />
        <view class="modal_foot">
          <view class="modal_button" @tap="modalCancel">取消</view>
          <view class="modal_button" style="color:#576b95;border-left:1px solid rgba(0,0,0,.1)" @tap="modalConfirm">确定</view>
        </view>
      </view>
    </block>
    <diy-color-picker v-model="showColorPicker" :hexcolor="hexcolor" @confirm="getColor"></diy-color-picker>
  </view>
</template>
 
<script>
  import Emitter from "../../libs/util/emitter.js";
  
  export default {
    mixins: [Emitter],
    emits: ["update:modelValue", "change"],
    props: {
      value: {
        type: String
      },
      modelValue:{
        type: String
      },
      placeholder: {
        type: String,
        default: '开始输入...'
      },
      height:{
        type:String,
        default: '100vh'
      },
      tools: {
        type: Array,
        default: function() {
          return [
            'bold',
            'italic',
            'underline',
            'strike',
            'align-left',
            'align-center',
            'align-right',
            'align-justify',
            'lineHeight',
            'letterSpacing',
            'marginTop',
            'previewarginBottom',
            'removeFormat',
            'fontFamily',
            'fontSize',
            'color',
            'backgroundColor',
            'insertDate',
            'list',
            'ordered',
            'bullet',
            'redo',
            'undo',
            'indent-reduce',
            'indent-add',
            'insert-divider',
            'insert-image',
            'header',
            'script-sub',
            'script-super',
            'clear',
            'direction'
          ];
        }
      },
      //上传图片
      action:{
        type:String,
        default: '/sys/storage/upload'
      }/*,
      uploadFile: {
        type: Function
      }*/
    },
    data() {
      return {
        modal: {
          show: false,
          title: '',
          value: ''
        },
        showColorPicker:false,
        html: '',
        fontSizelist: [{
          code: "",
          name: "默认"
        }, {
          code: "x-small",
          name: "超小"
        }, {
          code: "small",
          name: "小"
        }, {
          code: "medium",
          name: "中等"
        }, {
          code: "large",
          name: "大"
        }, {
          code: "x-large",
          name: "超大"
        }, {
          code: "xx-large",
          name: "超级大"
        }],
        headerlist: ['默认', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'],
        headerindex: 0,
        colorPickerName: '',
        hexcolor: "#0000ff",
        readOnly: false,
        formats: {},
        update: 0,
        uForm:{
          inputAlign: "",
          clearable: ""
        }
      }
    },
    watch: {
      value: function(newval) {
        this.html = newval
      },
      modelValue: function(newval) {
        this.html = newval
      },
      html: function(newvar) {
        if (this.editorCtx) {
          if (this.update == 0) {
            this.editorCtx.setContents({
              html: this.html
            });
          } else {
            this.update = 0
          }
        }
      }
    },
    created() {
      this.html = this.value;
    },
    mounted() {
      let parent = this.$u.$parent.call(this, 'u-form');
      if (parent) {
        Object.keys(this.uForm).map(key => {
          this.uForm[key] = parent[key];
        });
      }
    },
    methods: {
      openColor(e) {
        let dataset = e.target.dataset
        this.colorPickerName = dataset.name;
        this.hexcolor = dataset.value;
        this.showColorPicker = true
        // this.$refs.colorPicker.open();
      },
      getColor(e) {
        let msg = '';
        switch (this.colorPickerName) {
          case 'backgroundColor':
            if (e.hex.toUpperCase() == '#FFFFFF') {
              e.hex = '';
            }
            msg = '背景色';
            break;
          case 'color':
            msg = '颜色';
            break;
        }
        this.setformat(this.colorPickerName, e.hex, msg + e.hex);
      },
 
      modalConfirm() {
        let src = this.modal.value || '';
        if (src) {
          this.insertImage(src, null, null)
        }
        this.modal.show = false;
      },
      modalCancel() {
        this.modal.show = false;
      },
 
      formatsChange(e) {
        if (e.type == 'click') { //不让上层触发点击事件
          return false;
        }
 
        let value = e.detail.value;
        let name = e.target.dataset.name
        if (name == 'header') {
          this.headerindex = value;
          if (value == 0) {
            value = null;
          }
        } else if (name == 'fontSize') {
          value = this.fontSizelist[value].code;
        } else if (name == 'size') {
          value = value > 0 ? value : 1;
        }
        let msg = name + '设置成功';
        console.log(value);
        this.setformat(name, value, msg)
        return false;
      },
      editorChange(e) {
        this.update = 1
        this.$emit('input', e.detail.html);
        
        this.$emit("update:modelValue", e.detail.html);
        
        // vue 原生的方法 return 出去
        this.$emit("change", e.detail.html);
        // 将当前的值发送到 u-form-item 进行校验
        this.dispatch("u-form-item", "onFieldBlur", e.detail.html);
      },
      readOnlyChange() {
        this.readOnly = !this.readOnly
      },
      onEditorReady() {
        const query = uni.createSelectorQuery().in(this);
        query.select('#editor').context((res) => {
          this.editorCtx = res.context
          if (this.html) {
            this.editorCtx.setContents({
              html: this.html
            });
          }
        }).exec()
      },
      undo() {
        this.editorCtx.undo()
      },
      redo() {
        this.editorCtx.redo()
      },
      format(e) {
        let {
          name,
          value
        } = e.target.dataset
        if (!name) return
        // console.log('format', name, value)
        this.editorCtx.format(name, value)
 
      },
      setformat(name, value, msg) {
        this.editorCtx.format(name, value);
        // this.toast(msg);
      },
      toast(msg) {
        uni.showToast({
          duration: 600,
          icon: 'none',
          title: msg
        });
      },
      onStatusChange(e) {
        const formats = e.detail
        this.formats = formats
      },
      insertDivider() {
        this.editorCtx.insertDivider({
          success: function() {
            console.log('insert divider success')
          }
        })
      },
      clear() {
        uni.showModal({
          content: "确定清空编辑器内容?",
          complete: (rs) => {
            if (rs.confirm) {
              this.editorCtx.clear({
                success: function(res) {
                  console.log("clear success")
                }
              })
            }
          }
        })
      },
      removeFormat() {
        this.editorCtx.removeFormat()
      },
      insertDate() {
        const date = new Date()
        let month = date.getMonth() + 1
        if(month<10){
          month = "0" +month
        }
        let day = date.getDate()
        if(day<10){
          day = "0" + day
        }
        const formatDate = `${date.getFullYear()}-${month}-${day}`
        this.editorCtx.insertText({
          text: formatDate
        })
      },
      selectImage() {
        let thiz = this
        // 本地选取 自已处理上传方法,包括选择文件
        uni.chooseImage({
          count: 9,
          sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
          sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有javascript:;
          success: function (res) {
            // 返回选定照片的本地文件路径列表,tempFilePath可以作为img标签的src属性显示图片
            let tempFilePaths = res.tempFilePaths;
            for (let i = 0; i < tempFilePaths.length; i++) {
              let header = {}
              if(getApp().globalData.currentPage && getApp().globalData.currentPage.$session){
                header.Authorization =  getApp().globalData.currentPage.$session.getToken()||''
              }
              uni.uploadFile({
                url: getApp().globalData.currentPage && getApp().globalData.currentPage.$http?getApp().globalData.currentPage.$http.setUrl(thiz.action,{}):thiz.action,
                filePath: tempFilePaths[i],
                name: 'file',
                header:header,
                success(res) {
                  let data = getApp().globalData.currentPage.$tools.fromJson(res.data);
                  let url = ''
                  if(data.url){
                    url = getApp().globalData.currentPage.$tools.renderImage(data.url);
                  }
                  if(data.data &&getApp().globalData.currentPage.$tools.isObject(data.data) && data.data.url){
                    url = getApp().globalData.currentPage.$tools.renderImage(data.data.url);
                  }
                  if(url){
                    thiz.insertImage(url,null,null);
                  }
                }
              });
            }
          },
        });
        // uni.showActionSheet({
        //  itemList: ['本地选取', '远程链接'],
        //  success: res => {
        //    if (res.tapIndex === 0) {
              
        //    } else {
        //      thiz.modal = {
        //        show: true,
        //        title: '图片链接',
        //        value: ''
        //      }
        //    }
        //  }
        // })
      },
      insertImage(src, data, alt) {
        debugger
        let inserdata = {
          src: src
        }
        if (data) {
          inserdata.data = data
        }
        if (alt) {
          inserdata.alt = alt
        }
        this.editorCtx.insertImage({
          ...inserdata,
          success: function() {
            console.log('insert image success')
          }
        })
      }
    }
  }
</script>
 
<style>
  @import "./editor-icon.css";
 
  .container {
    width: 100%;
  }
 
  .wrapper {
    width: 100%;
  }
 
  .editor-wrapper {
    width: 100%;
    background: #fff;
  }
 
  .iconfont {
    display: inline-block;
    padding: 8px 8px;
    cursor: pointer;
    font-size: 25px;
  }
 
  .toolbar {
    box-sizing: border-box;
    border-bottom: 0;
    font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
  }
 
  .ql-container {
    box-sizing: border-box;
    padding: 10px;
    width: 100%;
    min-height: 30vh;
    height: 100%;
    font-size: 16px;
    line-height: 1.5;
  }
 
  .ql-active {
    color: #06c;
  }
 
  /* 模态框 */
  .modal {
    position: fixed;
    z-index: 999999;
    top: 50%;
    left: 16px;
    right: 16px;
    background-color: #fff;
    border-radius: 12px;
    transform: translateY(-50%);
  }
 
  .modal_title {
    padding: 32px 24px 16px;
    font-size: 17px;
    font-weight: 700;
    text-align: center;
  }
 
  .modal_input {
    display: block;
    padding: 5px;
    line-height: 2.5em;
    height: 2.5em;
    margin: 0 24px 32px 24px;
    font-size: 14px;
    border: 1px solid #dfe2e5;
  }
 
  .modal_foot {
    display: flex;
    line-height: 56px;
    font-weight: 700;
    border-top: 1px solid rgba(0, 0, 0, .1);
  }
 
  .modal_button {
    flex: 1;
    text-align: center;
  }
 
  /* 遮罩版 */
  .mask {
    position: fixed;
    z-index: 99999;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background-color: black;
    opacity: 0.5;
  }
</style>

组件调用

<template>
  <view class="container container329152">
    <u-form-item :borderBottom="false" class="diygw-col-24" labelPosition="top" prop="editor">
      <diy-editor height="500px" v-model="editor"></diy-editor>
    </u-form-item>
    <view class="clearfix"></view>
  </view>
</template>
 
<script>
  export default {
    data() {
      return {
        //用户全局信息
        userInfo: {},
        //页面传参
        globalOption: {},
        //自定义全局变量
        globalData: {},
        editor: ''
      };
    },
    onShow() {
      this.setCurrentPage(this);
    },
    onLoad(option) {
      this.setCurrentPage(this);
      if (option) {
        this.setData({
          globalOption: this.getOption(option)
        });
      }
 
      this.init();
    },
    methods: {
      async init() {},
      // 新增方法 自定义方法
      async testFunction(param) {
        let thiz = this;
        console.log(this.checkbox);
      }
    }
  };
</script>
 
<style lang="scss" scoped>
  .container329152 {
  }
</style>



目录
相关文章
|
2月前
|
Web App开发 JavaScript 前端开发
用来用去还是用回了ueditor-Vue富文本编辑器二次扩展
用来用去还是用回了ueditor-Vue富文本编辑器二次扩展
39 11
|
2月前
|
移动开发 小程序 数据可视化
基于npm CLI脚手架的uniapp项目创建、运行与打包全攻略(微信小程序、H5、APP全覆盖)
基于npm CLI脚手架的uniapp项目创建、运行与打包全攻略(微信小程序、H5、APP全覆盖)
278 3
|
2月前
|
数据挖掘
uniapp uview扩展u-picker支持日历期间 年期间 月期间 时分期间组件
uniapp uview扩展u-picker支持日历期间 年期间 月期间 时分期间组件
103 10
|
2月前
|
移动开发 小程序 数据可视化
一招学会DIY官网可视化设计支持导出微擎、UNIAPP、H5、微信小程序源码
一招学会DIY官网可视化设计支持导出微擎、UNIAPP、H5、微信小程序源码
44 2
|
2月前
|
小程序 数据可视化 API
低代码可视化-uniapp商城首页小程序-代码生成器
低代码可视化-uniapp商城首页小程序-代码生成器
31 0
|
2月前
|
小程序
uniapp实现微信小程序隐私协议组件封装
uniapp实现微信小程序隐私协议组件封装
50 0
|
5月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的房屋租赁App的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的房屋租赁App的详细设计和实现(源码+lw+部署文档+讲解等)
131 7
基于SpringBoot+Vue+uniapp的房屋租赁App的详细设计和实现(源码+lw+部署文档+讲解等)
|
5月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的汉服交易小程序的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的汉服交易小程序的详细设计和实现(源码+lw+部署文档+讲解等)
63 7
|
5月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的宠物医院微信小程序的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的宠物医院微信小程序的详细设计和实现(源码+lw+部署文档+讲解等)
87 7
|
5月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的武汉市公交路线查询系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的武汉市公交路线查询系统的详细设计和实现(源码+lw+部署文档+讲解等)
117 7