Files
t-admin/uni_modules/uni-cms/components/editor/web/editor.vue
T
2025-06-13 21:16:12 +08:00

430 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view :id="id"></view>
</template>
<script>
// 扩展 Quill.js 编辑器
// #ifdef VUE3
import '@/uni_modules/uni-cms/common/quill.min.js'
import '@/uni_modules/uni-cms/common/quill-image-resize.js'
// #endif
// #ifndef VUE3
import Quill from '@/uni_modules/uni-cms/common/quill.min.js'
import QuillImageResize from '@/uni_modules/uni-cms/common/quill-image-resize.js'
window.Quill = Quill
window.ImageResize = QuillImageResize
// #endif
import { register as formatRegister } from './formats'
import { register as moduleRegister } from './modules'
import link from '../web/formats/link'
export default {
name: "editor", // 组件名
props: {
id: { // 编辑器的 id
type: String,
default: 'editor'
},
uploadHandler: { // 上传处理函数
type: Function,
default: null
}
},
mounted() {
// 初始化编辑器
this.initEditor()
},
methods: {
initEditor() {
// 注册自定义格式
formatRegister(Quill)
// 注册自定义模块
moduleRegister(Quill)
// 注册图片缩放模块
// #ifdef VUE3
Quill.register('modules/ImageResize', window.ImageResize.default)
// #endif
// #ifndef VUE3
Quill.register('modules/ImageResize', window.ImageResize)
// #endif
// 配置编辑器选项
const options = {
toolbar: false, // 隐藏工具栏
readOnly: false, // 可编辑
debug: false, // 关闭调试模式
placeholder: '输入内容', // 占位符
scrollingContainer: 'html,body', // 滚动容器
scrollingConfig: {
boundHeight: window.innerHeight - 100
},
modules: {
MediaVideo: true,
// 图片扩展模块
ImageExtend: {
uploadHandler: async (e, el) => {
if (typeof this.uploadHandler !== 'function') {
return uni.showModal({
title: "请先配置上传方法",
showCancel: false
})
}
return this.uploadHandler(e, el)
}
},
// 图片缩放模块
ImageResize: {
modules: ['DisplaySize', 'Toolbar', 'Resize']
}
}
}
// 创建编辑器实例
this.quill = new Quill('#' + this.id, options)
// 添加 Quill 事件监听器
this.addQuillListeners()
// 获取编辑器上下文
const context = this.getEditorContext()
// 触发 ready 事件并传递编辑器上下文
this.$emit('ready', context)
},
getEditorContext() {
// 返回一个对象,包含了编辑器的各种方法
return {
// 绑定 this,返回 format 方法
format: this.format.bind(this),
// 绑定 this,返回 insertDivider 方法
insertDivider: this.insertDivider.bind(this),
// 绑定 this,返回 insertImage 方法
insertImage: this.insertImage.bind(this),
// 绑定 this,返回 insertText 方法
insertText: this.insertText.bind(this),
// 绑定 this,返回 setContents 方法
setContents: this.setContents.bind(this),
// 绑定 this,返回 getContents 方法
getContents: this.getContents.bind(this),
// 绑定 this,返回 clear 方法
clear: this.clear.bind(this),
// 绑定 this,返回 removeFormat 方法
removeFormat: this.removeFormat.bind(this),
// 绑定 this,返回 undo 方法
undo: this.undo.bind(this),
// 绑定 this,返回 redo 方法
redo: this.redo.bind(this),
// 绑定 this,返回 blur 方法
blur: this.blur.bind(this),
// 绑定 this,返回 scrollIntoView 方法
scrollIntoView: this.scrollIntoView.bind(this),
// 绑定 this,返回 getSelectionText 方法
getSelectionText: this.getSelectionText.bind(this),
// 绑定 this,返回 insertUnlockContent 方法
insertUnlockContent: this.insertUnlockContent.bind(this),
// 绑定 this,返回 insertMediaVideo 方法
insertMediaVideo: this.insertMediaVideo.bind(this),
}
},
// 格式化文本
format(name, _value = false) {
// 获取当前光标所在位置
const range = this.quill.getSelection(true)
// 获取选中文本的值
let value = _value
// 获取选中文本的格式
let format = this.quill.getFormat(range)[name] || false
let isFormat = false
// 如果是加粗、斜体、下划线、删除线、插入线
if (['bold', 'italic', 'underline', 'strike', 'ins'].includes(name)) {
// 取反
value = !format
} else if (name === 'direction') { // 如果是文本方向
// 如果 value 是 rtl 且 format 存在,则 value 为 false,否则为 value
value = value === 'rtl' && format ? false : value
// 获取对齐方式
const align = this.quill.getFormat(range).align
// 如果 value 是 rtl 且 align 不存在
if (value === 'rtl' && !align) {
// 设置对齐方式为右对齐
this.quill.format('align', 'right', Quill.sources.USER)
isFormat = true
} else if (!value && align === 'right') { // 如果 value 不存在且 align 是右对齐
// 取消右对齐
this.quill.format('align', false, Quill.sources.USER)
isFormat = true
}
} else if (name === 'indent') { // 如果是缩进
// 获取文本方向是否为 rtl
const rtl = this.quill.getFormat(range).direction === 'rtl'
// 如果 value 是 +1
value = value === '+1'
// 如果文本方向是 rtl
if (rtl) {
// 取反
value = !value
}
// 如果 value 为 true,value 为 +1,否则为 -1
value = value ? '+1' : '-1'
} else if (name === 'link') {
const [blot, offset] = this.quill.scroll.descendant(link(Quill)['formats/link'], range.index);
if (blot) {
this.quill.formatText({
index: range.index - offset,
length: blot.length()
}, name, value, Quill.sources.USER)
} else {
this.quill.format(name, value, Quill.sources.USER)
}
isFormat = true
//
// if (range.length > 0) {
// this.quill.format(name, value, Quill.sources.USER)
// } else {
//
//
// const [blot] = this.quill.getLeaf(range.index);
// if (blot && blot.domNode && blot.domNode.tagName === "A") {
// blot.parent.format(name, value)
// isFormat = true
// }
// }
} else { // 其他情况
// 如果是列表
if (name === 'list') {
// 如果 value 是 check,value 为 unchecked,否则为 value
value = value === 'check' ? 'unchecked' : value
// 如果 format 是 checked,format 为 unchecked,否则为 format
format = format === 'checked' ? 'unchecked' : format
}
// 如果 format 存在且不等于 value 或者 format 不存在且 value 存在,则 value 为 value,否则为 format 的取反
value = ((format && format !== (value || false)) || (!format && value)) ? value : !format
}
// 设置格式
!isFormat && this.quill.format(name, value, Quill.sources.USER)
// 触发状态改变事件
this.emitStatusChange(range)
},
insertDivider() {
// 获取当前光标所在位置
const range = this.quill.getSelection(true)
// 在当前光标位置插入一个换行符
this.quill.insertText(range.index, '\n', Quill.sources.USER)
// 在当前光标位置插入一个分割线
this.quill.insertEmbed(range.index + 1, 'divider', true, Quill.sources.USER)
// 将光标移动到分割线下一行
this.quill.setSelection(range.index + 2, Quill.sources.SILENT)
},
insertImage({ src, alt, width, height, extClass, data = {} } = {}) {
// 获取当前光标所在位置
const range = this.quill.getSelection(true)
// 如果当前光标前面不是换行符,则在当前光标位置插入一个换行符
if (range.index > 0 && this.quill.getText(range.index - 1, 1) !== '\n') {
this.quill.insertText(range.index, '\n', Quill.sources.SILENT)
range.index += 1;
}
// 在当前光标位置插入一个图片
this.quill.insertEmbed(range.index, 'image', {src, uploaded: true}, Quill.sources.SILENT)
// 设置图片的本地路径
this.quill.formatText(range.index, 1, 'data-local', src, Quill.sources.SILENT)
// 设置图片的 alt 属性
this.quill.formatText(range.index, 1, 'alt', alt, Quill.sources.SILENT)
// 设置图片的宽度
this.quill.formatText(range.index, 1, 'width', width, Quill.sources.SILENT)
// 设置图片的高度
this.quill.formatText(range.index, 1, 'height', height, Quill.sources.SILENT)
// 设置图片的 class 属性
this.quill.formatText(range.index, 1, 'class', extClass, Quill.sources.SILENT)
// 设置图片的自定义属性
this.quill.formatText(range.index, 1, 'data-custom', Object.keys(data).map(key => `${key}=${data[key]}`).join('&'), Quill.sources.SILENT)
// 在图片下面插入一个换行符
this.quill.insertText(range.index + 1, '\n', Quill.sources.SILENT)
// 将光标移动到图片下一行
this.quill.setSelection(range.index + 2, Quill.sources.SILENT)
// 滚动到可视区域
this.quill.scrollIntoView()
},
insertText({ text }) {
// 获取当前光标所在位置
const range = this.quill.getSelection(true)
// 在当前光标位置插入文本
this.quill.insertText(range.index, text, Quill.sources.USER)
// 将光标移动到文本末尾
this.quill.setSelection(range.index + text.length, 0, Quill.sources.SILENT)
},
setContents({ delta }) {
// 设置编辑器内容
this.quill.setContents(delta, Quill.sources.SILENT)
},
// 获取编辑器内容
getContents({ success, fail, complete } = {}) {
let contents
try {
// 获取编辑器内容的 HTML
const html = this.quill.root.innerHTML
// 获取编辑器内容的纯文本
const text = this.quill.getText()
// 获取编辑器内容的 Delta 对象
const delta = this.quill.getContents()
contents = {
html,
text,
delta
}
// 如果传入了 success 回调函数,则执行该函数并传入编辑器内容
if (typeof success === "function") {
success(contents)
}
} catch (e) {
// 如果传入了 fail 回调函数,则执行该函数并传入错误信息
if (typeof fail === 'function') {
fail(e)
}
}
// 如果传入了 complete 回调函数,则执行该函数
if (typeof complete === 'function') complete()
// 返回一个 Promise 对象,其状态为已解决,并传入编辑器内容
return Promise.resolve(contents)
},
// 清空编辑器内容
clear() {
// 将编辑器内容设置为空数组
this.quill.setContents([])
},
// 移除选中文本的格式
removeFormat() {
// 获取当前选中文本的范围
const range = this.quill.getSelection(true)
// 获取 Parchment 对象
const parchment = Quill.import('parchment')
// 如果选中了文本,则移除选中文本的格式
if (range.length) {
this.quill.removeFormat(range, Quill.sources.USER)
} else {
// 否则,移除当前光标所在位置的格式
Object.keys(this.quill.getFormat(range)).forEach(key => {
if (parchment.query(key, parchment.Scope.INLINE)) {
this.quill.format(key, false)
}
})
}
},
// 撤销
undo() {
this.quill.history.undo()
},
// 重做
redo() {
this.quill.history.redo()
},
// 失去焦点
blur() {
this.quill.blur()
},
// 滚动到可视区域
scrollIntoView() {
this.quill.scrollIntoView()
},
// 在当前光标位置插入一个解锁内容的占位符
insertUnlockContent() {
const range = this.quill.getSelection(true)
this.quill.insertEmbed(range.index, 'unlockContent', Quill.sources.SILENT)
this.quill.setSelection(range.index + 1, Quill.sources.SILENT)
},
insertMediaVideo ({poster, src, duration}) {
const range = this.quill.getSelection(true)
this.quill.insertEmbed(range.index, 'mediaVideo', {poster, src, duration}, Quill.sources.SILENT)
this.quill.setSelection(range.index + 1, Quill.sources.SILENT)
},
// 获取选中文本的纯文本
getSelectionText() {
const range = this.quill.selection.savedRange
const res = { text: '' }
if (range && range.length !== 0) {
res.text = this.quill.getText(range.index, range.length)
}
return res
},
// 加载 Quill 和 Quill-image-resize-mp 库
// _loadQuill(callback) {
// // 加载本地 quill.js
// // const quillSrc = 'https://unpkg.com/quill@1.3.7/dist/quill.min.js'
// const imageResizeSrc = 'https://unpkg.com/quill-image-resize-mp@3.0.1/image-resize.min.js'
//
// // loadScript(window.Quill, quillSrc, () => {
// loadScript(window.ImageResize, imageResizeSrc, () => {
// callback()
// })
// // })
// },
// 添加 Quill 事件监听器
addQuillListeners() {
// 监听光标位置变化事件
this.quill.on(Quill.events.SELECTION_CHANGE, (range) => {
this.emitStatusChange(range)
if (range) {
this.$emit('focus')
} else {
this.$emit('blur')
}
})
// 监听滚动事件
this.quill.on(Quill.events.SCROLL_OPTIMIZE, () => {
const range = this.quill.selection.getRange()[0]
this.emitStatusChange(range)
})
// 监听文本变化事件
this.quill.on(Quill.events.TEXT_CHANGE, (delta, oldDelta, source) => {
const text = this.quill.getText().replace(/\n/g, '')
this.$emit('textchange', {
detail: text.length
})
})
},
// 触发状态变化事件
emitStatusChange(range) {
this.$emit('statuschange', {
detail: range ? this.quill.getFormat(range) : {}
})
}
}
}
</script>
<style scoped>
</style>