vue2项目集成canvas-editor文本编辑器

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 源码中的部分文件, 复制过来
在这里插入图片描述![[Pasted image 20240722134506.png]]
其中的 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>

在这里插入图片描述

  • 26
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值