vue2项目集成 canvas-editor 富文本编辑器; canvas-ediotr并不是一款开箱即用的插件, 需要通过下载源码来进行手动集成到项目中;
源码地址: https://github.com/Hufe921/canvas-editor
官方文档: https://hufe.club/canvas-editor-docs/guide/schema.html
本地运行结果:
Canvas-Editor环境配置
canvas-editor通过 vue3 + TypeScript 进行编写的, 考虑到大部分的 vue2项目中并没有集成TypeScript的环境, 要先进行环境的配置,需要配置的环境如下: npm install, 可能会出现报错,若是报错,建议 使用 npm clean 之后, 继续执行 npm install;
package.json
"dependencies": { "core-js": "^3.8.3", "vue": "^2.6.14", "@hufe921/canvas-editor": "^0.9.86", "@types/prismjs": "^1.26.0", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", "css-loader": "^6.5.0", "style-loader": "^2.0.0", "ts-loader": "^9.5.1", "vue-loader": "^15.9.7", "webpack": "^5.74.0" }
复制
创建 tsconfig.json 服务于 TypeScript
tsconfig.json
{ "compilerOptions": { "target": "es5", "module": "esnext", "lib": ["es2015","dom"], "strict": true, "jsx": "preserve", "importHelpers": true, "moduleResolution": "node", "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "sourceMap": true, "baseUrl": "..", "paths": { "@/*": ["src/*"] } }, "include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx", "tests/**/*.vue"], "exclude": ["node_modules","dist"] }
复制
修改 vue.config.js文件
const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, // 添加上 configureWebpack 配置 configureWebpack: { module: { rules: [ { test: /\.ts$/, loader: 'ts-loader', options: { appendTsSuffixTo: [/\.vue$/] }, exclude: /node_modules/ } ] }, resolve: { extensions: ['.ts', '.js', '.vue', '.json'], alias: { '@': require('path').resolve(__dirname, 'src') } } } })
复制
将 canvas-editor 源码中的部分文件, 复制过来
其中的 canvas.js 和 index.vue 文件是根据源代码中的 main.ts 和 index.html进行了变化, 用于服务当前的项目; components文件夹中只需获取 dialog.css文件 和 signature.css文件即可;
index.vue
<template> <div class="container"> <div class="menu" editor-component="menu"> <div class="menu-item disabled-btn" > <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 disabled-btn" > <div class="menu-item__font"> <span class="select" title="字体">宋体</span> <div class="options"> <ul> <li data-family="宋体" style="font-family: '宋体';">宋体</li> <li data-family="黑体" style="font-family: '黑体';">黑体</li> <li data-family="Microsoft YaHei" style="font-family:'Microsoft YaHei';">微软雅黑</li> <li data-family="Times New Roman" style="font-family:'Times New Roman';">Times New Roman</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> <li data-size="5">5</li> <li data-size="5.5">5.5</li> <li data-size="6.5">6.5</li> <li data-size="7.5">7.5</li> <li data-size="8">8</li> <li data-size="9">9</li> <li data-size="10">10</li> <li data-size="10.5">10.5</li> <li data-size="11">11</li> <li data-size="12">12</li> <li data-size="14">14</li> <li data-size="16">16</li> <li data-size="18">18</li> <li data-size="20">20</li> <li data-size="22">22</li> <li data-size="24">24</li> <li data-size="26">26</li> <li data-size="28">28</li> <li data-size="36">36</li> <li data-size="48">48</li> <li data-size="50">50</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> <!-- 下划线 TODO --> <div class="menu-item__italic"> <i></i> </div> <div class="menu-item__underline"> <i></i> <span class="select"></span> <div class="options"> <ul> <li data-decoration-style='solid'> <i></i> </li> <li data-decoration-style='double'> <i></i> </li> <li data-decoration-style='dashed'> <i></i> </li> <li data-decoration-style='dotted'> <i></i> </li> <li data-decoration-style='wavy'> <i></i> </li> </ul> </div> </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 disabled-btn"> <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__justify"> <i></i> </div> <div class="menu-item__row-margin"> <i title="行间距"></i> <div class="options options_row-margin"> <ul> <li data-rowmargin='0.5'>0.5</li> <li data-rowmargin='0.75'>0.75</li> <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='checkbox'> <label>复选框列表:</label> <ul style="list-style-type: '☑️ ';"> <li>________</li> </ul> </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 disabled-btn"> <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" style="display: none"> <i title="超链接"></i> </div> <div class="menu-item__separator"> <i title="分割线"></i> <div class="options options_separator"> <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" style="display: none"> <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="代码块" style="display: none"> <i></i> </div> <div class="menu-item__page-break" title="分页符"> <i></i> </div> <div class="menu-item__control" style="display: none"> <i title="控件"></i> <div class="options"> <ul> <li data-control='text'>文本</li> <li data-control="select">列举</li> <li data-control="date">日期</li> <li data-control="checkbox">复选框</li> <li data-control="radio">单选框</li> </ul> </div> </div> <div class="menu-item__checkbox" title="复选框"> <i></i> </div> <div class="menu-item__radio" title="单选框"> <i></i> </div> <div class="menu-item__latex" title="LateX" style="display: none"> <i></i> </div> <div class="menu-item__date"> <i title="日期"></i> <div class="options options_date"> <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="内容块" style="display: none"> <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 disabled-btn" > <input type="text"> <button>替换</button> </div> </div> <div class="menu-item__print" data-menu="print"> <i></i> </div> </div> </div> <!-- 目录进行隐藏 --> <div class="catalog" editor-component="catalog" style="display: none"> <div class="catalog__header"> <span>目录</span> <div class="catalog__header__close"> <i></i> </div> </div> <div class="catalog__main"></div> </div> <div class="canvas-editor editor"></div> <!-- 底部栏进行隐藏 --> <div class="footer" editor-component="footer" style="display: none;"> <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 class="editor-option" title="编辑器设置"> <i></i> </div> </div> </div> </div> </template> <script> import { Init } from './canvas.js'; export default { name: 'CanvasEditor', props:{ // 父组件传递的id parentContent:{ type:Object, default:null }, }, data() { return { instance: null, }; }, watch: { // 子组件监听 parentContent 的变化,获取到父组件数据 parentContent(newVal) { if (newVal) { this.instance = Init(newVal); } } }, methods: { // 向父组件返回添加的数据 saveContent() { let content = { data: {} }; content.data = this.instance.instance.command.getValue().data; this.$emit('save-content', content); } } }; </script> <style> @import url("./style.css"); </style> <style scoped> .container { position: relative; /* 确保子元素可以相对于父元素进行定位 */ width: 100%; /* 设置父元素的宽度 */ height: calc(100vh - 120px);/* 设置父元素的高度 */ overflow: hidden; /* 根据需要设置溢出行为 */ text-align: center; } .menu { position: fixed; /* 确保其相对于最近的已定位祖先元素 */ top: 0; left: 0; width: 100%; /* 确保菜单宽度与父元素一致 */ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); /* 添加阴影以示区别 */ margin-bottom: 10px; } .menu-item .options{ width:120px; height:300px; overflow-y: scroll; } .menu-item__separator .options_separator{ height: 160px; } .menu-item__date .options_date { width: 200px; height: 80px; } .menu-item__row-margin .options_row-margin{ height: 200px; } .canvas-editor { position: static; flex-direction: column; overflow-y: scroll; background-color: #f2f4f7; height: 100%; justify-content: center; } .disabled{ pointer-events: none; opacity: 0.5; } </style>
复制
canvas.js
import Editor, { ElementType} from "@hufe921/canvas-editor"; import { debounce, nextTick } from './utils/index.ts' export function Init (content) { const isApple = typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent); const editorElement = document.querySelector('.canvas-editor'); if (!editorElement) { console.error('Element with class .canvas-editor not found.'); return; } const RowFlex = { CENTER: 'center', LEFT: 'left', RIGHT: 'right' }; const commentList = [] const instance = new Editor( editorElement, { header: content.header, main: content.main, footer: content.footer, }, // 数据 { margins: [50, 50, 50, 50], watermark: { data: '', size: 120 }, // 水印 pageNumber: { format: '第{pageNo}页/共{pageCount}页' }, placeholder: { data: '请输入正文' }, zone: { tipDisabled: false }, maskMargin: [60, 0, 30, 0] // 菜单栏高度60,底部工具栏30为遮盖层 } // 可选择项 ); Reflect.set(window, 'editor', instance); // 1.菜单弹窗销毁 window.addEventListener('click', function (evt) { const visibleDom = document.querySelector('.visible'); if (!visibleDom || visibleDom.contains(evt.target)) return; visibleDom.classList.remove('visible'); }, { capture: true }); // 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'); let isFirstClick = true; let painterTimeout; painterDom.onclick = function () { if (isFirstClick) { isFirstClick = false; painterTimeout = window.setTimeout(() => { console.log('painter-click'); isFirstClick = true; instance.command.executePainter({ isDblclick: false }); }, 200); } else { window.clearTimeout(painterTimeout); } }; painterDom.ondblclick = function () { console.log('painter-dblclick'); isFirstClick = true; window.clearTimeout(painterTimeout); 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 = `增大字号(${isApple ? '⌘' : 'Ctrl'}+[)`; sizeAddDom.onclick = function () { console.log('size-add'); instance.command.executeSizeAdd(); }; // 减小字号 const sizeMinusDom = document.querySelector('.menu-item__size-minus'); sizeMinusDom.title = `减小字号(${isApple ? '⌘' : 'Ctrl'}+])`; sizeMinusDom.onclick = function () { console.log('size-minus'); instance.command.executeSizeMinus(); }; // 加粗 const boldDom = document.querySelector('.menu-item__bold'); boldDom.title = `加粗(${isApple ? '⌘' : 'Ctrl'}+B)`; boldDom.onclick = function () { console.log('bold'); instance.command.executeBold(); }; // 斜体 const italicDom = document.querySelector('.menu-item__italic'); italicDom.title = `斜体(${isApple ? '⌘' : 'Ctrl'}+I)`; italicDom.onclick = function () { console.log('italic'); instance.command.executeItalic(); }; // 下划线 const underlineDom = document.querySelector('.menu-item__underline'); underlineDom.title = `下划线(${isApple ? '⌘' : 'Ctrl'}+U)`; const underlineOptionDom = underlineDom.querySelector('.options'); underlineDom.querySelector('.select').onclick = function () { underlineOptionDom.classList.toggle('visible'); }; underlineDom.querySelector('i').onclick = function () { console.log('underline'); instance.command.executeUnderline(); underlineOptionDom.classList.remove('visible'); }; underlineDom.querySelector('ul').onmousedown = function (evt) { const li = evt.target; const decorationStyle = li.dataset.decorationStyle; instance.command.executeUnderline({ style: decorationStyle }); }; // 删除线 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 = `上标(${isApple ? '⌘' : 'Ctrl'}+Shift+,)`; superscriptDom.onclick = function () { console.log('superscript'); instance.command.executeSuperscript(); }; // 下标 const subscriptDom = document.querySelector('.menu-item__subscript'); subscriptDom.title = `下标(${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+${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 = `左对齐(${isApple ? '⌘' : 'Ctrl'}+L)`; leftDom.onclick = function () { console.log('left'); instance.command.executeRowFlex(RowFlex.LEFT); }; const centerDom = document.querySelector('.menu-item__center'); centerDom.title = `居中对齐(${isApple ? '⌘' : 'Ctrl'}+E)`; centerDom.onclick = function () { console.log('center'); instance.command.executeRowFlex(RowFlex.CENTER); }; const rightDom = document.querySelector('.menu-item__right'); rightDom.title = `右对齐(${isApple ? '⌘' : 'Ctrl'}+R)`; rightDom.onclick = function () { console.log('right'); instance.command.executeRowFlex(RowFlex.RIGHT); }; const alignmentDom = document.querySelector('.menu-item__alignment'); alignmentDom.title = `两端对齐(${isApple ? '⌘' : 'Ctrl'}+J)`; alignmentDom.onclick = function () { console.log('alignment'); instance.command.executeRowFlex(RowFlex.ALIGNMENT); }; const justifyDom = document.querySelector('.menu-item__justify'); justifyDom.title = `分散对齐(${isApple ? '⌘' : 'Ctrl'}+Shift+J)`; justifyDom.onclick = function () { console.log('justify'); instance.command.executeRowFlex('justify'); }; // 行间距 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 = `列表(${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'); // Draw rows and columns 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.appendChild(td); trCellList.push(td); } tablePanel.appendChild(tr); tableCellList.push(trCellList); } let colIndex = 0; let rowIndex = 0; // Remove all table cell selections function removeAllTableCellSelect() { tableCellList.forEach(tr => { tr.forEach(td => td.classList.remove('active')); }); } // Set table title content function setTableTitle(payload) { tableTitle.innerText = payload; } // Restore initial state function recoveryTable() { removeAllTableCellSelect(); setTableTitle('插入'); colIndex = 0; rowIndex = 0; 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 = fileReader.result; image.src = value; image.onload = function () { instance.command.executeImage({ value, width: image.width, height: image.height }); imageFileDom.value = ''; }; }; }; 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 checkboxDom = document.querySelector('.menu-item__checkbox'); checkboxDom.onclick = function () { console.log('checkbox'); instance.command.executeInsertElementList([ { type: ElementType.CHECKBOX, checkbox: { value: false }, value: '' } ]); }; const radioDom = document.querySelector('.menu-item__radio'); radioDom.onclick = function () { console.log('radio'); instance.command.executeInsertElementList([ { type: ElementType.RADIO, checkbox: { value: false }, value: '' } ]); }; const dateDom = document.querySelector('.menu-item__date'); const dateDomOptionDom = dateDom.querySelector('.options'); dateDom.onclick = function () { console.log('date'); dateDomOptionDom.classList.toggle('visible'); // Adjust position 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'; } // Current date 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() } ] } ]); }; // 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'); // Render const catalogItemContentDom = document.createElement('div'); catalogItemContentDom.classList.add('catalog-item__content'); const catalogItemContentSpanDom = document.createElement('span'); catalogItemContentSpanDom.innerText = catalogItem.name; catalogItemContentDom.append(catalogItemContentSpanDom); // Location catalogItemContentDom.onclick = () => { instance.command.executeLocationCatalog(catalogItem.id); }; catalogItemDom.append(catalogItemContentDom); if (catalogItem.subCatalog && catalogItem.subCatalog.length) { appendCatalog(catalogItemDom, catalogItem.subCatalog); } // Append 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 = () => { isCatalogShow = !isCatalogShow; if (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(); }; // Paper Size 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); // Paper status echo 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 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: 'EDIT', // EditorMode.EDIT name: '编辑模式' }, { mode: 'CLEAN', // EditorMode.CLEAN name: '清洁模式' }, { mode: 'READONLY', // EditorMode.READONLY name: '只读模式' }, { mode: 'FORM', // EditorMode.FORM name: '表单模式' }, { mode: 'PRINT', // EditorMode.PRINT name: '打印模式' } ]; const modeElement = document.querySelector('.editor-mode'); modeElement.onclick = function () { // 模式选择循环 modeIndex === modeList.length - 1 ? (modeIndex = 0) : modeIndex++; // 设置模式 const {name, mode} = modeList[modeIndex]; modeElement.innerText = name; instance.command.executeMode(mode); // 设置菜单栏权限视觉反馈 const isReadonly = mode === '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'); async function updateComment() { const groupIds = await instance.command.getGroupIds(); for (const comment of 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 === 'SUBSCRIPT' ? subscriptDom.classList.add('active') : subscriptDom.classList.remove('active'); payload.type === 'SUPERSCRIPT' ? superscriptDom.classList.add('active') : superscriptDom.classList.remove('active'); payload.type === 'SEPARATOR' ? separatorDom.classList.add('active') : separatorDom.classList.remove('active'); separatorOptionDom.querySelectorAll('li').forEach(li => li.classList.remove('active')); if (payload.type === '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'); justifyDom.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 if (payload.rowFlex && payload.rowFlex === 'justify') { justifyDom.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 === 'OL' ? 'DECIMAL' : payload.listType; const curListDom = listOptionDom.querySelector(`[data-list-type='${listType}'][data-list-style='${listType}']`); if (curListDom) { curListDom.classList.add('active'); } } else { listDom.classList.remove('active'); } } // 控件变更监听 instance.listener.controlChange = function (payload) { const disableMenusInControlContext = [ 'table', 'hyperlink', 'separator', 'page-break', 'control' ]; // 菜单操作权限 disableMenusInControlContext.forEach(menu => { const menuDom = document.querySelector(`.menu-item__${menu}`); if (menuDom) { payload ? menuDom.classList.add('disable') : menuDom.classList.remove('disable'); } }); }; // 页面模式变更监听 instance.listener.pageModeChange = function (payload) { const activeMode = pageModeOptionsDom.querySelector(`[data-page-mode='${payload}']`); if (activeMode) { pageModeOptionsDom.querySelectorAll('li').forEach(li => li.classList.remove('active')); activeMode.classList.add('active'); } }; // 内容变更处理函数 const handleContentChange = async function () { // 字数 const wordCount = await instance.command.getWordCount(); const wordCountDom = document.querySelector('.word-count'); if (wordCountDom) { wordCountDom.innerText = `${wordCount || 0}`; } // 目录 if (isCatalogShow) { nextTick(() => { updateCatalog(); }); } // 批注 nextTick(() => { updateComment(); }); }; // 内容变更监听,使用防抖函数 instance.listener.contentChange = debounce(handleContentChange, 200); handleContentChange(); // 保存监听 instance.listener.saved = function (payload) { console.log('elementList: ', payload); }; // 快捷键注册 instance.register.shortcutList([ { key: 'P', mod: true, isGlobal: true, callback: (command) => { command.executePrint(); } }, { key: 'F', mod: true, isGlobal: true, callback: (command) => { const text = command.getRangeText(); searchDom.click(); if (text) { searchInputDom.value = text; instance.command.executeSearch(text); setSearchResult(); } } }, { key: 'Minus', ctrl: true, isGlobal: true, callback: (command) => { command.executePageScaleMinus(); } }, { key: 'Equal', ctrl: true, isGlobal: true, callback: (command) => { command.executePageScaleAdd(); } }, { key: 'Zero', ctrl: true, isGlobal: true, callback: (command) => { command.executePageScaleRecovery(); } } ]); return {instance}; }
复制
将我提供的canvas.js 和 index.vue 文件进行复制放入到上图中 canvaseditor文件夹中, 即可在项目中集成 canvas-editor编辑器; 在其他文件中以子组件的形式引入 index.vue ,查看效果如下:
ParentTest.vue
模拟实现 父组件 ParentTest 和 子组件 CanvasEditor 实现通信, 以及数据的展示和存储
<template> <div> <CanvasEditor ref="canvasEditor" :parentContent="parentContent" @save-content="handleSaveCanvasEditorContent"/> <button style="width: 160px;height: 80px; border: 2px solid #2b4b6b;margin-right: 20px; float: right" @click="handleSaveContent">保 存</button> </div> </template> <script> import CanvasEditor from "@/view/canvas-editor/index.vue"; export default { name: 'ParentComponent', components: { CanvasEditor }, data() { return { parentContent:undefined, // 存放父组件传递的数据 content:undefined, // 存放子组件数据 } }, mounted() { console.log("模拟父组件向后端请求数据, 传递给子组件"); this.parentContent = { header:[ { value: "父类传递的数据", size: 12, bold: false, color: "rgb(33, 53, 71)", italic: false, }, ], main:[ { value: "父类传递的数据 通过后端获取", size: 40, bold: true, } ] } }, methods:{ handleSaveContent(){ console.log("父组件保存数据时即触发点击事件,执行 saveContent 方法获取子组件的数据"); this.$refs.canvasEditor.saveContent(); // 将获取到的子组件数据 this.content 入库处理 }, handleSaveCanvasEditorContent(data){ console.log("从子组件接收到的数据:", data); // 将data数据转换为 json 格式的数据, 方便入库处理 this.content = JSON.stringify(data); console.log("转换后的数据 this.content 为: ", this.content) } }, } </script>
复制