源码地址: https://github.com/Hufe921/canvas-editor
官方文档: https://hufe.club/canvas-editor-docs/guide/schema.html
之前写过一篇, vue2项目集成 cnavas-ediotr 富文本编辑器的文章(文章链接),
当时的 vue2 项目是通过 vue-cli 进行创建, 并且项目中并没有 ts 配置, 当需要我们集成 canvas-editor 时配置 ts ,相较于还是比较麻烦的.
这次通过 vite 创建 vue3 项目, 并且在创建项目的过程中选择好 ts 需要的配置;
注: 本文章只针对于 vite 创建的 vue3 项目, 对于通过 vue-cli 创建的 vue3 项目不适用.
Canvas-Editor环境配置
package.json
从devDependencies和 dependencies 属性中, 选择未具有的依赖; 然后 npm install
{
"name": "vue3-canvas-ediotr",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"devDependencies": {
"vue": "^3.4.31",
"@rollup/plugin-typescript": "^10.0.1",
"@types/node": "16.18.96",
"@types/prismjs": "^1.26.0",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"@vitejs/plugin-vue": "^2.0.4",
"vite-plugin-vue-setup-extend": "^0.4.0",
"cypress": "13.6.0",
"cypress-file-upload": "^5.0.8",
"eslint": "7.32.0",
"simple-git-hooks": "^2.8.1",
"typescript": "4.8.4",
"vite": "^2.4.2",
"ts-loader": "^9.5.1",
"vite-plugin-css-injected-by-js": "^2.1.1",
"canvas-editor": "^1.0.0",
"@hufe921/canvas-editor": "^0.9.86"
},
"dependencies": {
"prismjs": "^1.27.0"
}
}
tsconfig.json
在原有的文件基础上直接进行替换 ( 根据你的需要,建议根据你本身项目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"]
}
vite.config.ts
配置文件上 添加了 setup语法糖插件 和 将@表示地址, 指向 'src’文件夹; 建议同步一下
// @ts-ignore
import {
defineConfig} from "vite";
// @ts-ignore
import vue from "@vitejs/plugin-vue";
// path config
import path from 'path'
// 导入安装的 vite-plugin-vue-setup-extend 插件
// @ts-ignore
import vueSetupExtend from "vite-plugin-vue-setup-extend";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), vueSetupExtend],
resolve:{
alias:{
'@' : path.resolve(__dirname,'src')
}
}
});
获取文件
将 canvas-editor 源码中的部分文件, 复制过来;
将复制的文件, 建议放到 src/view/canvas-editor目录下, 并且将 main.ts 改名为 canvas.ts; 而且canvas.ts代码进行了更改, 更改如(将main.ts文件内容全部替换成下面文件内容):
cnavas.ts
import './style.css'
import prism from 'prismjs'
// @ts-ignore
import Editor, {
BlockType, Command, ControlType, EditorMode, EditorZone, ElementType, IBlock, ICatalogItem, IElement, KeyMap, ListStyle, ListType, PageMode, PaperDirection, RowFlex, TextDecorationStyle, TitleLevel, splitText} from './editor'
import {
Dialog } from './components/dialog/Dialog'
import {
formatPrismToken } from './utils/prism'
// @ts-ignore
import {
debounce, nextTick, scrollIntoView } from './utils'
export default function Init (content:any) {
const isApple =
typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent)
// 1. 初始化编辑器
const container = document.querySelector<HTMLDivElement>('.editor')!
const instance = new Editor(
container,
{
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为遮盖层,
} // 可选择项
)
console.log('实例: ', instance)
// cypress使用
Reflect.set(window, 'editor', instance)
// 菜单弹窗销毁
window.addEventListener(
'click',
evt => {
const visibleDom = document.querySelector('.visible')
if (!visibleDom || visibleDom.contains(<Node>evt.target)) return
visibleDom.classList.remove('visible')
},
{
capture: true
}
)
// 2. | 撤销 | 重做 | 格式刷 | 清除格式 |
const undoDom = document.querySelector<HTMLDivElement>('.menu-item__undo')!
undoDom.title = `撤销(${
isApple ? '⌘' : 'Ctrl'}+Z)`
undoDom.onclick = function () {
console.log('undo')
instance.command.executeUndo()
}
const redoDom = document.querySelector<HTMLDivElement>('.menu-item__redo')!
redoDom.title = `重做(${
isApple ? '⌘' : 'Ctrl'}+Y)`
redoDom.onclick = function () {
console.log('redo')
instance.command.executeRedo()
}
const painterDom = document.querySelector<HTMLDivElement>(
'.menu-item__painter'
)!
let isFirstClick = true
let painterTimeout: number
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<HTMLDivElement>('.menu-item__format')!.onclick =
function () {
console.log('format')
instance.command.executeFormat()
}
// 3. | 字体 | 字体变大 | 字体变小 | 加粗 | 斜体 | 下划线 | 删除线 | 上标 | 下标 | 字体颜色 | 背景色 |
const fontDom = document.querySelector<HTMLDivElement>('.menu-item__font')!
const fontSelectDom = fontDom.querySelector<HTMLDivElement>('.select')!
const fontOptionDom = fontDom.querySelector<HTMLDivElement>('.options')!
fontDom.onclick = function () {
console.log('font')
fontOptionDom.classList.toggle('visible')
}
fontOptionDom.onclick = function (evt) {
const li = evt.target as HTMLLIElement
instance.command.executeFont(li.dataset.family!)
}
const sizeSetDom = document.querySelector<HTMLDivElement>('.menu-item__size')!
const sizeSelectDom = sizeSetDom.querySelector<HTMLDivElement>('.select')!
const sizeOptionDom = sizeSetDom.querySelector<HTMLDivElement>('.options')!
sizeSetDom.title = `设置字号`
sizeSetDom.onclick = function () {
console.log('size')
sizeOptionDom.classList.toggle('visible')
}
sizeOptionDom.onclick = function (evt) {
const li = evt.target as HTMLLIElement
instance.command.executeSize(Number(li.dataset.size!))
}
const sizeAddDom = document.querySelector<HTMLDivElement>(
'.menu-item__size-add'
)!
sizeAddDom.title = `增大字号(${
isApple ? '⌘' : 'Ctrl'}+[)`
sizeAddDom.onclick = function () {
console.log('size-add')
instance.command.executeSizeAdd()
}
const sizeMinusDom = document.querySelector<HTMLDivElement>(
'.menu-item__size-minus'
)!
sizeMinusDom.title = `减小字号(${
isApple ? '⌘' : 'Ctrl'}+])`
sizeMinusDom.onclick = function () {
console.log('size-minus')
instance.command.executeSizeMinus()
}
const boldDom = document.querySelector<HTMLDivElement>('.menu-item__bold')!
boldDom.title = `加粗(${
isApple ? '⌘' : 'Ctrl'}+B)`
boldDom.onclick = function () {
console.log('bold')
instance.command.executeBold()
}
const italicDom =
document.querySelector<HTMLDivElement>('.menu-item__italic')!
italicDom.title = `斜体(${
isApple ? '⌘' : 'Ctrl'}+I)`
italicDom.onclick = function () {
console.log('italic')
instance.command.executeItalic()
}
const underlineDom = document.querySelector<HTMLDivElement>(
'.menu-item__underline'
)!
underlineDom.title = `下划线(${
isApple ? '⌘' : 'Ctrl'}+U)`
const underlineOptionDom =
underlineDom.querySelector<HTMLDivElement>('.options')!
underlineDom.querySelector<HTMLSpanElement>('.select')!.onclick =
function () {
underlineOptionDom.classList.toggle('visible')
}
underlineDom.querySelector<HTMLElement>('i')!.onclick = function () {
console.log('underline')
instance.command.executeUnderline()
underlineOptionDom.classList.remove('visible')
}
underlineDom.querySelector<HTMLUListElement>('ul')!.onmousedown = function (
evt
) {
const li = evt.target as HTMLLIElement
const decorationStyle = <TextDecorationStyle>li.dataset.decorationStyle
instance.command.executeUnderline({
style: decorationStyle
})
underlineOptionDom.classList.remove('visible')
}
const strikeoutDom = document.querySelector<HTMLDivElement>(
'.menu-item__strikeout'
)!
strikeoutDom.onclick = function () {
console.log('strikeout')
instance.command.executeStrikeout()
}
const superscriptDom = document.querySelector<HTMLDivElement>(
'.menu-item__superscript'
)!
superscriptDom.title = `上标(${
isApple ? '⌘' : 'Ctrl'}+Shift+,)`
superscriptDom.onclick = function () {
console.log('superscript')
instance.command.executeSuperscript()
}
const subscriptDom = document.querySelector<HTMLDivElement>(
'.menu-item__subscript'
)!
subscriptDom.title = `下标(${
isApple ? '⌘' : 'Ctrl'}+Shift+.)`
subscriptDom.onclick = function () {
console.log('subscript')
instance.command.executeSubscript()
}
const colorControlDom = document.querySelector<HTMLInputElement>('#color')!
colorControlDom.oninput = function () {
instance.command.executeColor(colorControlDom.value)
}
const colorDom = document.querySelector<HTMLDivElement>('.menu-item__color')!
const colorSpanDom = colorDom.querySelector('span')!
colorDom.onclick = function () {
console.log('color')
colorControlDom.click()
}
const highlightControlDom =
document.querySelector<HTMLInputElement>('#highlight')!
highlightControlDom.oninput = function () {
instance.command.executeHighlight(highlightControlDom.value)
}
const highlightDom = document.querySelector<HTMLDivElement>(
'.menu-item__highlight'
)!
const highlightSpanDom = highlightDom.querySelector('span')!
highlightDom.onclick = function () {
console.log('highlight')
highlightControlDom?.click()
}
const titleDom = document.querySelector<HTMLDivElement>('.menu-item__title')!
const titleSelectDom = titleDom.querySelector<HTMLDivElement>('.select')!
const titleOptionDom = titleDom.querySelector<HTMLDivElement>('.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 as HTMLLIElement
const level = <TitleLevel>li.dataset.level
instance.command.executeTitle(level || null)
}
const leftDom = document.querySelector<HTMLDivElement>('.menu-item__left')!
leftDom.title = `左对齐(${
isApple ? '⌘' : 'Ctrl'}+L)`
leftDom.onclick = function () {
console.log('left')
instance.command.executeRowFlex(RowFlex.LEFT)
}
const centerDom =
document.querySelector<HTMLDivElement>('.menu-item__center')!
centerDom.title = `居中对齐(${
isApple ? '⌘' : 'Ctrl'}+E)`
centerDom.onclick = function () {
console.log('center')
instance.command.executeRowFlex(RowFlex.CENTER)
}
const rightDom = document.querySelector<HTMLDivElement>('.menu-item__right')!
rightDom.title = `右对齐(${
isApple ? '⌘' : 'Ctrl'}+R)`
rightDom.onclick = function () {
console.log('right')
instance.command.executeRowFlex(RowFlex.RIGHT)
}
const alignmentDom = document.querySelector<HTMLDivElement>(
'.menu-item__alignment'
)!
alignmentDom.title = `两端对齐(${
isApple ? '⌘' : 'Ctrl'}+J)`
alignmentDom.onclick = function () {
console.log('alignment')
instance.command.executeRowFlex(RowFlex.ALIGNMENT)
}
const justifyDom = document.querySelector<HTMLDivElement>(
'.menu-item__justify'
)!
justifyDom.title = `分散对齐(${
isApple ? '⌘' : 'Ctrl'}+Shift+J)`
justifyDom.onclick = function () {
console.log('justify')
instance.command.executeRowFlex(RowFlex.JUSTIFY)
}
const rowMarginDom = document.querySelector<HTMLDivElement>(
'.menu-item__row-margin'
)!
const rowOptionDom = rowMarginDom.querySelector<HTMLDivElement>('.options')!
rowMarginDom.onclick = function () {
console.log('row-margin')
rowOptionDom.classList.toggle('visible')
}
rowOptionDom.onclick = function (evt) {
const li = evt.target as HTMLLIElement
instance.command.executeRowMargin(Number(li.dataset.rowmargin!))
}
const listDom = document.querySelector<HTMLDivElement>('.menu-item__list')!
listDom.title = `列表(${
isApple ? '⌘' : 'Ctrl'}+Shift+U)`
const listOptionDom = listDom.querySelector<HTMLDivElement>('.options')!
listDom.onclick = function () {
console.log('list')
listOptionDom.classList.toggle('visible')
}
listOptionDom.onclick = function (evt) {
const li = evt.target as HTMLLIElement
const listType = <ListType>li.dataset.listType || null
const listStyle = <ListStyle>(<unknown>li.dataset.listStyle)
instance.command.executeList(listType, listStyle)
}
// 4. | 表格 | 图片 | 超链接 | 分割线 | 水印 | 代码块 | 分隔符 | 控件 | 复选框 | LaTeX | 日期选择器
const tableDom = document.querySelector<HTMLDivElement>('.menu-item__table')!
const tablePanelContainer = document.querySelector<HTMLDivElement>(
'.menu-item__table__collapse'
)!
const tableClose = document.querySelector<HTMLDivElement>('.table-close')!
const tableTitle = document.querySelector<HTMLDivElement>('.table-select')!
const tablePanel = document.querySelector<HTMLDivElement>('.table-panel')!
// 绘制行列
const tableCellList: HTMLDivElement[][] = []
for (let i = 0; i < 10; i++) {
const tr = document.createElement('tr')
tr.classList.add('table-row')
const trCellList: HTMLDivElement[] = []
for (let j = 0; j < 10; j++) {
const td = document.createElement('td')
td.classList.add