参考文章:微信小程序之实现封装一个富文本编辑器 Editor 的完整流程【附demo源码】欢迎点赞收藏
地址:https://blog.csdn.net/XH_jing/article/details/115509316
demo源码: https://github.com/jxh1997/Editor ,所以源代码均在 Github 上,下载即可使用。
我个人在demo源码的基础上稍微进行了微调。
官方文档:
editor(富文本编辑器,可以对图片、文字进行编辑):
https://developers.weixin.qq.com/miniprogram/dev/component/editor.html
rich-text(富文本):
https://developers.weixin.qq.com/miniprogram/dev/component/rich-text.html
一、组件定义
组件的目录结构如下:
richText.js
const supportDateFormat = ['YY-MM', 'YY.MM.DD', 'YY-MM-DD', 'YY/MM/DD', 'YY.MM.DD HH:MM', 'YY/MM/DD HH:MM', 'YY-MM-DD HH:MM']; //支持的日期格式 Component({ /** * 组件的属性列表 */ properties: { //编辑器是否只读 readOnly: { type: Boolean, value: false }, //编辑器默认提示语 placeholder: { type: String, value: '开始编辑吧...' }, //插入的日期格式 formatDate: { type: String, value: 'YY/MM/DD' }, buttonTxt: { type: String, value: '保存' } }, /** * 组件的初始数据 */ data: { formats: {}, //样式集合 textTool: false, //文本工具是否显示,默认隐藏 }, /** * 组件的方法列表 */ methods: { //富文本工具点击事件 toolEvent(res) { let { tool_name } = res.currentTarget.dataset; switch (tool_name) { case 'insertImage': //插入图片 this.insertImageEvent(); break; case 'showTextTool': //展示文字编辑工具 this.showTextTool(); break; case 'insertDate': //插入日期 this.insertDate(); break; case 'undo': //撤退(向前) this.undo(); break; case 'redo': //撤退(向后) this.restore(); break; case 'clear': //清除 this.clearBeforeEvent(); break; } }, //编辑器初始化完成时触发 onEditorReady() { console.log('编辑器初始化完成时触发') this.triggerEvent('onEditorReady'); // 返回一个 SelectorQuery 对象实例。在自定义组件或包含自定义组件的页面中,应使用this.createSelectorQuery()来代替。 // https://developers.weixin.qq.com/miniprogram/dev/api/wxml/wx.createSelectorQuery.html this.createSelectorQuery().select('#editor').context(res => { console.log('createSelectorQuery=>', res) this.editorCtx = res.context; let rtTxt = ''; this.setContents(rtTxt); //设置富文本内容 }).exec(); }, //设置富文本内容 setContents(rechtext) { this.editorCtx.setContents({ html: rechtext, success: res => { console.log('[setContents success]', res) } }) }, //撤销 undo() { this.editorCtx.undo(); this.triggerEvent('undo'); }, //恢复 restore() { this.editorCtx.redo(); this.triggerEvent('restore'); }, /** * 修改样式,样式item点击事件 * @param {String} name 样式名称 * @param {String} value 样式值 */ format(res) { let { name, value } = res.target.dataset; if (!name) return; this.editorCtx.format(name, value); }, // 通过 Context 方法改变编辑器内样式时触发,返回选区已设置的样式 onStatusChange(res) { const formats = res.detail; console.log('onStatusChange=>',res) this.setData({ formats }) }, //在光标位置插入下换线 insertDivider() { this.editorCtx.insertDivider({ success: res => { console.log('[insert divider success]', res) } }) }, //清空编辑器内容 clear() { this.editorCtx.clear({ success: res => { this.triggerEvent('clearSuccess'); } }) }, //清空编辑器内容前的事件 clearBeforeEvent() { this.triggerEvent('clearBeforeEvent'); }, //清除当前选区的样式 removeFormat() { this.editorCtx.removeFormat(); }, //插入日期 insertDate() { if (supportDateFormat.indexOf(this.data.formatDate) < 0) { console.error(`Format Date ${this.data.formatDate} error \n It should be one of them [${supportDateFormat}]`) return; } let formatDate = this.getThisDate(this.data.formatDate); this.editorCtx.insertText({ text: formatDate }) }, //插入图片事件 insertImageEvent() { //触发父组件选择图片方法 this.triggerEvent('insertImageEvent', {}); }, /** * 插入图片方法 * @param {String} path 图片地址,仅支持 http(s)、base64、云图片(2.8.0)、临时文件(2.8.3) */ insertImageMethod(path) { return new Promise((resolve, reject) => { this.editorCtx.insertImage({ src: path, data: { id: 'imgage', }, success: res => { resolve(res); }, fail: res => { reject(res); } }) }) }, //保存按钮事件,获取编辑器内容 getEditorContent() { this.editorCtx.getContents({ success: res => { // console.log('[getContents rich text success]', res) this.triggerEvent('getEditorContent', { value: res, }); } }) }, //show文本工具栏 showTextTool() { this.setData({ textTool: !this.data.textTool }) }, //编辑器聚焦时触发 bindfocus(res) { this.triggerEvent('bindfocus', { value: res, }); }, //编辑器失去焦点时触发 bindblur(res) { this.triggerEvent('bindblur', { value: res, }); }, //编辑器输入中时触发 bindinput(res) { this.triggerEvent('bindinput', { value: res, }); }, /** * 返回当前日期 * @format {String} 需要返回的日期格式 */ getThisDate(format) { let date = new Date(), year = date.getFullYear(), month = date.getMonth() + 1, day = date.getDate(), h = date.getHours(), m = date.getMinutes(); //数值补0方法 const zero = (value) => { if (value < 10) return '0' + value; return value; } switch (format) { case 'YY-MM': return year + '-' + zero(month); case 'YY.MM.DD': return year + '.' + zero(month) + '.' + zero(day); case 'YY-MM-DD': return year + '-' + zero(month) + '-' + zero(day); case 'YY.MM.DD HH:MM': return year + '.' + zero(month) + '.' + zero(day) + ' ' + zero(h) + ':' + zero(m); case 'YY/MM/DD HH:MM': return year + '/' + zero(month) + '/' + zero(day) + ' ' + zero(h) + ':' + zero(m); case 'YY-MM-DD HH:MM': return year + '-' + zero(month) + '-' + zero(day) + ' ' + zero(h) + ':' + zero(m); default: return year + '/' + zero(month) + '/' + zero(day); } } }, /** * 组件生命周期函数-在组件布局完成后执行) */ ready() { }, })
richText.json
{ "component": true, "usingComponents": {} }
richText.wxml
<view class="whole" id="richText"> <view style="height:{{textTool?'200':'30'}}rpx;"></view> <view class="editor-toolbar" bindtap="format"> <view class="toolbar-2"> <view class="tool-item-cell"> <view class="tool-item-box"> <view class="cell-rg-shadow"></view> <scroll-view scroll-x class="flex-sb" style="height:70rpx;white-space: nowrap;"> <view class="tool-item"> <i class="iconfont icon-charutupian" data-tool_name='insertImage' bindtap="toolEvent"></i> </view> <view class="tool-item"> <i class="iconfont icon-font" data-tool_name='showTextTool' bindtap="toolEvent"></i> </view> <view class="tool-item"> <i class="iconfont icon-format-header-1 {{formats.header === 1 ? 'ql-active' : ''}}" data-tool_name='text_H1' data-name="header" data-value="{{1}}" bindtap="toolEvent"></i> </view> <view class="tool-item"> <i class="iconfont icon-date" data-tool_name='insertDate' bindtap="toolEvent"></i> </view> <view class="tool-item"> <i class="iconfont icon-undo" data-tool_name='undo' bindtap="toolEvent"></i> </view> <view class="tool-item"> <i class="iconfont icon-redo" data-tool_name='redo' bindtap="toolEvent"></i> </view> <view class="tool-item"> <i class="iconfont icon-shanchu" data-tool_name='clear' bindtap="toolEvent"></i> </view> </scroll-view> </view> </view> <lable class='save-icon' style='background:{{appColorConfig.check_color}}' bindtap="getEditorContent"> {{buttonTxt}} </lable> </view> <view class="toolbar-1" wx:if="{{textTool}}"> <scroll-view scroll-x style="height:70rpx;white-space: nowrap;"> <view class="tool-item"> <i class="iconfont icon-zitijiacu {{formats.bold ? 'ql-active' : ''}}" data-name="bold"></i> </view> <view class="tool-item"> <i class="iconfont icon-zitixieti {{formats.italic ? 'ql-active' : ''}}" data-name="italic"></i> </view> <view class="tool-item"> <i class="iconfont icon-zitixiahuaxian {{formats.underline ? 'ql-active' : ''}}" data-name="underline"></i> </view> <view class="tool-item"> <i class="iconfont icon-fengexian" bindtap='insertDivider'></i> </view> <view class="tool-item"> <i class="iconfont icon-zuoduiqi {{formats.align === 'left' ? 'ql-active' : ''}}" data-name="align" data-value="left"></i> </view> <view class="tool-item"> <i class="iconfont icon-juzhongduiqi {{formats.align === 'center' ? 'ql-active' : ''}}" data-name="align" data-value="center"></i> </view> <view class="tool-item"> <i class="iconfont icon-youduiqi {{formats.align === 'right' ? 'ql-active' : ''}}" data-name="align" data-value="right"></i> </view> <view class="tool-item"> <i class="iconfont icon-zuoyouduiqi {{formats.align === 'justify' ? 'ql-active' : ''}}" data-name="align" data-value="justify"></i> </view> <view class="tool-item"> <i class="iconfont icon--checklist" data-name="list" data-value="check"></i> </view> <view class="tool-item"> <i class="iconfont icon-youxupailie {{formats.list === 'ordered' ? 'ql-active' : ''}}" data-name="list" data-value="ordered"></i> </view> <view class="tool-item"> <i class="iconfont icon-wuxupailie {{formats.list === 'bullet' ? 'ql-active' : ''}}" data-name="list" data-value="bullet"></i> </view> </scroll-view> </view> </view> <view class="page-body"> <view class='wrapper'> <editor id="editor" class="ql-container" placeholder="{{placeholder}}" showImgSize showImgToolbar showImgResize bindstatuschange="onStatusChange" read-only="{{readOnly}}" bindready="onEditorReady" bindfocus='bindfocus' bindblur='bindblur' bindinput='bindinput'> </editor> </view> </view> </view>
richText.wxss
/* components/richText/richText.wxss */ @import "./assets/iconfont.wxss"; page { background: #f8f8f8; } .page-body{ padding-bottom: 10rpx; } .editor-toolbar { /* position: fixed; top: 0; left: 0; */ width: 100%; z-index: 9999; margin-bottom: 30rpx; } .editor-toolbar i { display: flex; align-items: center; justify-content: center; } .toolbar-1 { padding: 5rpx 0; background: #e4e4e4; } .editor-toolbar .tool-item { display: inline-block; } .toolbar-2 { padding: 5rpx 20px 5rpx 10px; background: #f4f4f4; display: flex; align-items: center; justify-content:space-between; position: relative; } .toolbar-2 .tool-item-cell{ max-width: 80%; } .toolbar-2 .tool-item-box{ position: relative; } .toolbar-2 .cell-rg-shadow{ position: absolute; right: 0; top: 0; width: 1px; height: 100%; z-index: 999; background:#dddddd; } .iconfont { display: inline-block; padding: 8px 8px; width: 24px; height: 24px; cursor: pointer; font-size: 20px; } .toolbar { box-sizing: border-box; border-bottom: 0; font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; } .ql-container { box-sizing: border-box; padding: 12px 15px; width: 100%; min-height: 30vh; height: auto; background: #fff; font-size: 16px; line-height: 1.5; } .ql-active { color: #06c; } .save-icon { padding: 15rpx 30rpx; font-size: 20rpx; background: #bf98d2; color: #fff; } .flex{ display: flex; } .flex-cc{ display: flex; align-items: center; -ms-flex-item-align: center; justify-content: center; } .flex-sb{ display: flex; align-items: center; -ms-flex-item-align: center; justify-content: space-between; } .flex-sa{ display: flex; align-items: center; -ms-flex-item-align: center; justify-content: space-around; }
二、组件使用
index.js
const app = getApp(); let richText = null; //富文本编辑器实例 Page({ /** * 页面的初始数据 */ data: { readOnly: false, //编辑器是否只读 placeholder: '开始编辑吧...', }, /** * 生命周期函数--监听页面加载 */ onLoad(options) { }, /** * 生命周期函数--监听页面显示 */ onShow: function () { }, // 编辑器初始化完成时触发,可以获取组件实例 onEditorReady() { console.log('[onEditorReady callback]') richText = this.selectComponent('#richText'); //获取组件实例 }, //设置富文本内容 setContents(rechtext) { this.editorCtx.setContents({ html: rechtext, success: res => { console.log('[setContents success]', res) } }) }, //撤销 undo() { console.log('[undo callback]') }, //恢复 restore() { console.log('[restore callback]') }, //清空编辑器内容 clear() { this.editorCtx.clear({ success: res => { console.log("[clear success]", res) } }) }, //清空编辑器事件 clearBeforeEvent(){ console.log('[clearBeforeEvent callback]') wx.showModal({ cancelText: '取消', confirmText: '确认', content: '确认清空编辑器内容吗?', success: (result) => { if(result.confirm){ richText.clear(); } }, fail: (res) => {}, }) }, //清空编辑器成功回调 clearSuccess(){ console.log('[clearSuccess callback]') }, //清除当前选区的样式 removeFormat() { this.editorCtx.removeFormat(); }, //插入图片 insertImageEvent() { wx.chooseImage({ count: 1, success: res => { let path = res.tempFilePaths[0]; //调用子组件方法,图片应先上传再插入,不然预览时无法查看图片。 richText.insertImageMethod(path).then(res => { console.log('[insert image success callback]=>', res) }).catch(res => { console.log('[insert image fail callback]=>', res) }); } }) }, //保存,获取编辑器内容 getEditorContent(res) { let { value } = res.detail; wx.showToast({ title: '获取编辑器内容成功', icon: 'none', }) console.log('[getEditorContent callback]=>', value) }, //show文本工具栏 showTextTool() { this.setData({ textTool: !this.data.textTool }) }, //编辑器聚焦时触发 bindfocus(res) { let { value } = res.detail; // console.log('[bindfocus callback]=>', value) }, //编辑器失去焦点时触发 bindblur(res) { let { value } = res.detail; // console.log('[bindblur callback]=>', value) }, //编辑器输入中时触发 bindinput(res) { let { value } = res.detail; // console.log('[bindinput callback]=>', value) app.data.richTextContents = value.detail.html; }, //预览富文本 preview(){ wx.navigateTo({ url: `../preview/preview`, }) } })
富文本插入图片时将图片先保存到服务器再预览,修改insertImageEvent方法如下所示:
//插入图片 insertImageEvent() { wx.chooseImage({ count: 1,//选择图片的数量 默认为9 success: res => { console.log(res) let path = res.tempFilePaths[0]; //tempFilePaths 图片的本地临时文件路径列表 wx.uploadFile({ url: app.globalData.serverApi+"/appletApi/fileUpload/appletRichTextImgUpload", filePath: path,//要上传文件资源的路径 name: 'file', header: { 'content-type': 'application/json', 'Authorization': wx.getStorageSync('token') }, formData:{ 'table': 't_applet_product', 'column': 'productDescImg' }, success: (resp) => { var result = JSON.parse(resp.data); var relativePath = result.data.replace("\\","/") var path1 = app.globalData.staticApi+relativePath //调用子组件方法,图片应先上传再插入,不然预览时无法查看图片。 richText.insertImageMethod(path1).then(res => { console.log('[insert image success callback]=>', res) }).catch(res => { console.log('[insert image fail callback]=>', res) }); } }); } }) },
index.wxml
<view class="row3" style="height: 700rpx"> <richText class="richText" id='richText' readOnly='{{readOnly}}' placeholder='{{placeholder}}' formatDate='YY/MM/DD' buttonTxt='保存' bind:clearBeforeEvent='clearBeforeEvent' bind:clearSuccess='clearSuccess' bind:undo='undo' bind:restore='restore' bind:onEditorReady='onEditorReady' bind:bindfocus='bindfocus' bind:bindblur='bindblur' bind:bindinput='bindinput' bind:insertImageEvent='insertImageEvent' bind:getEditorContent='getEditorContent'></richText> <view class="preview" bindtap="preview">预览</view> </view>
index.json
{ "usingComponents": { "richText":"../../components/richText/richText" } }
index.wxss
.preview{ width: 20%; height: 80rpx; line-height: 80rpx; margin: 0 auto; margin-bottom: 50rpx; text-align: center; font-size:26rpx; color: #ffffff; background-color: #bf98d2; float: right; }
效果如下:
添加一张图片
三、预览页面
preview.js
const app = getApp(); Page({ /** * 页面的初始数据 */ data: { }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, /** * 生命周期函数--监听页面显示 */ onShow: function () { }, onEditorReady() { wx.createSelectorQuery().select('#editor').context(res => { this.editorCtx = res.context; this.editorCtx.setContents({ html: app.globalData.richTextContents, success: res => { console.log('[setContents success]') } }) }).exec() } })
preview.json
{ "usingComponents": {}, "navigationBarTitleText": "预览富文本" }
preview.wxml
<view style="padding:20rpx;"> <editor id="editor" style="height: 1200rpx;" read-only bindready="onEditorReady"></editor> </view>
preview.wxss
.editor { box-sizing: border-box; padding: 12px 15px; width: 100%; min-height: 10vh; height: auto; background: #fff; font-size: 24rpx; line-height: 1.5; }
预览效果如下:
四、修改时回显富文本
1、从后台获取富文本,
// 该方法用于修改 getProductInfoById: function(){ var that = this; request.postParam("/api/product/getById",that.QueryParams,function(res){ console.log(res) if(res.data.success){ let result = res.data.data that.setData({ richTextContents: result.productDesc }) console.log(that.data.richTextContents) that.setData({ material: result }) } }); },
2、在富文本中将富文本内容传递到子组件
<richText class="richText" id='richText' readOnly='{{readOnly}}' placeholder='{{placeholder}}' content= '{{richTextContents}}' formatDate='YY/MM/DD' buttonTxt='保存' bind:clearBeforeEvent='clearBeforeEvent' bind:clearSuccess='clearSuccess' bind:undo='undo' bind:restore='restore' bind:onEditorReady='onEditorReady' bind:bindfocus='bindfocus' bind:bindblur='bindblur' bind:bindinput='bindinput' bind:insertImageEvent='insertImageEvent' bind:getEditorContent='getEditorContent'></richText>
3、子组件接收父组件传递过来的content
properties: { //编辑器是否只读 readOnly: { type: Boolean, value: false }, //编辑器默认提示语 placeholder: { type: String, value: '开始编辑吧...' }, //插入的日期格式 formatDate: { type: String, value: 'YY/MM/DD' }, buttonTxt: { type: String, value: '保存' }, content: { type: String, value: '' } },
4、在onEditorReady中设置富文本内容
//编辑器初始化完成时触发 onEditorReady() { console.log('编辑器初始化完成时触发') this.triggerEvent('onEditorReady'); // 返回一个 SelectorQuery 对象实例。在自定义组件或包含自定义组件的页面中,应使用this.createSelectorQuery()来代替。 // https://developers.weixin.qq.com/miniprogram/dev/api/wxml/wx.createSelectorQuery.html this.createSelectorQuery().select('#editor').context(res => { console.log('createSelectorQuery=>', res) this.editorCtx = res.context; let rtTxt = ''; if(this.data.content!=''){ this.setContents(this.data.content); //设置富文本内容 } else{ this.setContents(rtTxt); //设置富文本内容 } }).exec(); },
五、微信小程序 如何控制rich-text中图片的宽度
修改之前,图片要么小了,要么大了
使用rich-text展示富文本内容时,富文本的图片自带样式超出屏幕或者不是我们想要的大小时,我们可以使用replace给他添加额外样式,来控制图片大小
对后台返回的数据进行处理
result.productDesc = result.productDesc.replace(/<img/g, '<img class="richImg"');
这个时候直接设置richImg样式就行
.richImg{ width: 100% !important; height: auto !important; }
修改之后: