富文本编辑器,可以对图片、文字进行编辑。
| Web | 微信小程序 | Android 系统版本 | Android | iOS | iOS(Vapor) | HarmonyOS | HarmonyOS(Vapor) |
|---|---|---|---|---|---|---|---|
| 4.0 | 4.41 | 7.1.1 | 5.08 | 5.08 | 5.11 | 5.08 | 5.08 |
| 名称 | 类型 | 默认值 | 兼容性 | 描述 | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| read-only | boolean | false | 设置编辑器为只读 | |||||||
| placeholder | string | - | 提示信息 | |||||||
| show-img-size | boolean | false | 点击图片时显示图片大小控件 | |||||||
| show-img-toolbar | boolean | false | 点击图片时显示工具栏控件 | |||||||
| show-img-resize | boolean | false | 点击图片时显示修改尺寸控件 | |||||||
| enable-formats | Array.<string> | - | (Array.<string>) 编辑器允许的名单内的格式 | |||||||
| type | "none" | "null" | 输入类型,暂只支持 "none": 聚焦时不弹出键盘 | |||||||
| ||||||||||
| name | string | - | - | 表单的控件名称,作为键值对的一部分与表单(form组件)一同提交 | ||||||
| @ready | (event: UniEvent) => void | - | 编辑器初始化完成时触发 | |||||||
| @focus | (event: UniEvent) => void | - | 编辑器聚焦时触发 | |||||||
| @blur | (event: UniEvent) => void | - | 编辑器失去焦点时触发 | |||||||
| @input | (event: UniEvent) => void | - | 编辑器内容改变时触发 | |||||||
| @statuschange | (event: UniEvent) => void | - | 通过 Context 方法改变编辑器内样式时触发,返回选区已设置的样式 | |||||||
editor组件有上下文对象,api为uni.createEditorContextAsync()。
给editor组件设一个id属性,将id的值传入uni.createEditorContextAsync(),即可得到editor组件的上下文对象,进一步可使用对象上的方法。
示例为hello uni-app x alpha分支,与最新HBuilderX Alpha版同步。与最新正式版同步的master分支示例另见
示例
<template>
<view class="page-root">
<view class="preview-panel">
<view class="preview-header">
<text class="uni-title-text">editor 属性示例</text>
</view>
<view class="preview-state-row">
<text class="preview-state-text">read-only: {{ data.readOnly }}</text>
<!-- #ifndef MP -->
<text class="preview-state-text">type: {{ getTypeLabel() }}</text>
<!-- #endif -->
</view>
<view class="preview-state-row">
<text class="preview-state-text">keyboard height: {{ Math.floor(data.keyboardHeight) }}</text>
<view v-if="showLogs">
<text class="preview-state-text">X: {{ data.editorX }}</text>
<text class="preview-state-text">Y: {{ data.editorY }}</text>
<text class="preview-state-text">width: {{ data.editorWidth }}</text>
<text class="preview-state-text">height: {{ data.editorHeight }}</text>
</view>
</view>
<view class="preview-editor-box">
<editor
v-if="data.showEditor"
id="editor-props-demo"
class="preview-editor"
:read-only="data.readOnly"
:placeholder="data.appliedPlaceholder"
:show-img-size="data.appliedShowImgSize"
:show-img-toolbar="data.appliedShowImgToolbar"
:show-img-resize="data.appliedShowImgResize"
:type="data.editorType"
@ready="onEditorReady"
@focus="onEditorFocus"
@blur="onEditorBlur"
@input="onEditorInput"
@statuschange="onStatusChange"
/>
</view>
<view class="preview-toolbar">
<view class="preview-toolbar-item" @tap="insertSampleText">
<text class="preview-toolbar-icon iconfont">A</text>
</view>
<view class="preview-toolbar-item" @tap="insertSampleImage">
<text class="preview-toolbar-icon iconfont">{{""}}</text>
</view>
<view :class="data.boldActive ? 'preview-toolbar-item preview-toolbar-item-active' : 'preview-toolbar-item'" @tap="toggleBold">
<text :class="data.boldActive ? 'preview-toolbar-icon preview-toolbar-icon-active iconfont' : 'preview-toolbar-icon iconfont'">{{""}}</text>
</view>
<view :class="data.alignCenterActive ? 'preview-toolbar-item preview-toolbar-item-active' : 'preview-toolbar-item'" @tap="setAlignCenter">
<text :class="data.alignCenterActive ? 'preview-toolbar-icon preview-toolbar-icon-active iconfont' : 'preview-toolbar-icon iconfont'">{{""}}</text>
</view>
<view class="preview-toolbar-item" @tap="removeFormat">
<text class="preview-toolbar-icon iconfont">{{""}}</text>
</view>
<view class="preview-toolbar-item" @tap="clearEditor">
<text class="preview-toolbar-icon iconfont">{{""}}</text>
</view>
<view class="preview-toolbar-item" @tap="blurEditor">
<text class="preview-toolbar-icon iconfont">{{""}}</text>
</view>
<view class="preview-toolbar-item" @tap="insertMention">
<text class="preview-toolbar-icon iconfont">@</text>
</view>
</view>
</view>
<scroll-view class="props-scroll" direction="vertical">
<page-intro content="本页演示 editor 的 read-only、placeholder、show-img-size、show-img-toolbar、show-img-resize、type 属性,以及 ready、focus、blur、input、statuschange 事件。placeholder 与 show-img-* 为初始化生效属性,调整后需要点击按钮重建 editor。"></page-intro>
<view class="uni-padding-wrap">
<view class="uni-title uni-common-mt">
<text class="uni-title-text">可动态修改属性</text>
</view>
</view>
<boolean-data :defaultValue="data.readOnly" title="read-only:设置编辑器为只读,可动态修改" @change="onReadOnlyChange"></boolean-data>
<!-- #ifndef MP -->
<enum-data :items="data.typeItems" title="type:null | none,控制聚焦时是否弹出键盘" @change="onTypeChange"></enum-data>
<!-- #endif -->
<view class="uni-padding-wrap">
<view class="uni-title uni-common-mt">
<text class="uni-title-text">初始化生效属性</text>
</view>
<text class="uni-subtitle-text">placeholder 与 show-img-* 调整后不会直接作用到当前 editor,请点击下方按钮重建后查看。</text>
</view>
<input-data :defaultValue="data.draftPlaceholder" title="placeholder:提示信息,不可动态修改" type="text" @confirm="onPlaceholderChange"></input-data>
<boolean-data :defaultValue="data.draftShowImgSize" title="show-img-size:点击图片时显示图片大小控件,不可动态修改" @change="onDraftShowImgSizeChange"></boolean-data>
<boolean-data :defaultValue="data.draftShowImgToolbar" title="show-img-toolbar:点击图片时显示工具栏控件,不可动态修改" @change="onDraftShowImgToolbarChange"></boolean-data>
<boolean-data :defaultValue="data.draftShowImgResize" title="show-img-resize:点击图片时显示修改尺寸控件,不可动态修改" @change="onDraftShowImgResizeChange"></boolean-data>
<view class="uni-padding-wrap uni-common-mt">
<view class="uni-title">
<text class="uni-title-text">当前应用中的 editor 配置</text>
</view>
<text class="uni-subtitle-text">placeholder:{{ data.appliedPlaceholder }}</text>
<text class="uni-subtitle-text">show-img-size:{{ data.appliedShowImgSize }}</text>
<text class="uni-subtitle-text">show-img-toolbar:{{ data.appliedShowImgToolbar }}</text>
<text class="uni-subtitle-text">show-img-resize:{{ data.appliedShowImgResize }}</text>
<button type="primary" size="mini" class="action-button apply-button" @tap="rebuildEditor">重建 editor 并应用初始化属性</button>
</view>
<view v-if="showLogs" class="uni-padding-wrap uni-common-mt">
<view class="uni-title">
<text class="uni-title-text">事件示例</text>
</view>
<text class="uni-subtitle-text">@ready:{{ data.readyCount }}</text>
<text class="uni-subtitle-text">@focus:{{ data.focusCount }}</text>
<text class="uni-subtitle-text">@blur:{{ data.blurCount }}</text>
<text class="uni-subtitle-text">@input:{{ data.inputCount }}</text>
<text class="uni-subtitle-text">@statuschange:{{ data.statusChangeCount }}</text>
<text class="uni-subtitle-text">最近状态:{{ data.statusSummary }}</text>
</view>
<view v-if="showLogs" class="uni-padding-wrap uni-common-mt uni-common-mb">
<view class="uni-title event-header-row">
<text class="uni-title-text">事件日志</text>
<button size="mini" @tap="clearLogs">清空日志</button>
</view>
<view class="log-box" v-if="data.logs.length > 0">
<text v-for="(item, index) in data.logs" :key="index" class="log-text">{{ item }}</text>
</view>
<text v-else class="uni-subtitle-text">等待事件触发...</text>
</view>
</scroll-view>
<navigator style="margin-top: 20px;" url="./editor-edit">
<button>富文本编辑模板</button>
</navigator>
<bottom-safe-area />
</view>
</template>
<script setup lang="uts">
import { ItemType } from '@/components/enum-data/enum-data-types'
type EditorPropsPageData = {
showEditor: boolean
readOnly: boolean
editorType: string | null
draftPlaceholder: string
appliedPlaceholder: string
draftShowImgSize: boolean
appliedShowImgSize: boolean
draftShowImgToolbar: boolean
appliedShowImgToolbar: boolean
draftShowImgResize: boolean
appliedShowImgResize: boolean
editorCtx: EditorContext | null
readyCount: number
focusCount: number
blurCount: number
inputCount: number
statusChangeCount: number
boldActive: boolean
alignCenterActive: boolean
statusSummary: string
logs: string[]
typeItems: ItemType[]
editorX: number
editorY: number
editorWidth: number
editorHeight: number
keyboardHeight: number
keyboardHeightChangeCount: number
}
const SAMPLE_TEXT = '这是 editor 示例文本。你可以继续输入内容,也可以滚动下方属性区实时调整 editor 状态。'
function createTypeItems(selectedValue: number): ItemType[] {
return [
{ value: 0, name: 'null:聚焦弹出键盘', checked: selectedValue == 0 },
{ value: 1, name: 'none:聚焦不弹出键盘', checked: selectedValue == 1 }
]
}
const showLogs = false
const initialData: EditorPropsPageData = {
showEditor: true,
readOnly: false,
editorType: null,
draftPlaceholder: '请输入内容,placeholder 仅初始化生效',
appliedPlaceholder: '请输入内容,placeholder 仅初始化生效',
draftShowImgSize: true,
appliedShowImgSize: true,
draftShowImgToolbar: true,
appliedShowImgToolbar: true,
draftShowImgResize: true,
appliedShowImgResize: true,
editorCtx: null,
readyCount: 0,
focusCount: 0,
blurCount: 0,
inputCount: 0,
statusChangeCount: 0,
boldActive: false,
alignCenterActive: false,
statusSummary: '等待 @statuschange',
logs: [],
typeItems: createTypeItems(0),
editorX: 0,
editorY: 0,
editorWidth: 0,
editorHeight: 0,
keyboardHeight: 0,
keyboardHeightChangeCount: 0
}
const data = reactive<EditorPropsPageData>(initialData)
function appendLog(tag: string, detail: string) {
const entry = `${new Date().toLocaleTimeString()} [${tag}] ${detail}`
data.logs.unshift(entry)
if (data.logs.length > 12) {
data.logs.pop()
}
}
function getTypeLabel(): string {
return data.editorType == 'none' ? 'none(聚焦不弹键盘)' : 'null(聚焦弹键盘)'
}
function summarizeText(text: string): string {
if (text.length <= 24) {
return text
}
return `${text.substring(0, 24)}...`
}
function syncTypeItems() {
const selectedValue = data.editorType == 'none' ? 1 : 0
data.typeItems = createTypeItems(selectedValue)
}
function onReadOnlyChange(checked: boolean) {
data.readOnly = checked
appendLog('prop', `read-only => ${checked}`)
}
function onTypeChange(value: number) {
data.editorType = value == 1 ? 'none' : null
syncTypeItems()
appendLog('prop', `type => ${data.editorType == null ? 'null' : data.editorType}`)
}
function onPlaceholderChange(value: string) {
data.draftPlaceholder = value
appendLog('draft', `placeholder 待应用 => ${data.draftPlaceholder}`)
}
function onDraftShowImgSizeChange(checked: boolean) {
data.draftShowImgSize = checked
appendLog('draft', `show-img-size 待应用 => ${checked}`)
}
function onDraftShowImgToolbarChange(checked: boolean) {
data.draftShowImgToolbar = checked
appendLog('draft', `show-img-toolbar 待应用 => ${checked}`)
}
function onDraftShowImgResizeChange(checked: boolean) {
data.draftShowImgResize = checked
appendLog('draft', `show-img-resize 待应用 => ${checked}`)
}
function rebuildEditor() {
data.appliedPlaceholder = data.draftPlaceholder
data.appliedShowImgSize = data.draftShowImgSize
data.appliedShowImgToolbar = data.draftShowImgToolbar
data.appliedShowImgResize = data.draftShowImgResize
data.editorCtx = null
data.boldActive = false
data.alignCenterActive = false
data.statusSummary = '等待 @statuschange'
data.showEditor = false
setTimeout(() => {
data.showEditor = true
}, 0)
appendLog('rebuild', '已重建 editor,初始化属性已重新应用')
}
function updateEditorRect() {
const editorElement = uni.getElementById('editor-props-demo')
const editorRect = editorElement?.getBoundingClientRect()
if (editorRect != null) {
data.editorX = editorRect.x
data.editorY = editorRect.y
data.editorWidth = editorRect.width
data.editorHeight = editorRect.height
// #ifdef APP-IOS || APP-HARMONY || APP-ANDROID
const systemInfo = uni.getSystemInfoSync()
data.editorY += systemInfo.safeAreaInsets.top + 44
// #endif
}
}
function onEditorReady() {
const options: CreateEditorContextAsyncOptions = {
id: 'editor-props-demo',
success: (context: EditorContext) => {
data.editorCtx = context
data.readyCount += 1
updateEditorRect()
appendLog('ready', '编辑器初始化完成')
},
fail: (error: UniError) => {
appendLog('error', `createEditorContextAsync fail: ${error.errMsg}`)
}
}
uni.createEditorContextAsync(options)
}
function onEditorFocus(event: UniEditorFocusEvent) {
data.focusCount += 1
appendLog('focus', `text=${summarizeText(event.detail.text)}`)
}
function onEditorBlur(event: UniEditorBlurEvent) {
data.blurCount += 1
appendLog('blur', `text=${summarizeText(event.detail.text)}`)
}
function onEditorInput(event: UniEditorInputEvent) {
data.inputCount += 1
appendLog('input', `text=${summarizeText(event.detail.text)}`)
}
function onStatusChange(event: UniEditorStatusChangeEvent) {
data.statusChangeCount += 1
data.boldActive = event.detail.bold == true
data.alignCenterActive = event.detail.align == 'center'
const align = event.detail.align != null ? event.detail.align : 'default'
const fontSize = event.detail.fontSize != null ? event.detail.fontSize : 'default'
data.statusSummary = `bold=${event.detail.bold == true}, align=${align}, fontSize=${fontSize}`
appendLog('statuschange', data.statusSummary)
}
onLoad(() => {
uni.onKeyboardHeightChange((res) => {
data.keyboardHeight = res.height
data.keyboardHeightChangeCount += 1
appendLog('keyboard', `height => ${res.height}`)
})
})
onUnload(() => {
uni.offKeyboardHeightChange()
})
function insertSampleText() {
data.editorCtx?.insertText({
text: SAMPLE_TEXT,
success: () => {
appendLog('action', '已插入示例文本')
}
})
}
function insertSampleImage() {
data.editorCtx?.insertImage({
src: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni-app.png',
alt: '示例图片',
success: () => {
appendLog('action', '已插入示例图片,请点击图片验证 show-img-*')
},
fail: () => {
appendLog('error', '插入示例图片失败')
}
})
}
const insertMention = () => {
data.editorCtx?.insertMention({
id: '000',
name: 'uni-app x',
success: (res: any) => {
console.log('insert mention success')
}
})
}
function toggleBold() {
data.editorCtx?.format('bold', null)
appendLog('action', `请求切换加粗 => ${!data.boldActive}`)
}
function setAlignCenter() {
data.editorCtx?.format('align', 'center')
appendLog('action', '请求设置居中对齐')
}
function removeFormat() {
data.editorCtx?.removeFormat({
success: () => {
appendLog('action', '已清除当前样式')
},
fail: () => {
appendLog('error', '清除样式失败')
}
})
}
function clearEditor() {
data.editorCtx?.clear({
success: () => {
appendLog('action', '已清空编辑器内容')
},
fail: () => {
appendLog('error', '清空编辑器失败')
}
})
}
function blurEditor() {
data.editorCtx?.blur({
success: () => {
appendLog('action', '已主动触发失焦')
},
fail: () => {
appendLog('error', '主动失焦失败')
}
})
}
function hideKeyboardForTest() {
uni.hideKeyboard()
data.keyboardHeight = 0
appendLog('keyboard', '已请求隐藏键盘')
}
function clearLogs() {
data.logs.splice(0, data.logs.length)
}
defineExpose({
data,
getTypeLabel,
onReadOnlyChange,
onTypeChange,
onPlaceholderChange,
onDraftShowImgSizeChange,
onDraftShowImgToolbarChange,
onDraftShowImgResizeChange,
rebuildEditor,
insertSampleText,
insertMention,
insertSampleImage,
toggleBold,
setAlignCenter,
removeFormat,
clearEditor,
blurEditor,
hideKeyboardForTest,
updateEditorRect
})
</script>
<style>
@import './editor-icon.css';
.page-root {
flex-direction: column;
flex: 1;
}
.preview-panel {
flex-direction: column;
padding: 10px;
background-color: #ffffff;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: rgba(0, 0, 0, 0.06);
}
.preview-header {
flex-direction: column;
}
.preview-state-row {
flex-direction: row;
flex-wrap: wrap;
margin-top: 6px;
}
.preview-toolbar {
flex-direction: row;
flex-wrap: wrap;
margin-top: 10px;
padding-top: 8px;
padding-right: 8px;
padding-bottom: 8px;
border-radius: 6px;
background-color: #f5f6f8;
}
.preview-toolbar-item {
width: 36px;
height: 36px;
margin-top: 8px;
margin-left: 8px;
border-radius: 6px;
justify-content: center;
align-items: center;
background-color: #ffffff;
}
.preview-toolbar-item-active {
background-color: #e6f0ff;
}
.preview-toolbar-icon {
color: #333333;
font-size: 18px;
}
.preview-toolbar-icon-active {
color: #007aff;
}
.preview-state-text {
margin-right: 12px;
color: #666666;
font-size: 13px;
}
.preview-editor-box {
margin-top: 10px;
height: 150px;
border-width: 1px;
border-style: solid;
border-color: rgba(0, 0, 0, 0.08);
border-radius: 6px;
background-color: #ffffff;
}
.preview-editor {
height: 150px;
padding: 10px;
}
.props-scroll {
flex: 1;
}
.button-list {
flex-direction: column;
margin-top: 10px;
}
.action-button {
margin-top: 10px;
}
.apply-button {
margin-bottom: 10px;
}
.event-header-row {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.log-box {
flex-direction: column;
margin-top: 10px;
padding: 10px;
border-radius: 6px;
background-color: #ffffff;
}
.log-text {
margin-top: 6px;
color: #333333;
font-size: 12px;
line-height: 18px;
}
</style>