CanvasEditor
实现Word在线编辑器
官网文档:https://hufe.club/canvas-editor-docs/guide/schema.html
源码地址:https://github.com/Hufe921/canvas-editor
前提声明:
由于
CanvasEditor
目前不支持vue、react等框架开箱即用版,所以需要我们去Git下载源码,拿到其中两个主要文件,集成到我们自己的项目中
第一步:项目安装CanvasEditor
在你需要集成编辑器的项目中,安装CanvasEditor
bash
npm i CanvasEditor
第二步:主要文件集成
1、下载源码,目录如下:红色框就是我们需要在自己项目中引用的
2、在你的vue项目项目中,新建一个文件夹,叫CanvasEditor,这个文件夹内,就放集成的相关文件(代码以及目录结构贴在后边)
- 新建个
index.vue
文件,作为后边集成封装的CanvasEditor组件引入的入口 index.vue
的template
的html部分,就把CanvasEditor源码的html文件中粘过来- 结合文档,把
main.ts
相关的工具函数内容放到你的index.vue
的script
的method
部分(某些引入报错就把相关报错的文件找到),并引入到自己的CanvasEditor文件夹下(由于我的项目用的js所以我复用main.ts的相关方法时转换为了js) - 新建
componments
文件夹,把源码中的dialog
和signature
文件夹放到里面(记得文件引用的assets文件粘到自己项目中,且更改引用路径,否则会报错) option.js
是参照官网的相关样式默认配置style.css
也是源码同名文件粘过来的- 缺失的相关静态资源,根据报错从源码中粘过来即可,
- 封装完成后,自定义传值、调用接口等逻辑,就根据自己需要自主发挥吧
集成组件目录结构:如图
index.vue文件如下:
说明:
1、有很多注释的代码,是因为我不需要而已,自己需要的话 自己放开注释即可;
2、为了更好的作为子组件引入,我更改了样式在style标签内,可自行根据需要修改
3、由于我的项目用的js所以我复用main.ts的相关方法时转换为了js
typescript
<style lang="scss" scoped>
.footer {
position: static;
}
.menu {
position: absolute;
top: 0;
left: 0;
}
.el-footer {
background-color: #f2f4f7;
text-align: center;
line-height: 30px!important; /* 调整footer高度 */
}
.content {
position: static;
display: flex;
flex-direction: column;
height: calc(100% - 90px); /* 减去header和footer的高度 */
background-color: #f2f4f7;
overflow-y: auto; /* 仅让Main区域可滚动 */
flex-grow: 1; /* 让Main区域占据剩余空间 */
}
@media (max-width: 1220px) {
.menu {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: flex-start;
}
}
#canvasEditor {
display: flex;
justify-content: center;
background: #f2f4f7;
}
.el-header {
position: relative;
}
.canvas-container {
height: 100%;
margin-top: 10px;
}
@import url('./components/dialog/dialog.css');
@import url('./components/signature/signature.css');
@import url('./style.css');
</style>
<template>
<div class="canvas-container">
<el-container style="height: 100%;">
<el-header>
<div class="menu" editor-component="menu">
<div class="menu-item">
<div class="menu-item__save" title="保存">
<i class="el-icon-s-claim" style="color:#646464"></i>
</div>
<div class="menu-item__undo">
<i></i>
</div>
<div class="menu-item__redo">
<i></i>
</div>
<div class="menu-item__painter" title="格式刷(双击可连续使用)">
<i></i>
</div>
<div class="menu-item__format" title="清除格式">
<i></i>
</div>
</div>
<div class="menu-divider"></div>
<div class="menu-item">
<div class="menu-item__font">
<span class="select" title="字体">微软雅黑</span>
<div class="options">
<ul>
<li data-family="Microsoft YaHei" style="font-family:'Microsoft YaHei';">微软雅黑</li>
<li data-family="宋体" style="font-family:'宋体';">宋体</li>
<li data-family="黑体" style="font-family:'黑体';">黑体</li>
<li data-family="仿宋" style="font-family:'仿宋';">仿宋</li>
<li data-family="楷体" style="font-family:'楷体';">楷体</li>
<li data-family="等线" style="font-family:'等线';">等线</li>
<!-- <li data-family="华文琥珀" style="font-family:'华文琥珀';">华文琥珀</li>
<li data-family="华文楷体" style="font-family:'华文楷体';">华文楷体</li>
<li data-family="华文隶书" style="font-family:'华文隶书';">华文隶书</li>
<li data-family="华文新魏" style="font-family:'华文新魏';">华文新魏</li>
<li data-family="华文行楷" style="font-family:'华文行楷';">华文行楷</li>
<li data-family="华文中宋" style="font-family:'华文中宋';">华文中宋</li>
<li data-family="华文彩云" style="font-family:'华文彩云';">华文彩云</li> -->
<li data-family="Arial" style="font-family:'Arial';">Arial</li>
<li data-family="Segoe UI" style="font-family:'Segoe UI';">Segoe UI</li>
<li data-family="Ink Free" style="font-family:'Ink Free';">Ink Free</li>
<li data-family="Fantasy" style="font-family:'Fantasy';">Fantasy</li>
</ul>
</div>
</div>
<div class="menu-item__size">
<span class="select" title="字体">小四</span>
<div class="options">
<ul>
<li data-size="56">初号</li>
<li data-size="48">小初</li>
<li data-size="34">一号</li>
<li data-size="32">小一</li>
<li data-size="29">二号</li>
<li data-size="24">小二</li>
<li data-size="21">三号</li>
<li data-size="20">小三</li>
<li data-size="18">四号</li>
<li data-size="16">小四</li>
<li data-size="14">五号</li>
<li data-size="12">小五</li>
<li data-size="10">六号</li>
<li data-size="8">小六</li>
<li data-size="7">七号</li>
<li data-size="6">八号</li>
</ul>
</div>
</div>
<div class="menu-item__size-add">
<i></i>
</div>
<div class="menu-item__size-minus">
<i></i>
</div>
<div class="menu-item__bold">
<i></i>
</div>
<div class="menu-item__italic">
<i></i>
</div>
<div class="menu-item__underline">
<i></i>
</div>
<div class="menu-item__strikeout" title="删除线(Ctrl+Shift+X)">
<i></i>
</div>
<div class="menu-item__superscript">
<i></i>
</div>
<div class="menu-item__subscript">
<i></i>
</div>
<div class="menu-item__color" title="字体颜色">
<i></i>
<span></span>
<input type="color" id="color" />
</div>
<div class="menu-item__highlight" title="高亮">
<i></i>
<span></span>
<input type="color" id="highlight">
</div>
</div>
<div class="menu-divider"></div>
<div class="menu-item">
<div class="menu-item__title">
<i></i>
<span class="select" title="切换标题">正文</span>
<div class="options">
<ul>
<li style="font-size:16px;">正文</li>
<li data-level="first" style="font-size:26px;">标题1</li>
<li data-level="second" style="font-size:24px;">标题2</li>
<li data-level="third" style="font-size:22px;">标题3</li>
<li data-level="fourth" style="font-size:20px;">标题4</li>
<li data-level="fifth" style="font-size:18px;">标题5</li>
<li data-level="sixth" style="font-size:16px;">标题6</li>
</ul>
</div>
</div>
<div class="menu-item__left">
<i></i>
</div>
<div class="menu-item__center">
<i></i>
</div>
<div class="menu-item__right">
<i></i>
</div>
<div class="menu-item__alignment">
<i></i>
</div>
<div class="menu-item__row-margin">
<i title="行间距"></i>
<div class="options">
<ul>
<li data-rowmargin='1'>1</li>
<li data-rowmargin="1.25">1.25</li>
<li data-rowmargin="1.5">1.5</li>
<li data-rowmargin="1.75">1.75</li>
<li data-rowmargin="2">2</li>
<li data-rowmargin="2.5">2.5</li>
<li data-rowmargin="3">3</li>
</ul>
</div>
</div>
<div class="menu-item__list">
<i></i>
<div class="options">
<ul>
<li>
<label>取消列表</label>
</li>
<li data-list-type="ol" data-list-style='decimal'>
<label>有序列表:</label>
<ol>
<li>________</li>
</ol>
</li>
<li data-list-type="ul" data-list-style='disc'>
<label>实心圆点列表:</label>
<ul style="list-style-type: disc;">
<li>________</li>
</ul>
</li>
<li data-list-type="ul" data-list-style='circle'>
<label>空心圆点列表:</label>
<ul style="list-style-type: circle;">
<li>________</li>
</ul>
</li>
<li data-list-type="ul" data-list-style='square'>
<label>空心方块列表:</label>
<ul style="list-style-type: square;">
<li>________</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
<div class="menu-divider"></div>
<div class="menu-item">
<!-- <div class="menu-item__table">
<i title="表格"></i>
</div> -->
<!-- <div class="menu-item__table__collapse">
<div class="table-close">×</div>
<div class="table-title">
<span class="table-select">插入</span>
<span>表格</span>
</div>
<div class="table-panel"></div>
</div> -->
<!-- <div class="menu-item__image">
<i title="图片"></i>
<input type="file" id="image" accept=".png, .jpg, .jpeg, .svg, .gif">
</div> -->
<div class="menu-item__hyperlink">
<i title="超链接"></i>
</div>
<div class="menu-item__separator">
<i title="分割线"></i>
<div class="options">
<ul>
<li data-separator='0,0'>
<i></i>
</li>
<li data-separator="1,1">
<i></i>
</li>
<li data-separator="3,1">
<i></i>
</li>
<li data-separator="4,4">
<i></i>
</li>
<li data-separator="7,3,3,3">
<i></i>
</li>
<li data-separator="6,2,2,2,2,2">
<i></i>
</li>
</ul>
</div>
</div>
<!-- <div class="menu-item__watermark">
<i title="水印(添加、删除)"></i>
<div class="options">
<ul>
<li data-menu="add">添加水印</li>
<li data-menu="delete">删除水印</li>
</ul>
</div>
</div>
<div class="menu-item__codeblock" title="代码块">
<i></i>
</div>
<div class="menu-item__page-break" title="分页符">
<i></i>
</div>
<div class="menu-item__control">
<i title="控件"></i>
<div class="options">
<ul>
<li data-control='text'>文本</li>
<li data-control="select">列举</li>
<li data-control="checkbox">复选框</li>
</ul>
</div>
</div>
<div class="menu-item__checkbox" title="复选框">
<i></i>
</div>
<div class="menu-item__latex" title="LateX">
<i></i>
</div>
<div class="menu-item__date">
<i title="日期"></i>
<div class="options">
<ul>
<li data-format="yyyy-MM-dd"></li>
<li data-format="yyyy-MM-dd hh:mm:ss"></li>
</ul>
</div>
</div>
<div class="menu-item__block" title="内容块">
<i></i>
</div> -->
</div>
<div class="menu-divider"></div>
<div class="menu-item">
<div class="menu-item__search" data-menu="search">
<i></i>
</div>
<div class="menu-item__search__collapse" data-menu="search">
<div class="menu-item__search__collapse__search">
<input type="text" />
<label class="search-result"></label>
<div class="arrow-left">
<i></i>
</div>
<div class="arrow-right">
<i></i>
</div>
<span>×</span>
</div>
<div class="menu-item__search__collapse__replace">
<input type="text">
<button>替换</button>
</div>
</div>
<!-- <div class="menu-item__print" data-menu="print">
<i></i>
</div> -->
</div>
</div>
</el-header>
<el-main class="content">
<!-- <div class="catalog" editor-component="catalog">
<div class="catalog__header">
<span>目录</span>
<div class="catalog__header__close">
<i></i>
</div>
</div>
<div class="catalog__main"></div>
</div> -->
<div id="canvasEditor" class="canvas-editor" editor-component="main"></div>
<!-- <div class="comment" editor-component="comment"></div> -->
</el-main>
<el-footer style="height: 30px;">
<div class="footer" editor-component="footer">
<div>
<!-- <div class="catalog-mode" title="目录">
<i></i>
</div> -->
<div class="page-mode">
<i title="页面模式(分页、连页)"></i>
<div class="options">
<ul>
<li data-page-mode="paging" class="active">分页</li>
<li data-page-mode="continuity">连页</li>
</ul>
</div>
</div>
<span>可见页码:<span class="page-no-list">1</span></span>
<span>页面:<span class="page-no">1</span>/<span class="page-size">1</span></span>
<span>字数:<span class="word-count">0</span></span>
</div>
<div class="editor-mode" title="编辑模式(编辑、清洁、只读、表单)">编辑模式</div>
<div>
<div class="page-scale-minus" title="缩小(Ctrl+-)">
<i></i>
</div>
<span class="page-scale-percentage" title="显示比例(点击可复原Ctrl+0)">100%</span>
<div class="page-scale-add" title="放大(Ctrl+=)">
<i></i>
</div>
<div class="paper-size">
<i title="纸张类型"></i>
<div class="options">
<ul>
<li data-paper-size="794*1123" class="active">A4</li>
<li data-paper-size="1593*2251">A2</li>
<li data-paper-size="1125*1593">A3</li>
<li data-paper-size="565*796">A5</li>
<li data-paper-size="412*488">5号信封</li>
<li data-paper-size="450*866">6号信封</li>
<li data-paper-size="609*862">7号信封</li>
<li data-paper-size="862*1221">9号信封</li>
<li data-paper-size="813*1266">法律用纸</li>
<li data-paper-size="813*1054">信纸</li>
</ul>
</div>
</div>
<div class="paper-direction">
<i title="纸张方向"></i>
<div class="options">
<ul>
<li data-paper-direction="vertical" class="active">纵向</li>
<li data-paper-direction="horizontal">横向</li>
</ul>
</div>
</div>
<div class="paper-margin" title="页边距">
<i></i>
</div>
<div class="fullscreen" title="全屏显示">
<i></i>
</div>
</div>
</div>
</el-footer>
</el-container>
</div>
</template>
<script>
import Editor from '@hufe921/canvas-editor'
import { Dialog } from './components/dialog/Dialog'
import { Signature } from './components/signature/Signature'
import {IEditorOption, ITableOption,IHeader, IFooter } from './options'
import {BlockType,
Command,
ControlType,
EditorMode,
EditorZone,
ElementType,
IBlock,
ICatalogItem,
IElement,
KeyMap,
ListStyle,
ListType,
PageMode,
PaperDirection,
RowFlex,
TitleLevel,
splitText } from '@hufe921/canvas-editor'
export default {
data() {
return {
editorRef: null,
isApple: typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent),
// 编辑模式
modeList: [{
mode: EditorMode.READONLY,
name: '只读模式'
},
{
mode: EditorMode.EDIT,
name: '编辑模式'
}
],
header: [],// 页眉配置
main: [],// 主要编辑内容
footer: [],// 页脚信息
options: IEditorOption,
// 批注 TODO
commentList: []
};
},
props: {
// 编辑模式
editMode: {
type: String
},
// html数据
htmlData: {
type: String
},
// 后端传过来的保存html的JSON数据(用于回显)
docJson: {
type: String
}
},
watch: {
// 监听父组件传过来的编辑模式,设置模式
editMode: {
handler (val) {
if(this.editorRef) {
this.editorRef.command.executeMode(val)
// 设置模式
const modeElement = document.querySelector('.editor-mode')
modeElement.innerText = this.modeList.filter((item) => item.mode == val).map((data) => data.name) || ''
// 设置菜单栏权限视觉反馈
const isReadonly = val === EditorMode.READONLY
const enableMenuList = ['search', 'print']
document.querySelectorAll('.menu-item>div').forEach(dom => {
const menu = dom.dataset.menu
isReadonly && (!menu || !enableMenuList.includes(menu))
? dom.classList.add('disable')
: dom.classList.remove('disable')
})
}
},
deep: true
}
},
methods:{
debounce(func, delay) {
let timer;
return function(...args) {
if (timer) {
window.clearTimeout(timer);
}
timer = window.setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
},
mounted () {
const isApple = typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent)
const instance = new Editor(
document.querySelector('.canvas-editor'),
{
header: this.header,
main: this.main,
footer: this.footer
},
this.options
)
this.editorRef = instance;
// cypress使用
Reflect.set(window, 'editor', instance)
// 回显编辑器数据
if(this.docJson !== null) {
// 通过getValue来的数据回显页面(因为用html回显页面会丢掉font-family,官网git issue有解释)
instance.command.executeSetValue({main: JSON.parse(this.docJson)})
} else {
// 处理后端返回的html字符串
// 先替换 \r\n 为 空格,以统一处理空格问题
let step1 = this.htmlData.replace(/\r\n/g, ' ');
// 然后替换 \\\" 为 \" ,确保样式字符串内的引号正确
let step2 = step1.replace(/\\\"/g, '"');
// 接着替换 \\ 为 空字符,去掉其他不必要的转义
let cleanedHtml = step2.replace(/\\+/g, '');
// 设置Word模板数据
instance.command.executeSetHTML({main: cleanedHtml})
}
// 菜单弹窗销毁
window.addEventListener(
'click',
evt => {
const visibleDom = document.querySelector('.visible')
if (!visibleDom || visibleDom.contains(evt.target)) return
visibleDom.classList.remove('visible')
},
{
capture: true
}
)
/*
工具栏方法
*/
// 1.保存(自定义)
const saveDom = document.querySelector('.menu-item__save');
saveDom.title = `保存(${this.isApple ? '⌘' : 'Ctrl'}+S)`;
saveDom.onclick = () => {
const value = instance.command.getValue(this.options)
const htmlVal = instance.command.getHTML()
this.$emit('save', htmlVal)// 保存数据传给父组件
};
// 快捷键保存
instance.listener.saved = (payload) => {
console.log('elementList: ', payload)
this.$emit('save', htmlVal)// 保存数据传给父组件
}
// 2. | 撤销 | 重做 | 格式刷 | 清除格式 |
const undoDom = document.querySelector('.menu-item__undo')
undoDom.title = `撤销(${isApple ? '⌘' : 'Ctrl'}+Z)`
undoDom.onclick = function () {
console.log('undo')
instance.command.executeUndo()
}
const redoDom = document.querySelector('.menu-item__redo')
redoDom.title = `重做(${isApple ? '⌘' : 'Ctrl'}+Y)`
redoDom.onclick = function () {
console.log('redo')
instance.command.executeRedo()
}
const painterDom = document.querySelector(
'.menu-item__painter'
)
painterDom.onclick = function () {
console.log('painter')
instance.command.executePainter({
isDblclick: false
})
}
painterDom.ondblclick = function () {
console.log('painter')
instance.command.executePainter({
isDblclick: true
})
}
document.querySelector('.menu-item__format').onclick =
function () {
console.log('format')
instance.command.executeFormat()
}
// 3. | 字体 | 字体变大 | 字体变小 | 加粗 | 斜体 | 下划线 | 删除线 | 上标 | 下标 | 字体颜色 | 背景色 |
const fontDom = document.querySelector('.menu-item__font')
const fontSelectDom = fontDom.querySelector('.select')
const fontOptionDom = fontDom.querySelector('.options')
fontDom.onclick = function () {
console.log('font')
fontOptionDom.classList.toggle('visible')
}
fontOptionDom.onclick = function (evt) {
const li = evt.target
instance.command.executeFont(li.dataset.family)
}
const sizeSetDom = document.querySelector('.menu-item__size')
const sizeSelectDom = sizeSetDom.querySelector('.select')
const sizeOptionDom = sizeSetDom.querySelector('.options')
sizeSetDom.title = `设置字号`
sizeSetDom.onclick = function () {
console.log('size')
sizeOptionDom.classList.toggle('visible')
}
sizeOptionDom.onclick = function (evt) {
const li = evt.target
instance.command.executeSize(Number(li.dataset.size))
}
const sizeAddDom = document.querySelector(
'.menu-item__size-add'
)
sizeAddDom.title = `增大字号(${this.isApple ? '⌘' : 'Ctrl'}+[)`
sizeAddDom.onclick = function () {
console.log('size-add')
instance.command.executeSizeAdd()
}
const sizeMinusDom = document.querySelector(
'.menu-item__size-minus'
)
sizeMinusDom.title = `减小字号(${this.isApple ? '⌘' : 'Ctrl'}+])`
sizeMinusDom.onclick = function () {
console.log('size-minus')
instance.command.executeSizeMinus()
}
const boldDom = document.querySelector('.menu-item__bold')
boldDom.title = `加粗(${this.isApple ? '⌘' : 'Ctrl'}+B)`
boldDom.onclick = function () {
console.log('bold')
instance.command.executeBold()
}
const italicDom =
document.querySelector('.menu-item__italic')
italicDom.title = `斜体(${this.isApple ? '⌘' : 'Ctrl'}+I)`
italicDom.onclick = function () {
console.log('italic')
instance.command.executeItalic()
}
const underlineDom = document.querySelector(
'.menu-item__underline'
)
underlineDom.title = `下划线(${this.isApple ? '⌘' : 'Ctrl'}+U)`
underlineDom.onclick = function () {
console.log('underline')
instance.command.executeUnderline()
}
const strikeoutDom = document.querySelector(
'.menu-item__strikeout'
)
strikeoutDom.onclick = function () {
console.log('strikeout')
instance.command.executeStrikeout()
}
const superscriptDom = document.querySelector(
'.menu-item__superscript'
)
superscriptDom.title = `上标(${this.isApple ? '⌘' : 'Ctrl'}+Shift+,)`
superscriptDom.onclick = function () {
console.log('superscript')
instance.command.executeSuperscript()
}
const subscriptDom = document.querySelector(
'.menu-item__subscript'
)
subscriptDom.title = `下标(${this.isApple ? '⌘' : 'Ctrl'}+Shift+.)`
subscriptDom.onclick = function () {
console.log('subscript')
instance.command.executeSubscript()
}
const colorControlDom = document.querySelector('#color')
colorControlDom.oninput = function () {
instance.command.executeColor(colorControlDom.value)
}
const colorDom = document.querySelector('.menu-item__color')
const colorSpanDom = colorDom.querySelector('span')
colorDom.onclick = function () {
console.log('color')
colorControlDom.click()
}
const highlightControlDom =
document.querySelector('#highlight')
highlightControlDom.oninput = function () {
instance.command.executeHighlight(highlightControlDom.value)
}
const highlightDom = document.querySelector(
'.menu-item__highlight'
)
const highlightSpanDom = highlightDom.querySelector('span')
highlightDom.onclick = function () {
console.log('highlight')
highlightControlDom?.click()
}
const titleDom = document.querySelector('.menu-item__title')
const titleSelectDom = titleDom.querySelector('.select')
const titleOptionDom = titleDom.querySelector('.options')
titleOptionDom.querySelectorAll('li').forEach((li, index) => {
li.title = `Ctrl+${this.isApple ? 'Option' : 'Alt'}+${index}`
})
titleDom.onclick = function () {
console.log('title')
titleOptionDom.classList.toggle('visible')
}
titleOptionDom.onclick = function (evt) {
const li = evt.target
const level = li.dataset.level
instance.command.executeTitle(level || null)
}
const leftDom = document.querySelector('.menu-item__left')
leftDom.title = `左对齐(${this.isApple ? '⌘' : 'Ctrl'}+L)`
leftDom.onclick = function () {
console.log('left')
instance.command.executeRowFlex(RowFlex.LEFT)
}
const centerDom =
document.querySelector('.menu-item__center')
centerDom.title = `居中对齐(${this.isApple ? '⌘' : 'Ctrl'}+E)`
centerDom.onclick = function () {
console.log('center')
instance.command.executeRowFlex(RowFlex.CENTER)
}
const rightDom = document.querySelector('.menu-item__right')
rightDom.title = `右对齐(${this.isApple ? '⌘' : 'Ctrl'}+R)`
rightDom.onclick = function () {
console.log('right')
instance.command.executeRowFlex(RowFlex.RIGHT)
}
const alignmentDom = document.querySelector(
'.menu-item__alignment'
)
alignmentDom.title = `两端对齐(${this.isApple ? '⌘' : 'Ctrl'}+J)`
alignmentDom.onclick = function () {
console.log('alignment')
instance.command.executeRowFlex(RowFlex.ALIGNMENT)
}
const rowMarginDom = document.querySelector(
'.menu-item__row-margin'
)
const rowOptionDom = rowMarginDom.querySelector('.options')
rowMarginDom.onclick = function () {
console.log('row-margin')
rowOptionDom.classList.toggle('visible')
}
rowOptionDom.onclick = function (evt) {
const li = evt.target
instance.command.executeRowMargin(Number(li.dataset.rowmargin))
}
const listDom = document.querySelector('.menu-item__list')
listDom.title = `列表(${this.isApple ? '⌘' : 'Ctrl'}+Shift+U)`
const listOptionDom = listDom.querySelector('.options')
listDom.onclick = function () {
console.log('list')
listOptionDom.classList.toggle('visible')
}
listOptionDom.onclick = function (evt) {
const li = evt.target
const listType = li.dataset.listType || null
const listStyle = (li.dataset.listStyle)
instance.command.executeList(listType, listStyle)
}
// 4. | 表格 | 图片 | 超链接 | 分割线 | 水印 | 代码块 | 分隔符 | 控件 | 复选框 | LaTeX | 日期选择器
// const tableDom = document.querySelector('.menu-item__table')
// const tablePanelContainer = document.querySelector(
// '.menu-item__table__collapse'
// )
// const tableClose = document.querySelector('.table-close')
// const tableTitle = document.querySelector('.table-select')
// const tablePanel = document.querySelector('.table-panel')
// // 绘制行列
// const tableCellList = []
// for (let i = 0; i < 10; i++) {
// const tr = document.createElement('tr')
// tr.classList.add('table-row')
// const trCellList = []
// for (let j = 0; j < 10; j++) {
// const td = document.createElement('td')
// td.classList.add('table-cel')
// tr.append(td)
// trCellList.push(td)
// }
// tablePanel.append(tr)
// tableCellList.push(trCellList)
// }
// let colIndex = 0
// let rowIndex = 0
// // 移除所有格选择
// function removeAllTableCellSelect() {
// tableCellList.forEach(tr => {
// tr.forEach(td => td.classList.remove('active'))
// })
// }
// // 设置标题内容
// function setTableTitle(payload) {
// tableTitle.innerText = payload
// }
// // 恢复初始状态
// function recoveryTable() {
// // 还原选择样式、标题、选择行列
// removeAllTableCellSelect()
// setTableTitle('插入')
// colIndex = 0
// rowIndex = 0
// // 隐藏panel
// tablePanelContainer.style.display = 'none'
// }
// tableDom.onclick = function () {
// console.log('table')
// tablePanelContainer.style.display = 'block'
// }
// tablePanel.onmousemove = function (evt) {
// const celSize = 16
// const rowMarginTop = 10
// const celMarginRight = 6
// const { offsetX, offsetY } = evt
// // 移除所有选择
// removeAllTableCellSelect()
// colIndex = Math.ceil(offsetX / (celSize + celMarginRight)) || 1
// rowIndex = Math.ceil(offsetY / (celSize + rowMarginTop)) || 1
// // 改变选择样式
// tableCellList.forEach((tr, trIndex) => {
// tr.forEach((td, tdIndex) => {
// if (tdIndex < colIndex && trIndex < rowIndex) {
// td.classList.add('active')
// }
// })
// })
// // 改变表格标题
// setTableTitle(`${rowIndex}×${colIndex}`)
// }
// tableClose.onclick = function () {
// recoveryTable()
// }
// tablePanel.onclick = function () {
// // 应用选择
// instance.command.executeInsertTable(rowIndex, colIndex)
// recoveryTable()
// }
// const imageDom = document.querySelector('.menu-item__image')
// const imageFileDom = document.querySelector('#image')
// imageDom.onclick = function () {
// imageFileDom.click()
// }
// imageFileDom.onchange = function () {
// const file = imageFileDom.files[0]
// const fileReader = new FileReader()
// fileReader.readAsDataURL(file)
// fileReader.onload = function () {
// // 计算宽高
// const image = new Image()
// const value = String(fileReader.result)
// image.src = value
// image.onload = function () {
// instance.command.executeImage({
// value,
// width: image.width,
// height: image.height
// })
// imageFileDom.value = ''
// }
// }
// }
const hyperlinkDom = document.querySelector(
'.menu-item__hyperlink'
)
hyperlinkDom.onclick = function () {
console.log('hyperlink')
new Dialog({
title: '超链接',
data: [
{
type: 'text',
label: '文本',
name: 'name',
required: true,
placeholder: '请输入文本',
value: instance.command.getRangeText()
},
{
type: 'text',
label: '链接',
name: 'url',
required: true,
placeholder: '请输入链接'
}
],
onConfirm: payload => {
const name = payload.find(p => p.name === 'name')?.value
if (!name) return
const url = payload.find(p => p.name === 'url')?.value
if (!url) return
instance.command.executeHyperlink({
type: ElementType.HYPERLINK,
value: '',
url,
valueList: splitText(name).map(n => ({
value: n,
size: 16
}))
})
}
})
}
const separatorDom = document.querySelector(
'.menu-item__separator'
)
const separatorOptionDom =
separatorDom.querySelector('.options')
separatorDom.onclick = function () {
console.log('separator')
separatorOptionDom.classList.toggle('visible')
}
separatorOptionDom.onmousedown = function (evt) {
let payload = []
const li = evt.target
const separatorDash = li.dataset.separator?.split(',').map(Number)
if (separatorDash) {
const isSingleLine = separatorDash.every(d => d === 0)
if (!isSingleLine) {
payload = separatorDash
}
}
instance.command.executeSeparator(payload)
}
// const pageBreakDom = document.querySelector(
// '.menu-item__page-break'
// )
// pageBreakDom.onclick = function () {
// console.log('pageBreak')
// instance.command.executePageBreak()
// }
// const watermarkDom = document.querySelector(
// '.menu-item__watermark'
// )
// const watermarkOptionDom =
// watermarkDom.querySelector('.options')
// watermarkDom.onclick = function () {
// console.log('watermark')
// watermarkOptionDom.classList.toggle('visible')
// }
// watermarkOptionDom.onmousedown = function (evt) {
// const li = evt.target
// const menu = li.dataset.menu
// watermarkOptionDom.classList.toggle('visible')
// if (menu === 'add') {
// new Dialog({
// title: '水印',
// data: [
// {
// type: 'text',
// label: '内容',
// name: 'data',
// required: true,
// placeholder: '请输入内容'
// },
// {
// type: 'color',
// label: '颜色',
// name: 'color',
// required: true,
// value: '#AEB5C0'
// },
// {
// type: 'number',
// label: '字体大小',
// name: 'size',
// required: true,
// value: '120'
// }
// ],
// onConfirm: payload => {
// const nullableIndex = payload.findIndex(p => !p.value)
// if (~nullableIndex) return
// const watermark = payload.reduce((pre, cur) => {
// pre[cur.name] = cur.value
// return pre
// }, {})
// instance.command.executeAddWatermark({
// data: watermark.data,
// color: watermark.color,
// size: Number(watermark.size)
// })
// }
// })
// } else {
// instance.command.executeDeleteWatermark()
// }
// }
// const codeblockDom = document.querySelector(
// '.menu-item__codeblock'
// )
// codeblockDom.onclick = function () {
// console.log('codeblock')
// new Dialog({
// title: '代码块',
// data: [
// {
// type: 'textarea',
// name: 'codeblock',
// placeholder: '请输入代码',
// width: 500,
// height: 300
// }
// ],
// onConfirm: payload => {
// const codeblock = payload.find(p => p.name === 'codeblock')?.value
// if (!codeblock) return
// const tokenList = prism.tokenize(codeblock, prism.languages.javascript)
// const formatTokenList = formatPrismToken(tokenList)
// const elementList = []
// for (let i = 0; i < formatTokenList.length; i++) {
// const formatToken = formatTokenList[i]
// const tokenStringList = splitText(formatToken.content)
// for (let j = 0; j < tokenStringList.length; j++) {
// const value = tokenStringList[j]
// const element = {
// value
// }
// if (formatToken.color) {
// element.color = formatToken.color
// }
// if (formatToken.bold) {
// element.bold = true
// }
// if (formatToken.italic) {
// element.italic = true
// }
// elementList.push(element)
// }
// }
// elementList.unshift({
// value: '\n'
// })
// instance.command.executeInsertElementList(elementList)
// }
// })
// }
// const controlDom = document.querySelector(
// '.menu-item__control'
// )
// const controlOptionDom = controlDom.querySelector('.options')
// controlDom.onclick = function () {
// console.log('control')
// controlOptionDom.classList.toggle('visible')
// }
// controlOptionDom.onmousedown = function (evt) {
// controlOptionDom.classList.toggle('visible')
// const li = evt.target
// const type = li.dataset.control
// switch (type) {
// case ControlType.TEXT:
// new Dialog({
// title: '文本控件',
// data: [
// {
// type: 'text',
// label: '占位符',
// name: 'placeholder',
// required: true,
// placeholder: '请输入占位符'
// },
// {
// type: 'text',
// label: '默认值',
// name: 'value',
// placeholder: '请输入默认值'
// }
// ],
// onConfirm: payload => {
// const placeholder = payload.find(
// p => p.name === 'placeholder'
// )?.value
// if (!placeholder) return
// const value = payload.find(p => p.name === 'value')?.value || ''
// instance.command.executeInsertElementList([
// {
// type: ElementType.CONTROL,
// value: '',
// control: {
// type,
// value: value
// ? [
// {
// value
// }
// ]
// : null,
// placeholder
// }
// }
// ])
// }
// })
// break
// case ControlType.SELECT:
// new Dialog({
// title: '列举控件',
// data: [
// {
// type: 'text',
// label: '占位符',
// name: 'placeholder',
// required: true,
// placeholder: '请输入占位符'
// },
// {
// type: 'text',
// label: '默认值',
// name: 'code',
// placeholder: '请输入默认值'
// },
// {
// type: 'textarea',
// label: '值集',
// name: 'valueSets',
// required: true,
// height: 100,
// placeholder: `请输入值集JSON,例:\n[{\n"value":"有",\n"code":"98175"\n}]`
// }
// ],
// onConfirm: payload => {
// const placeholder = payload.find(
// p => p.name === 'placeholder'
// )?.value
// if (!placeholder) return
// const valueSets = payload.find(p => p.name === 'valueSets')?.value
// if (!valueSets) return
// const code = payload.find(p => p.name === 'code')?.value
// instance.command.executeInsertElementList([
// {
// type: ElementType.CONTROL,
// value: '',
// control: {
// type,
// code,
// value: null,
// placeholder,
// valueSets: JSON.parse(valueSets)
// }
// }
// ])
// }
// })
// break
// case ControlType.CHECKBOX:
// new Dialog({
// title: '复选框控件',
// data: [
// {
// type: 'text',
// label: '默认值',
// name: 'code',
// placeholder: '请输入默认值,多个值以英文逗号分割'
// },
// {
// type: 'textarea',
// label: '值集',
// name: 'valueSets',
// required: true,
// height: 100,
// placeholder: `请输入值集JSON,例:\n[{\n"value":"有",\n"code":"98175"\n}]`
// }
// ],
// onConfirm: payload => {
// const valueSets = payload.find(p => p.name === 'valueSets')?.value
// if (!valueSets) return
// const code = payload.find(p => p.name === 'code')?.value
// instance.command.executeInsertElementList([
// {
// type: ElementType.CONTROL,
// value: '',
// control: {
// type,
// code,
// value: null,
// valueSets: JSON.parse(valueSets)
// }
// }
// ])
// }
// })
// break
// default:
// break
// }
// }
// const checkboxDom = document.querySelector(
// '.menu-item__checkbox'
// )
// checkboxDom.onclick = function () {
// console.log('checkbox')
// instance.command.executeInsertElementList([
// {
// type: ElementType.CHECKBOX,
// checkbox: {
// value: false
// },
// value: ''
// }
// ])
// }
// const latexDom = document.querySelector('.menu-item__latex')
// latexDom.onclick = function () {
// console.log('LaTeX')
// new Dialog({
// title: 'LaTeX',
// data: [
// {
// type: 'textarea',
// height: 100,
// name: 'value',
// placeholder: '请输入LaTeX文本'
// }
// ],
// onConfirm: payload => {
// const value = payload.find(p => p.name === 'value')?.value
// if (!value) return
// instance.command.executeInsertElementList([
// {
// type: ElementType.LATEX,
// value
// }
// ])
// }
// })
// }
// const dateDom = document.querySelector('.menu-item__date')
// const dateDomOptionDom = dateDom.querySelector('.options')
// dateDom.onclick = function () {
// console.log('date')
// dateDomOptionDom.classList.toggle('visible')
// // 定位调整
// const bodyRect = document.body.getBoundingClientRect()
// const dateDomOptionRect = dateDomOptionDom.getBoundingClientRect()
// if (dateDomOptionRect.left + dateDomOptionRect.width > bodyRect.width) {
// dateDomOptionDom.style.right = '0px'
// dateDomOptionDom.style.left = 'unset'
// } else {
// dateDomOptionDom.style.right = 'unset'
// dateDomOptionDom.style.left = '0px'
// }
// // 当前日期
// const date = new Date()
// const year = date.getFullYear().toString()
// const month = (date.getMonth() + 1).toString().padStart(2, '0')
// const day = date.getDate().toString().padStart(2, '0')
// const hour = date.getHours().toString().padStart(2, '0')
// const minute = date.getMinutes().toString().padStart(2, '0')
// const second = date.getSeconds().toString().padStart(2, '0')
// const dateString = `${year}-${month}-${day}`
// const dateTimeString = `${dateString} ${hour}:${minute}:${second}`
// dateDomOptionDom.querySelector('li:first-child').innerText = dateString
// dateDomOptionDom.querySelector('li:last-child').innerText = dateTimeString
// }
// dateDomOptionDom.onmousedown = function (evt) {
// const li = evt.target
// const dateFormat = li.dataset.format
// dateDomOptionDom.classList.toggle('visible')
// instance.command.executeInsertElementList([
// {
// type: ElementType.DATE,
// value: '',
// dateFormat,
// valueList: [
// {
// value: li.innerText.trim()
// }
// ]
// }
// ])
// }
// const blockDom = document.querySelector('.menu-item__block')
// blockDom.onclick = function () {
// console.log('block')
// new Dialog({
// title: '内容块',
// data: [
// {
// type: 'select',
// label: '类型',
// name: 'type',
// value: 'iframe',
// required: true,
// options: [
// {
// label: '网址',
// value: 'iframe'
// },
// {
// label: '视频',
// value: 'video'
// }
// ]
// },
// {
// type: 'number',
// label: '宽度',
// name: 'width',
// placeholder: '请输入宽度(默认页面内宽度)'
// },
// {
// type: 'number',
// label: '高度',
// name: 'height',
// required: true,
// placeholder: '请输入高度'
// },
// {
// type: 'textarea',
// label: '地址',
// height: 100,
// name: 'value',
// required: true,
// placeholder: '请输入地址'
// }
// ],
// onConfirm: payload => {
// const type = payload.find(p => p.name === 'type')?.value
// if (!type) return
// const value = payload.find(p => p.name === 'value')?.value
// if (!value) return
// const width = payload.find(p => p.name === 'width')?.value
// const height = payload.find(p => p.name === 'height')?.value
// if (!height) return
// const block = {
// type: null
// }
// if (block.type === BlockType.IFRAME) {
// block.iframeBlock = {
// src: value
// }
// } else if (block.type === BlockType.VIDEO) {
// block.videoBlock = {
// src: value
// }
// }
// const blockElemen = {
// type: ElementType.BLOCK,
// value: '',
// height: Number(height),
// block
// }
// if (width) {
// blockElement.width = Number(width)
// }
// instance.command.executeInsertElementList([blockElement])
// }
// })
// }
// 5. | 搜索&替换 | 打印 |
const searchCollapseDom = document.querySelector(
'.menu-item__search__collapse'
)
const searchInputDom = document.querySelector(
'.menu-item__search__collapse__search input'
)
const replaceInputDom = document.querySelector(
'.menu-item__search__collapse__replace input'
)
const searchDom =
document.querySelector('.menu-item__search')
searchDom.title = `搜索与替换(${isApple ? '⌘' : 'Ctrl'}+F)`
const searchResultDom =
searchCollapseDom.querySelector('.search-result')
function setSearchResult() {
const result = instance.command.getSearchNavigateInfo()
if (result) {
const { index, count } = result
searchResultDom.innerText = `${index}/${count}`
} else {
searchResultDom.innerText = ''
}
}
searchDom.onclick = function () {
console.log('search')
searchCollapseDom.style.display = 'block'
const bodyRect = document.body.getBoundingClientRect()
const searchRect = searchDom.getBoundingClientRect()
const searchCollapseRect = searchCollapseDom.getBoundingClientRect()
if (searchRect.left + searchCollapseRect.width > bodyRect.width) {
searchCollapseDom.style.right = '0px'
searchCollapseDom.style.left = 'unset'
} else {
searchCollapseDom.style.right = 'unset'
}
searchInputDom.focus()
}
searchCollapseDom.querySelector('span').onclick =
function () {
searchCollapseDom.style.display = 'none'
searchInputDom.value = ''
replaceInputDom.value = ''
instance.command.executeSearch(null)
setSearchResult()
}
searchInputDom.oninput = function () {
instance.command.executeSearch(searchInputDom.value || null)
setSearchResult()
}
searchInputDom.onkeydown = function (evt) {
if (evt.key === 'Enter') {
instance.command.executeSearch(searchInputDom.value || null)
setSearchResult()
}
}
searchCollapseDom.querySelector('button').onclick =
function () {
const searchValue = searchInputDom.value
const replaceValue = replaceInputDom.value
if (searchValue && replaceValue && searchValue !== replaceValue) {
instance.command.executeReplace(replaceValue)
}
}
searchCollapseDom.querySelector('.arrow-left').onclick =
function () {
instance.command.executeSearchNavigatePre()
setSearchResult()
}
searchCollapseDom.querySelector('.arrow-right').onclick =
function () {
instance.command.executeSearchNavigateNext()
setSearchResult()
}
// const printDom = document.querySelector('.menu-item__print')
// printDom.title = `打印(${isApple ? '⌘' : 'Ctrl'}+P)`
// printDom.onclick = function () {
// console.log('print')
// instance.command.executePrint()
// }
// 6. 目录显隐 | 页面模式 | 纸张缩放 | 纸张大小 | 纸张方向 | 页边距 | 全屏
// async function updateCatalog() {
// const catalog = await instance.command.getCatalog()
// const catalogMainDom =
// document.querySelector('.catalog__main')
// catalogMainDom.innerHTML = ''
// if (catalog) {
// const appendCatalog = (
// parent,
// catalogItems
// ) => {
// for (let c = 0; c < catalogItems.length; c++) {
// const catalogItem = catalogItems[c]
// const catalogItemDom = document.createElement('div')
// catalogItemDom.classList.add('catalog-item')
// // 渲染
// const catalogItemContentDom = document.createElement('div')
// catalogItemContentDom.classList.add('catalog-item__content')
// const catalogItemContentSpanDom = document.createElement('span')
// catalogItemContentSpanDom.innerText = catalogItem.name
// catalogItemContentDom.append(catalogItemContentSpanDom)
// // 定位
// catalogItemContentDom.onclick = () => {
// instance.command.executeLocationCatalog(catalogItem.id)
// }
// catalogItemDom.append(catalogItemContentDom)
// if (catalogItem.subCatalog && catalogItem.subCatalog.length) {
// appendCatalog(catalogItemDom, catalogItem.subCatalog)
// }
// // 追加
// parent.append(catalogItemDom)
// }
// }
// appendCatalog(catalogMainDom, catalog)
// }
// }
// let isCatalogShow = true
// const catalogDom = document.querySelector('.catalog')
// const catalogModeDom =
// document.querySelector('.catalog-mode')
// const catalogHeaderCloseDom = document.querySelector(
// '.catalog__header__close'
// )
// const switchCatalog = () => {
// console.log('目录', isCatalogShow)
// isCatalogShow = !isCatalogShow
// if (isCatalogShow) {
// console.log('目录', isCatalogShow)
// catalogDom.style.display = 'block'
// updateCatalog()
// } else {
// catalogDom.style.display = 'none'
// }
// }
// catalogModeDom.onclick = switchCatalog
// catalogHeaderCloseDom.onclick = switchCatalog
const pageModeDom = document.querySelector('.page-mode')
const pageModeOptionsDom =
pageModeDom.querySelector('.options')
pageModeDom.onclick = function () {
pageModeOptionsDom.classList.toggle('visible')
}
pageModeOptionsDom.onclick = function (evt) {
const li = evt.target
instance.command.executePageMode(li.dataset.pageMode)
}
document.querySelector('.page-scale-percentage').onclick =
function () {
console.log('page-scale-recovery')
instance.command.executePageScaleRecovery()
}
document.querySelector('.page-scale-minus').onclick =
function () {
console.log('page-scale-minus')
instance.command.executePageScaleMinus()
}
document.querySelector('.page-scale-add').onclick =
function () {
console.log('page-scale-add')
instance.command.executePageScaleAdd()
}
// 纸张大小
const paperSizeDom = document.querySelector('.paper-size')
const paperSizeDomOptionsDom =
paperSizeDom.querySelector('.options')
paperSizeDom.onclick = function () {
paperSizeDomOptionsDom.classList.toggle('visible')
}
paperSizeDomOptionsDom.onclick = function (evt) {
const li = evt.target
const paperType = li.dataset.paperSize
const [width, height] = paperType.split('*').map(Number)
instance.command.executePaperSize(width, height)
// 纸张状态回显
paperSizeDomOptionsDom
.querySelectorAll('li')
.forEach(child => child.classList.remove('active'))
li.classList.add('active')
}
// 纸张方向
const paperDirectionDom =
document.querySelector('.paper-direction')
const paperDirectionDomOptionsDom =
paperDirectionDom.querySelector('.options')
paperDirectionDom.onclick = function () {
paperDirectionDomOptionsDom.classList.toggle('visible')
}
paperDirectionDomOptionsDom.onclick = function (evt) {
const li = evt.target
const paperDirection = li.dataset.paperDirection
instance.command.executePaperDirection(paperDirection)
// 纸张方向状态回显
paperDirectionDomOptionsDom
.querySelectorAll('li')
.forEach(child => child.classList.remove('active'))
li.classList.add('active')
}
// 页面边距
const paperMarginDom =
document.querySelector('.paper-margin')
paperMarginDom.onclick = function () {
const [topMargin, rightMargin, bottomMargin, leftMargin] =
instance.command.getPaperMargin()
new Dialog({
title: '页边距',
data: [
{
type: 'text',
label: '上边距',
name: 'top',
required: true,
value: `${topMargin}`,
placeholder: '请输入上边距'
},
{
type: 'text',
label: '下边距',
name: 'bottom',
required: true,
value: `${bottomMargin}`,
placeholder: '请输入下边距'
},
{
type: 'text',
label: '左边距',
name: 'left',
required: true,
value: `${leftMargin}`,
placeholder: '请输入左边距'
},
{
type: 'text',
label: '右边距',
name: 'right',
required: true,
value: `${rightMargin}`,
placeholder: '请输入右边距'
}
],
onConfirm: payload => {
const top = payload.find(p => p.name === 'top')?.value
if (!top) return
const bottom = payload.find(p => p.name === 'bottom')?.value
if (!bottom) return
const left = payload.find(p => p.name === 'left')?.value
if (!left) return
const right = payload.find(p => p.name === 'right')?.value
if (!right) return
instance.command.executeSetPaperMargin([
Number(top),
Number(right),
Number(bottom),
Number(left)
])
}
})
}
// 全屏
const fullscreenDom = document.querySelector('.fullscreen')
fullscreenDom.onclick = toggleFullscreen
window.addEventListener('keydown', evt => {
if (evt.key === 'F11') {
toggleFullscreen()
evt.preventDefault()
}
})
document.addEventListener('fullscreenchange', () => {
fullscreenDom.classList.toggle('exist')
})
function toggleFullscreen() {
console.log('fullscreen')
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen()
} else {
document.exitFullscreen()
}
}
// 7. 编辑器使用模式
let modeIndex = 0
const modeList = [{
mode: EditorMode.READONLY,
name: '只读模式'
},
{
mode: EditorMode.EDIT,
name: '编辑模式'
},
{
mode: EditorMode.CLEAN,
name: '清洁模式'
},
{
mode: EditorMode.FORM,
name: '表单模式'
},
{
mode: EditorMode.PRINT,
name: '打印模式'
}
]
const modeElement = document.querySelector('.editor-mode')
// 初始设置只读模式
const { name, mode } = modeList[modeIndex]
modeElement.innerText = name
instance.command.executeMode(mode)
// 设置菜单栏权限视觉反馈
const isReadonly = mode === EditorMode.READONLY
const enableMenuList = ['search', 'print']
document.querySelectorAll('.menu-item>div').forEach(dom => {
const menu = dom.dataset.menu
isReadonly && (!menu || !enableMenuList.includes(menu))
? dom.classList.add('disable')
: dom.classList.remove('disable')
})
// modeElement.onclick = function () {
// // 模式选择循环
// modeIndex === modeList.length - 1 ? (modeIndex = 0) : modeIndex++
// // 设置模式
// const { name, mode } = modeList[modeIndex]
// modeElement.innerText = name
// console.log(1212, name)
// instance.command.executeMode(mode)
// // 设置菜单栏权限视觉反馈
// const isReadonly = mode === EditorMode.READONLY
// const enableMenuList = ['search', 'print']
// document.querySelectorAll('.menu-item>div').forEach(dom => {
// const menu = dom.dataset.menu
// isReadonly && (!menu || !enableMenuList.includes(menu))
// ? dom.classList.add('disable')
// : dom.classList.remove('disable')
// })
// }
// 模拟批注
// const commentDom = document.querySelector('.comment')
// const updateComment = async() => {
// const groupIds = await instance.command.getGroupIds()
// for (const comment of this.commentList) {
// const activeCommentDom = commentDom.querySelector(
// `.comment-item[data-id='${comment.id}']`
// )
// // 编辑器是否存在对应成组id
// if (!groupIds.includes(comment.id)) {
// // 当前dom是否存在-不存在则追加
// if (!activeCommentDom) {
// const commentItem = document.createElement('div')
// commentItem.classList.add('comment-item')
// commentItem.setAttribute('data-id', comment.id)
// commentItem.onclick = () => {
// instance.command.executeLocationGroup(comment.id)
// }
// commentDom.append(commentItem)
// // 选区信息
// const commentItemTitle = document.createElement('div')
// commentItemTitle.classList.add('comment-item__title')
// commentItemTitle.append(document.createElement('span'))
// const commentItemTitleContent = document.createElement('span')
// commentItemTitleContent.innerText = comment.rangeText
// commentItemTitle.append(commentItemTitleContent)
// const closeDom = document.createElement('i')
// closeDom.onclick = () => {
// instance.command.executeDeleteGroup(comment.id)
// }
// commentItemTitle.append(closeDom)
// commentItem.append(commentItemTitle)
// // 基础信息
// const commentItemInfo = document.createElement('div')
// commentItemInfo.classList.add('comment-item__info')
// const commentItemInfoName = document.createElement('span')
// commentItemInfoName.innerText = comment.userName
// const commentItemInfoDate = document.createElement('span')
// commentItemInfoDate.innerText = comment.createdDate
// commentItemInfo.append(commentItemInfoName)
// commentItemInfo.append(commentItemInfoDate)
// commentItem.append(commentItemInfo)
// // 详细评论
// const commentItemContent = document.createElement('div')
// commentItemContent.classList.add('comment-item__content')
// commentItemContent.innerText = comment.content
// commentItem.append(commentItemContent)
// commentDom.append(commentItem)
// }
// } else {
// // 编辑器内不存在对应成组id则dom则移除
// activeCommentDom?.remove()
// }
// }
// }
// 8. 内部事件监听
instance.listener.rangeStyleChange = function (payload) {
// 控件类型
payload.type === ElementType.SUBSCRIPT
? subscriptDom.classList.add('active')
: subscriptDom.classList.remove('active')
payload.type === ElementType.SUPERSCRIPT
? superscriptDom.classList.add('active')
: superscriptDom.classList.remove('active')
payload.type === ElementType.SEPARATOR
? separatorDom.classList.add('active')
: separatorDom.classList.remove('active')
separatorOptionDom
.querySelectorAll('li')
.forEach(li => li.classList.remove('active'))
if (payload.type === ElementType.SEPARATOR) {
const separator = payload.dashArray.join(',') || '0,0'
const curSeparatorDom = separatorOptionDom.querySelector(
`[data-separator='${separator}']`
)
if (curSeparatorDom) {
curSeparatorDom.classList.add('active')
}
}
// 富文本
fontOptionDom.querySelectorAll('li').forEach(li => li.classList.remove('active'))
const curFontDom = fontOptionDom.querySelector(
`[data-family='${payload.font}']`
)
if (curFontDom) {
fontSelectDom.innerText = curFontDom.innerText
fontSelectDom.style.fontFamily = payload.font
curFontDom.classList.add('active')
}
sizeOptionDom
.querySelectorAll('li')
.forEach(li => li.classList.remove('active'))
const curSizeDom = sizeOptionDom.querySelector(
`[data-size='${payload.size}']`
)
if (curSizeDom) {
sizeSelectDom.innerText = curSizeDom.innerText
curSizeDom.classList.add('active')
} else {
sizeSelectDom.innerText = `${payload.size}`
}
payload.bold
? boldDom.classList.add('active')
: boldDom.classList.remove('active')
payload.italic
? italicDom.classList.add('active')
: italicDom.classList.remove('active')
payload.underline
? underlineDom.classList.add('active')
: underlineDom.classList.remove('active')
payload.strikeout
? strikeoutDom.classList.add('active')
: strikeoutDom.classList.remove('active')
if (payload.color) {
colorDom.classList.add('active')
colorControlDom.value = payload.color
colorSpanDom.style.backgroundColor = payload.color
} else {
colorDom.classList.remove('active')
colorControlDom.value = '#000000'
colorSpanDom.style.backgroundColor = '#000000'
}
if (payload.highlight) {
highlightDom.classList.add('active')
highlightControlDom.value = payload.highlight
highlightSpanDom.style.backgroundColor = payload.highlight
} else {
highlightDom.classList.remove('active')
highlightControlDom.value = '#ffff00'
highlightSpanDom.style.backgroundColor = '#ffff00'
}
// 行布局
leftDom.classList.remove('active')
centerDom.classList.remove('active')
rightDom.classList.remove('active')
alignmentDom.classList.remove('active')
if (payload.rowFlex && payload.rowFlex === 'right') {
rightDom.classList.add('active')
} else if (payload.rowFlex && payload.rowFlex === 'center') {
centerDom.classList.add('active')
} else if (payload.rowFlex && payload.rowFlex === 'alignment') {
alignmentDom.classList.add('active')
} else {
leftDom.classList.add('active')
}
// 行间距
rowOptionDom
.querySelectorAll('li')
.forEach(li => li.classList.remove('active'))
const curRowMarginDom = rowOptionDom.querySelector(
`[data-rowmargin='${payload.rowMargin}']`
)
curRowMarginDom.classList.add('active')
// 功能
payload.undo
? undoDom.classList.remove('no-allow')
: undoDom.classList.add('no-allow')
payload.redo
? redoDom.classList.remove('no-allow')
: redoDom.classList.add('no-allow')
payload.painter
? painterDom.classList.add('active')
: painterDom.classList.remove('active')
// 标题
titleOptionDom
.querySelectorAll('li')
.forEach(li => li.classList.remove('active'))
if (payload.level) {
const curTitleDom = titleOptionDom.querySelector(
`[data-level='${payload.level}']`
)
titleSelectDom.innerText = curTitleDom.innerText
curTitleDom.classList.add('active')
} else {
titleSelectDom.innerText = '正文'
titleOptionDom.querySelector('li:first-child').classList.add('active')
}
// 列表
listOptionDom
.querySelectorAll('li')
.forEach(li => li.classList.remove('active'))
if (payload.listType) {
listDom.classList.add('active')
const listType = payload.listType
const listStyle =
payload.listType === ListType.OL ? ListStyle.DECIMAL : payload.listType
const curListDom = listOptionDom.querySelector(
`[data-list-type='${listType}'][data-list-style='${listStyle}']`
)
if (curListDom) {
curListDom.classList.add('active')
}
} else {
listDom.classList.remove('active')
}
// 批注
// commentDom
// .querySelectorAll('.comment-item')
// .forEach(commentItemDom => {
// commentItemDom.classList.remove('active')
// })
// if (payload.groupIds) {
// const [id] = payload.groupIds
// const activeCommentDom = commentDom.querySelector(
// `.comment-item[data-id='${id}']`
// )
// if (activeCommentDom) {
// activeCommentDom.classList.add('active')
// scrollIntoView(commentDom, activeCommentDom)
// }
// }
}
instance.listener.visiblePageNoListChange = function (payload) {
const text = payload.map(i => i + 1).join('、')
document.querySelector('.page-no-list').innerText = text
}
instance.listener.pageSizeChange = function (payload) {
if(document.querySelector('.page-size')) {
document.querySelector(
'.page-size'
).innerText = payload.toString()
}
}
instance.listener.intersectionPageNoChange = function (payload) {
document.querySelector('.page-no').innerText = `${
payload + 1
}`
}
instance.listener.pageScaleChange = function (payload) {
document.querySelector(
'.page-scale-percentage'
).innerText = `${Math.floor(payload * 10 * 10)}%`
}
instance.listener.controlChange = function (payload) {
const disableMenusInControlContext = [
'table',
'hyperlink',
'separator',
'page-break'
]
// 菜单操作权限
disableMenusInControlContext.forEach(menu => {
const menuDom = document.querySelector(
`.menu-item__${menu}`
)
payload
? menuDom.classList.add('disable')
: menuDom.classList.remove('disable')
})
}
instance.listener.pageModeChange = function (payload) {
const activeMode = pageModeOptionsDom.querySelector(
`[data-page-mode='${payload}']`
)
pageModeOptionsDom
.querySelectorAll('li')
.forEach(li => li.classList.remove('active'))
activeMode.classList.add('active')
}
const handleContentChange = async () => {
this.$emit('isSave', true)
// 字数
const wordCount = await instance.command.getWordCount()
document.querySelector('.word-count').innerText = `${
wordCount || 0
}`
// 目录
// if (isCatalogShow) {
// this.$nextTick(() => {
// updateCatalog()
// })
// }
// // 批注
// this.$nextTick(() => {
// updateComment()
// })
}
instance.listener.contentChange = this.debounce(handleContentChange, 200)
handleContentChange()
// 9. 右键菜单注册
instance.register.contextMenuList([
{
name: '批注',
when: payload => {
return (
!payload.isReadonly &&
payload.editorHasSelection &&
payload.zone === EditorZone.MAIN
)
},
callback: (command) => {
new Dialog({
title: '批注',
data: [
{
type: 'textarea',
label: '批注',
height: 100,
name: 'value',
required: true,
placeholder: '请输入批注'
}
],
onConfirm: payload => {
const value = payload.find(p => p.name === 'value')?.value
if (!value) return
const groupId = command.executeSetGroup()
if (!groupId) return
commentList.push({
id: groupId,
content: value,
userName: 'Hufe',
rangeText: command.getRangeText(),
createdDate: new Date().toLocaleString()
})
}
})
}
},
{
name: '签名',
icon: 'signature',
when: payload => {
return !payload.isReadonly && payload.editorTextFocus
},
callback: (command) => {
new Signature({
onConfirm(payload) {
if (!payload) return
const { value, width, height } = payload
if (!value || !width || !height) return
command.executeInsertElementList([
{
value,
width,
height,
type: ElementType.IMAGE
}
])
}
})
}
},
{
name: '格式整理',
icon: 'word-tool',
when: payload => {
return !payload.isReadonly
},
callback: (command) => {
command.executeWordTool()
}
}
])
// 10. 快捷键注册
instance.register.shortcutList([
{
key: KeyMap.P,
mod: true,
isGlobal: true,
callback: (command) => {
command.executePrint()
}
},
{
key: KeyMap.F,
mod: true,
isGlobal: true,
callback: (command) => {
const text = command.getRangeText()
searchDom.click()
if (text) {
searchInputDom.value = text
instance.command.executeSearch(text)
setSearchResult()
}
}
},
{
key: KeyMap.MINUS,
ctrl: true,
isGlobal: true,
callback: (command) => {
command.executePageScaleMinus()
}
},
{
key: KeyMap.EQUAL,
ctrl: true,
isGlobal: true,
callback: (command) => {
command.executePageScaleAdd()
}
},
{
key: KeyMap.ZERO,
ctrl: true,
isGlobal: true,
callback: (command) => {
command.executePageScaleRecovery()
}
}
])
}
};
</script>
我的实现效果:
我的完整CanvasEditor封装组件源码已附上,仅供参考!!!
可根据以上步骤自定义集成,多查阅官方文档即可。
end~
希望记录的问题能帮助到你~