This commit is contained in:
hanshiyang 2025-09-23 10:43:28 +08:00
commit 977ba1c283
456 changed files with 52892 additions and 0 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = false

46
.eslintrc Normal file
View File

@ -0,0 +1,46 @@
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"env": {
"browser": true
},
"globals": {
"process": true
},
"rules": {
"linebreak-style": 0,
"no-console": 0,
"no-debugger": 0,
"no-useless-escape": "off",
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/no-this-alias": 0,
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/ban-types": [1, {
"types": {
"Function": false,
"{}": false
},
"extendDefaults": true
}],
"no-constant-condition": ["error", {
"checkLoops": false
}],
"semi": [1, "never"],
"quotes": [1, "single", {
"allowTemplateLiterals": true
}]
},
"ignorePatterns": ["node_modules", "dist", "index.html"]
}

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.idea
node_modules
.DS_Store
dist
dist-ssr
*.local
cache
.temp

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 80,
"trailingComma": "none",
"arrowParens": "avoid",
"endOfLine": "lf"
}

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

57
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,57 @@
{
"cSpell.words": [
"atrule",
"Chainable",
"colspan",
"compositionend",
"compositionstart",
"contenteditable",
"contextmenu",
"CRDT",
"deletable",
"dppx",
"esbenp",
"eventbus",
"inputarea",
"keyof",
"linebreak",
"mousedown",
"mouseup",
"mousemove",
"mouseleave",
"noopener",
"Parens",
"prismjs",
"resizer",
"richtext",
"rowmargin",
"rowspan",
"srcdoc",
"TEXTLIKE",
"trlist",
"updown",
"vite",
"vitepress",
"Yahei"
],
"cSpell.ignorePaths": [
".github",
"dist",
"node_modules",
"yarn.lock",
"src/editor/core/draw/particle/latex/utils"
],
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

2326
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2021-present, hufe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

104
README.md Normal file
View File

@ -0,0 +1,104 @@
<h1 align="center">canvas-editor</h1>
<p align="center">
<a href="https://www.npmjs.com/package/@hufe921/canvas-editor" target="_blank"><img src="https://img.shields.io/npm/v/@hufe921/canvas-editor.svg?sanitize=true" alt="Version"></a>
<a href="https://github.com/hufe921/canvas-editor/actions" target="_blank">
<img alt="Cypress Passing" src="https://github.com/hufe921/canvas-editor/workflows/cypress/badge.svg" />
</a>
<a href="https://github.com/hufe921/canvas-editor/graphs/contributors" target="_blank">
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/hufe921/canvas-editor" />
</a>
<a href="https://www.npmjs.com/package/@hufe921/canvas-editor" target="_blank"><img src="https://img.shields.io/npm/l/@hufe921/canvas-editor.svg?sanitize=true" alt="License"></a>
<a href="https://github.com/Hufe921/canvas-editor/issues/new/choose" target="_blank"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs"></a>
</p>
<p align="center"> a rich text editor by canvas/svg</p>
<p align="center">
<a href="https://hufe.club/canvas-editor" target="_blank">View Demo</a>
·
<a href="https://hufe.club/canvas-editor-docs" target="_blank">View Docs</a>
·
<a href="https://github.com/Hufe921/canvas-editor/issues/new?assignees=&labels=&projects=&template=bug_report.yml" target="_blank">Report Bug</a>
·
<a href="https://github.com/Hufe921/canvas-editor/issues/new?assignees=&labels=%3Asparkles%3A+feature+request&projects=&template=feature_request.yml" target="_blank">Request Feature</a>
·
<a href="https://github.com/Hufe921/canvas-editor/discussions" target="_blank">FAQ</a>
</p>
<p align="center">Love the project? Please consider <a href="https://hufe.club/donate.jpg" target="_blank">donating(赞助)</a> to help it improve!</p>
## Tips
1. Official plugin: [canvas-editor-plugin](https://github.com/Hufe921/canvas-editor-plugin)
2. The render layer by svg is under development, see [feature/svg](https://github.com/Hufe921/canvas-editor/tree/feature/svg)
3. The export pdf feature is available now, see [feature/pdf](https://github.com/Hufe921/canvas-editor/tree/feature/pdf)
4. The AI-powered text processing demo, see [feature/ai](https://github.com/Hufe921/canvas-editor/tree/feature/ai)
5. Table pagination [#41](https://github.com/Hufe921/canvas-editor/issues/41) is under active development, see: [poc/table-paging](https://github.com/Hufe921/canvas-editor/tree/poc/table-paging) · [demo](https://hufe.club/canvas-editor-table/)
## Basic usage
```bash
npm i @hufe921/canvas-editor --save
```
```html
<div class="canvas-editor"></div>
```
```javascript
import Editor from '@hufe921/canvas-editor'
new Editor(document.querySelector('.canvas-editor'), {
main: [
{
value: 'Hello World'
}
]
})
```
## Features
- Rich text operations (Undo, Redo, Font, Size, Bold, Italic, Underline, Strikeout, Superscript, Alignment, Title, List, ...)
- Insert elements (Table, Image, Link, Code Block, Page Break, Math Formula, Date Picker, Block, ...)
- Print (Based on canvas to picture, pdf drawing)
- Controls (Select, Text, Date, Radio, Checkbox)
- Contextmenu (Internal, Custom)
- Shortcut keys (Internal, Custom)
- Drag and Drop(Text, Element, Control)
- Header, Footer, Page Number
- Page Margin
- Watermark
- Pagination
- Comment
- Catalog
## Roadmap
1. Table paging
2. Control rules
3. Improve performance
4. [CRDT](https://github.com/Hufe921/canvas-editor/tree/feature/CRDT)
## Snapshot
![image](https://github.com/Hufe921/canvas-editor/blob/main/src/assets/snapshots/main_v0.9.35.png)
## Install
`yarn`
## Dev
`npm run dev`
## Build
#### app
`npm run build`
#### lib
`npm run lib`

10
cypress.config.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineConfig } from 'cypress'
export default defineConfig({
video: false,
viewportWidth: 1366,
viewportHeight: 720,
e2e: {
experimentalRunAllSpecs: true
}
})

View File

@ -0,0 +1,46 @@
import Editor, { ControlType, ElementType } from '../../../src/editor'
describe('控件-复选框', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const elementType: ElementType = <ElementType>'control'
const controlType: ControlType = <ControlType>'checkbox'
it('复选框', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
type: elementType,
value: '',
control: {
code: '98175',
type: controlType,
value: null,
valueSets: [
{
value: '有',
code: '98175'
},
{
value: '无',
code: '98176'
}
]
}
}
])
const data = editor.command.getValue().data.main[0]
expect(data.control!.code).to.be.eq('98175')
})
})
})

View File

@ -0,0 +1,56 @@
import Editor, { ControlType, ElementType } from '../../../src/editor'
describe('控件-列举型', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const text = ``
const elementType: ElementType = <ElementType>'control'
const controlType: ControlType = <ControlType>'select'
it('列举型', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
type: elementType,
value: '',
control: {
type: controlType,
value: null,
placeholder: '列举型',
valueSets: [
{
value: '有',
code: '98175'
},
{
value: '无',
code: '98176'
}
]
}
}
])
cy.get('@canvas').type(`{leftArrow}`)
cy.get('.ce-select-control-popup li')
.eq(0)
.click()
.then(() => {
const data = editor.command.getValue().data.main[0]
expect(data.control!.value![0].value).to.be.eq(text)
expect(data.control!.code).to.be.eq('98175')
})
})
})
})

View File

@ -0,0 +1,43 @@
import Editor, { ControlType, ElementType } from '../../../src/editor'
describe('控件-文本型', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const text = `canvas-editor`
const elementType: ElementType = <ElementType>'control'
const controlType: ControlType = <ControlType>'text'
it('文本型', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
type: elementType,
value: '',
control: {
type: controlType,
value: null,
placeholder: '文本型'
}
}
])
cy.get('@canvas').type(`{leftArrow}`)
cy.get('.ce-inputarea')
.type(text)
.then(() => {
const data = editor.command.getValue().data.main[0]
expect(data.control!.value![0].value).to.be.eq(text)
})
})
})
})

67
cypress/e2e/editor.cy.ts Normal file
View File

@ -0,0 +1,67 @@
import Editor from '../../src/editor'
describe('基础功能', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const text = 'canvas-editor'
it('编辑保存', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
cy.get('@canvas')
.type(text)
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].value).to.eq(text)
})
})
})
it('模式切换', () => {
cy.get('@canvas').click()
cy.get('.ce-cursor').should('have.css', 'display', 'block')
cy.get('.editor-mode').click().click()
cy.get('.editor-mode').contains('只读')
cy.get('@canvas').click()
cy.get('.ce-cursor').should('have.css', 'display', 'none')
})
it('页面缩放', () => {
cy.get('.page-scale-add').click()
cy.get('.page-scale-percentage').contains('110%')
cy.get('.page-scale-minus').click().click()
cy.get('.page-scale-percentage').contains('90%')
})
it('字数统计', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: 'canvas-editor 2022 编辑器'
}
])
cy.get('.word-count').contains('7')
})
})
})

View File

@ -0,0 +1,38 @@
import Editor from '../../../src/editor'
describe('菜单-内容块', () => {
const url = 'http://localhost:3000/canvas-editor/'
beforeEach(() => {
cy.visit(url)
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
it('内容块', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
cy.get('.menu-item__block').click()
cy.get('.dialog-option__item [name="width"]').type('500')
cy.get('.dialog-option__item [name="height"]').type('300')
cy.get('.dialog-option__item [name="src"]').type(url)
cy.get('.dialog-menu button')
.eq(1)
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].type).to.eq('block')
expect(data[0].block?.iframeBlock?.src).to.eq(url)
})
})
})
})

View File

@ -0,0 +1,33 @@
import Editor, { ElementType } from '../../../src/editor'
describe('菜单-复选框', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const type: ElementType = <ElementType>'checkbox'
it('代码块', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
type,
value: '',
checkbox: {
value: true
}
}
])
const data = editor.command.getValue().data.main[0]
expect(data.checkbox?.value).to.eq(true)
})
})
})

View File

@ -0,0 +1,34 @@
import Editor from '../../../src/editor'
describe('菜单-代码块', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const text = `console.log('canvas-editor')`
it('代码块', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
cy.get('.menu-item__codeblock').click()
cy.get('.dialog-option [name="codeblock"]').type(text)
cy.get('.dialog-menu button')
.eq(1)
.click()
.then(() => {
const data = editor.command.getValue().data.main[2]
expect(data.value).to.eq('log')
expect(data.color).to.eq('#b9a40a')
})
})
})
})

View File

@ -0,0 +1,28 @@
import Editor from '../../../src/editor'
describe('菜单-日期选择器', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
it('LaTeX', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
cy.get('.menu-item__date').click()
cy.get('.menu-item__date li')
.first()
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].type).to.eq('date')
})
})
})
})

View File

@ -0,0 +1,40 @@
import Editor from '../../../src/editor'
describe('菜单-清除格式', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const text = 'canvas-editor'
const textLength = text.length
it('清除格式', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text,
bold: true,
italic: true
}
])
editor.command.executeSetRange(0, textLength)
cy.get('.menu-item__format')
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].italic).to.eq(undefined)
expect(data[0].bold).to.eq(undefined)
})
})
})
})

View File

@ -0,0 +1,39 @@
import Editor from '../../../src/editor'
describe('菜单-超链接', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const text = 'canvas-editor'
const url = 'https://hufe.club/canvas-editor'
it('超链接', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
cy.get('.menu-item__hyperlink').click()
cy.get('.dialog-option__item [name="name"]').type(text)
cy.get('.dialog-option__item [name="url"]').type(url)
cy.get('.dialog-menu button')
.eq(1)
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].type).to.eq('hyperlink')
expect(data[0].url).to.eq(url)
expect(data[0]?.valueList?.[0].value).to.eq(text)
})
})
})
})

View File

@ -0,0 +1,25 @@
import Editor from '../../../src/editor'
describe('菜单-图片', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
it('图片', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
cy.get('#image').attachFile('test.png')
cy.wait(200).then(() => {
const data = editor.command.getValue().data.main
expect(data[0].type).to.eq('image')
})
})
})
})

View File

@ -0,0 +1,34 @@
import Editor from '../../../src/editor'
describe('菜单-LaTeX', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const text = 'canvas-editor'
it('LaTeX', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
cy.get('.menu-item__latex').click()
cy.get('.dialog-option__item [name="value"]').type(text)
cy.get('.dialog-menu button')
.eq(1)
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].type).to.eq('latex')
expect(data[0].value).to.eq(text)
})
})
})
})

View File

@ -0,0 +1,21 @@
import Editor from '../../../src/editor'
describe('菜单-分页符', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
it('分页符', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
cy.get('.menu-item__page-break').click().click()
cy.get('canvas').should('have.length', 2)
})
})
})

View File

@ -0,0 +1,53 @@
import Editor from '../../../src/editor'
describe('菜单-格式刷', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const text = 'canvas-editor'
const textLength = text.length
it('格式刷', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text,
bold: true,
italic: true
}
])
editor.command.executeInsertElementList([
{
value: text
}
])
editor.command.executeSetRange(0, textLength)
cy.get('.menu-item__painter')
.click()
.wait(300)
.then(() => {
editor.command.executeSetRange(textLength, 2 * textLength)
editor.command.executeApplyPainterStyle()
const data = editor.command.getValue().data.main
expect(data.length).to.eq(1)
expect(data[0].italic).to.eq(true)
expect(data[0].bold).to.eq(true)
})
})
})
})

View File

@ -0,0 +1,25 @@
import Editor from '../../../src/editor'
describe('菜单-打印', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').should('have.length', 2)
})
it('打印', () => {
cy.getEditor().then(async (editor: Editor) => {
const imageList2 = await editor.command.getImage()
expect(imageList2.length).to.eq(2)
editor.command.executeSelectAll()
editor.command.executeBackspace()
cy.wait(200).then(async () => {
const imageList1 = await editor.command.getImage()
expect(imageList1.length).to.eq(1)
})
})
})
})

103
cypress/e2e/menus/row.cy.ts Normal file
View File

@ -0,0 +1,103 @@
import Editor from '../../../src/editor'
describe('菜单-行处理', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const text = 'canvas-editor'
it('左对齐', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
cy.get('.menu-item__left')
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].rowFlex).to.eq('left')
})
})
})
it('居中对齐', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
cy.get('.menu-item__center')
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].rowFlex).to.eq('center')
})
})
})
it('靠右对齐', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
cy.get('.menu-item__right')
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].rowFlex).to.eq('right')
})
})
})
it('行间距', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
cy.get('.menu-item__row-margin').as('rowMargin').click()
cy.get('@rowMargin')
.find('li')
.eq(1)
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].rowMargin).to.eq(1.25)
})
})
})
})

View File

@ -0,0 +1,112 @@
import Editor, { ElementType } from '../../../src/editor'
describe('菜单-搜索', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const searchText = 'canvas-editor'
const replaceText = 'replace'
const type: ElementType = <ElementType>'table'
it('搜索', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: searchText
},
{
value: '\n',
type,
trList: [
{
height: 42,
tdList: [
{
colspan: 1,
rowspan: 1,
value: [
{
value: searchText
}
]
},
{
colspan: 1,
rowspan: 1,
value: []
}
]
},
{
height: 42,
tdList: [
{
colspan: 1,
rowspan: 1,
value: []
},
{
colspan: 1,
rowspan: 1,
value: [
{
value: searchText
}
]
}
]
}
],
colgroup: [
{
width: 200
},
{
width: 200
}
]
}
])
cy.get('.menu-item__search').click()
cy.get('.menu-item__search__collapse input').eq(0).type(searchText)
// 搜索导航
cy.get('.menu-item__search__collapse .arrow-right').click()
cy.get('.menu-item__search__collapse__search .search-result').should(
'have.text',
'1/3'
)
cy.get('.menu-item__search__collapse__replace').as('replace')
cy.get('@replace').find('input').type(replaceText)
cy.get('@replace')
.find('button')
.click()
.then(() => {
const data = editor.command.getValue().data.main
// 普通文本
expect(data[0].value).to.be.eq(replaceText)
// 表格内文本
expect(data[1].trList![0].tdList[0].value[0].value).to.be.eq(
replaceText
)
expect(data[1].trList![1].tdList[1].value[0].value).to.be.eq(
replaceText
)
})
})
})
})

View File

@ -0,0 +1,32 @@
import Editor from '../../../src/editor'
describe('菜单-分割线', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
it('分割线', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
cy.get('.menu-item__separator').click()
cy.get('.menu-item__separator li')
.eq(1)
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].type).to.eq('separator')
expect(data[0]?.dashArray?.[0]).to.eq(1)
expect(data[0]?.dashArray?.[1]).to.eq(1)
})
})
})
})

View File

@ -0,0 +1,25 @@
import Editor from '../../../src/editor'
describe('菜单-表格', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
it('表格', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertTable(8, 8)
const data = editor.command.getValue().data.main
expect(data[0].type).to.eq('table')
expect(data[0].trList?.length).to.eq(8)
})
})
})

View File

@ -0,0 +1,304 @@
import Editor from '../../../src/editor'
describe('菜单-文本处理', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const text = 'canvas-editor'
const textLength = text.length
it('字体', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
editor.command.executeSetRange(0, textLength)
cy.get('.menu-item__font').as('font').click()
cy.get('@font')
.find('li')
.eq(1)
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].font).to.eq('华文宋体')
})
})
})
it('字号设置', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
editor.command.executeSetRange(0, textLength)
cy.get('.menu-item__size').as('size').click()
cy.get('@size')
.find('li')
.eq(0)
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].size).to.eq(56)
})
})
})
it('字体增大', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
editor.command.executeSetRange(0, textLength)
cy.get('.menu-item__size-add')
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].size).to.eq(18)
})
})
})
it('字体减小', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
editor.command.executeSetRange(0, textLength)
cy.get('.menu-item__size-minus')
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].size).to.eq(14)
})
})
})
it('加粗', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
editor.command.executeSetRange(0, textLength)
cy.get('.menu-item__bold')
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].bold).to.eq(true)
})
})
})
it('斜体', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
editor.command.executeSetRange(0, textLength)
cy.get('.menu-item__italic')
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].italic).to.eq(true)
})
})
})
it('下划线', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
editor.command.executeSetRange(0, textLength)
cy.get('.menu-item__underline')
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].underline).to.eq(true)
})
})
})
it('删除线', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
editor.command.executeSetRange(0, textLength)
cy.get('.menu-item__strikeout')
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].strikeout).to.eq(true)
})
})
})
it('上标', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
editor.command.executeSetRange(0, textLength)
cy.get('.menu-item__superscript')
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].type).to.eq('superscript')
})
})
})
it('下标', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
editor.command.executeSetRange(0, textLength)
cy.get('.menu-item__subscript')
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].type).to.eq('subscript')
})
})
})
it('字体颜色', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
editor.command.executeSetRange(0, textLength)
editor.command.executeColor('red')
const data = editor.command.getValue().data.main
expect(data[0].color).to.eq('red')
})
})
it('高亮', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
editor.command.executeSetRange(0, textLength)
editor.command.executeHighlight('red')
const data = editor.command.getValue().data.main
expect(data[0].highlight).to.eq('red')
})
})
})

View File

@ -0,0 +1,43 @@
import Editor, { ElementType, TitleLevel } from '../../../src/editor'
describe('菜单-标题', () => {
const url = 'http://localhost:3000/canvas-editor/'
beforeEach(() => {
cy.visit(url)
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const text = 'canvas-editor'
const elementType = <ElementType>'title'
const level = <TitleLevel>'first'
it('标题', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
editor.command.executeInsertElementList([
{
value: text
}
])
cy.get('.menu-item__title').as('title').click()
cy.get('@title')
.find('li')
.eq(1)
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].type).to.eq(elementType)
expect(data[0].level).to.eq(level)
})
})
})
})

View File

@ -0,0 +1,49 @@
import Editor from '../../../src/editor'
describe('菜单-撤销&重做', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const text = 'canvas-editor'
it('撤销', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
cy.get('@canvas').type(`${text}1`)
cy.get('.menu-item__undo')
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].value).to.eq(text)
})
})
})
it('重做', () => {
cy.getEditor().then((editor: Editor) => {
editor.command.executeSelectAll()
editor.command.executeBackspace()
cy.get('@canvas').type(`${text}1`)
cy.get('.menu-item__undo').click()
cy.get('.menu-item__redo')
.click()
.then(() => {
const data = editor.command.getValue().data.main
expect(data[0].value).to.eq(`${text}1`)
})
})
})
})

View File

@ -0,0 +1,64 @@
import Editor from '../../../src/editor'
describe('菜单-水印', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const text = 'canvas-editor'
const size = 80
it('添加水印', () => {
cy.getEditor().then((editor: Editor) => {
cy.get('.menu-item__watermark').click()
cy.get('.menu-item__watermark li').eq(0).click()
cy.get('.dialog-option [name="data"]').type(text)
cy.get('.dialog-option [name="size"]').as('size')
cy.get('@size').clear()
cy.get('@size').type(`${size}`)
cy.get('.dialog-menu button')
.eq(1)
.click()
.then(() => {
const payload = editor.command.getValue()
const {
options: { watermark }
} = payload
expect(watermark?.data).to.eq(text)
expect(watermark?.size).to.eq(size)
})
})
})
it('删除水印', () => {
cy.getEditor().then((editor: Editor) => {
cy.get('.menu-item__watermark').click()
cy.get('.menu-item__watermark li')
.eq(1)
.click()
.then(() => {
const payload = editor.command.getValue()
const {
options: { watermark }
} = payload
expect(watermark?.data).to.eq('')
expect(watermark?.size).to.eq(200)
})
})
})
})

View File

@ -0,0 +1,3 @@
{
"name": "canvas-editor"
}

BIN
cypress/fixtures/test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

13
cypress/global.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
declare namespace Editor {
import('../src/editor/index')
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import Editor from '../src/editor/index'
}
declare namespace Cypress {
interface Chainable {
getEditor(): Chainable<Editor>
}
}

View File

@ -0,0 +1,5 @@
import 'cypress-file-upload'
Cypress.Commands.add('getEditor', () => {
return cy.window().its('editor')
})

1
cypress/support/e2e.ts Normal file
View File

@ -0,0 +1 @@
import './commands'

21
cypress/tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es2015", "dom", "esnext"],
"types": ["cypress", "cypress-file-upload"],
"isolatedModules": false,
"allowJs": true,
"noEmit": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
},
"include": [
"./**/*.ts"
]
}

191
docs/.vitepress/config.ts Normal file
View File

@ -0,0 +1,191 @@
import { defineConfig } from 'vitepress'
export default defineConfig({
base: '/canvas-editor-docs/',
title: 'canvas-editor',
description: 'rich text editor by canvas/svg',
themeConfig: {
i18nRouting: false,
algolia: {
appId: 'RWSVW6F3S5',
apiKey: 'e462fffb4d2e9ab4a78c29e0b457ab33',
indexName: 'hufe'
},
logo: '/favicon.png',
nav: [
{
text: '指南',
link: '/guide/start',
activeMatch: '/guide/'
},
{
text: 'Demo',
link: 'https://hufe.club/canvas-editor'
},
{
text: '官方插件',
link: '/guide/plugin-internal.html'
},
{
text: '赞助',
link: 'https://hufe.club/donate.jpg'
}
],
sidebar: [
{
text: '开始',
items: [
{ text: '入门', link: '/guide/start' },
{ text: '配置', link: '/guide/option' },
{ text: '国际化', link: '/guide/i18n' },
{ text: '数据结构', link: '/guide/schema' }
]
},
{
text: '命令',
items: [
{ text: '执行动作命令', link: '/guide/command-execute' },
{ text: '获取数据命令', link: '/guide/command-get' }
]
},
{
text: '监听',
items: [
{ text: '事件监听(listener)', link: '/guide/listener' },
{ text: '事件监听(eventBus)', link: '/guide/eventbus' }
]
},
{
text: '快捷键',
items: [
{ text: '内部快捷键', link: '/guide/shortcut-internal' },
{ text: '自定义快捷键', link: '/guide/shortcut-custom' }
]
},
{
text: '右键菜单',
items: [
{ text: '内部右键菜单', link: '/guide/contextmenu-internal' },
{ text: '自定义右键菜单', link: '/guide/contextmenu-custom' }
]
},
{
text: '重写方法',
items: [{ text: '重写方法', link: '/guide/override' }]
},
{
text: 'API',
items: [
{ text: '实例API', link: '/guide/api-instance' },
{ text: '通用API', link: '/guide/api-common' }
]
},
{
text: '插件',
items: [
{ text: '自定义插件', link: '/guide/plugin-custom' },
{ text: '官方插件', link: '/guide/plugin-internal' }
]
}
],
socialLinks: [
{
icon: 'github',
link: 'https://github.com/Hufe921/canvas-editor'
}
],
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2021-present Hufe'
}
},
locales: {
root: {
label: '简体中文',
lang: 'zh-CN'
},
en: {
label: 'English',
lang: 'en',
link: '/en/',
themeConfig: {
nav: [
{
text: 'Guide',
link: '/en/guide/start',
activeMatch: '/en/guide/'
},
{
text: 'Demo',
link: 'https://hufe.club/canvas-editor'
},
{
text: 'Official plugin',
link: '/en/guide/plugin-internal.html'
},
{
text: 'Donate',
link: 'https://hufe.club/donate.jpg'
}
],
sidebar: [
{
text: 'Start',
items: [
{ text: 'start', link: '/en/guide/start' },
{ text: 'option', link: '/en/guide/option' },
{ text: 'i18n', link: '/en/guide/i18n' },
{ text: 'schema', link: '/en/guide/schema' }
]
},
{
text: 'Command',
items: [
{ text: 'execute', link: '/en/guide/command-execute' },
{ text: 'get', link: '/en/guide/command-get' }
]
},
{
text: 'Listener',
items: [
{ text: 'listener', link: '/en/guide/listener' },
{ text: 'eventbus', link: '/en/guide/eventbus' }
]
},
{
text: 'Shortcut',
items: [
{ text: 'internal', link: '/en/guide/shortcut-internal' },
{ text: 'custom', link: '/en/guide/shortcut-custom' }
]
},
{
text: 'Contextmenu',
items: [
{ text: 'internal', link: '/en/guide/contextmenu-internal' },
{ text: 'custom', link: '/en/guide/contextmenu-custom' }
]
},
{
text: 'Override',
items: [{ text: 'override', link: '/en/guide/override' }]
},
{
text: 'Api',
items: [
{ text: 'instance', link: '/en/guide/api-instance' },
{ text: 'common', link: '/en/guide/api-common' }
]
},
{
text: 'Plugin',
items: [
{ text: 'custom', link: '/en/guide/plugin-custom' },
{ text: 'official', link: '/en/guide/plugin-internal' }
]
}
]
}
}
}
})

View File

@ -0,0 +1,49 @@
# Common API
## splitText
Feature: split text
Usage
```javascript
import { splitText } from '@hufe921/canvas-editor'
splitText(text: string): string[]
```
## createDomFromElementList
Feature: Create a DOM tree based on the elementList
Usage
```javascript
import { createDomFromElementList } from '@hufe921/canvas-editor'
createDomFromElementList(elementList: IElement[], options?: IEditorOption): HTMLDivElement
```
## getElementListByHTML
Feature: Create an elementList based on HTML
Usage
```javascript
import { getElementListByHTML } from '@hufe921/canvas-editor'
getElementListByHTML(htmlText: string, options: IGetElementListByHTMLOption): IElement[]
```
## getTextFromElementList
Feature: Create text based on elementList
Usage
```javascript
import { getTextFromElementList } from '@hufe921/canvas-editor'
getTextFromElementList(elementList: IElement[]): string
```

View File

@ -0,0 +1,24 @@
# Instance API
## How to Use
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
instance.apiName()
```
## destroy
Feature: Destroy the editor
Usage
```javascript
instance.destroy()
```
::: warning
Only destroy the editor DOM and related events, menu bars, toolbars, external variables, etc. need to be handled by themselves.
:::

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,333 @@
# Get Data Command
## How to Use
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
const value = instance.command.commandName()
```
## getValue
Feature: Get the current document value
Usage:
```javascript
const {
version: string
data: IEditorData
options: IEditorOption
} = instance.command.getValue(options?: IGetValueOption)
```
## getValueAsync
Feature: Get the current document value (async)
Usage:
```javascript
const {
version: string
data: IEditorData
options: IEditorOption
} = await instance.command.getValueAsync(options?: IGetValueOption)
```
## getImage
Feature: Gets the base64 string of the current page image
Usage:
```javascript
const base64StringList = await instance.command.getImage(option?: IGetImageOption)
```
## getOptions
Feature: Get editor options
Usage:
```javascript
const editorOption = await instance.command.getOptions()
```
## getWordCount
Feature: Get document word count
Usage:
```javascript
const wordCount = await instance.command.getWordCount()
```
## getCursorPosition
Feature: Get cursor position with coordinates
Usage:
```javascript
const range = instance.command.getCursorPosition()
```
## getRange
Feature: Get range
Usage:
```javascript
const range = instance.command.getRange()
```
## getRangeText
Feature: Get range text
Usage:
```javascript
const rangeText = instance.command.getRangeText()
```
## getRangeContext
Feature: Get range context
Usage:
```javascript
const rangeContext = instance.command.getRangeContext()
```
## getRangeRow
Feature: Get range row element list
Usage:
```javascript
const rowElementList = instance.command.getRangeRow()
```
## getKeywordRangeList
Feature: Get range list by keyword
Usage:
```javascript
const rangeList = instance.command.getKeywordRangeList()
```
## getKeywordContext
Feature: Get context list by keyword
Usage:
```javascript
const keywordContextList = instance.command.getKeywordContext(payload: string)
```
## getRangeParagraph
Feature: Get range paragraph element list
Usage:
```javascript
const paragraphElementList = instance.command.getRangeParagraph()
```
## getPaperMargin
Feature: Gets the margins
Usage:
```javascript
const [top: number, right: number, bottom: number, left: number] =
instance.command.getPaperMargin()
```
## getSearchNavigateInfo
Feature: Get search navigation information
Usage:
```javascript
const {
index: number;
count: number;
} = instance.command.getSearchNavigateInfo()
```
## getCatalog
Feature: Get directory
Usage:
```javascript
const catalog = await instance.command.getCatalog()
```
## getHTML
Feature: Get HTML
Usage:
```javascript
const {
header: string
main: string
footer: string
} = await instance.command.getHTML()
```
## getText
Feature: Get text
Usage:
```javascript
const {
header: string
main: string
footer: string
} = await instance.command.getText()
```
## getLocale
Feature: Get current locale
Usage:
```javascript
const locale = await instance.command.getLocale()
```
## getGroupIds
Feature: Get all group ids
Usage:
```javascript
const groupIds = await instance.command.getGroupIds()
```
## getControlValue
Feature: Get control value
Usage:
```javascript
const {
value: string | null
innerText: string | null
zone: EditorZone
elementList?: IElement[]
} = await instance.command.getControlValue(payload: IGetControlValueOption)
```
## getControlList
Feature: Get control list
Usage:
```javascript
const controlList = await instance.command.getControlList()
```
## getContainer
Feature: Get editor container
Usage:
```javascript
const container = await instance.command.getContainer()
```
## getTitleValue
Feature: Get title value
Usage:
```javascript
const {
value: string | null
elementList: IElement[]
zone: EditorZone
}[] = await instance.command.getTitleValue(payload: IGetTitleValueOption)
```
## getPositionContextByEvent
Feature: Get position context by mouse event
Usage:
```javascript
const {
pageNo: number
element: IElement | null
rangeRect: RangeRect | null
tableInfo: ITableInfoByEvent | null
}[] = await instance.command.getPositionContextByEvent(evt: MouseEvent, options?: IPositionContextByEventOption)
```
demo:
```javascript
instance.eventBus.on(
'mousemove',
debounce(evt => {
const positionContext = instance.command.getPositionContextByEvent(evt)
console.log(positionContext)
}, 200)
)``
```
## getElementById
Feature: Get element list by id
Usage:
```javascript
const elementList = await instance.command.getElementById(payload: IGetElementByIdOption)
```
## getAreaValue
Feature: Get area value
Usage:
```javascript
const {
id?: string
area: IArea
value: IElement[]
startPageNo: number
endPageNo: number
} = instance.command.getAreaValue(options: IGetAreaValueOption)
```

View File

@ -0,0 +1,44 @@
# Customize Contextmenu
## How to Use
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
instance.register.contextMenuList([
{
key?: string;
isDivider?: boolean;
icon?: string;
name?: string; // Use %s for selection text. Example: Search: %s
shortCut?: string;
disable?: boolean;
when?: (payload: IContextMenuContext) => boolean;
callback?: (command: Command, context: IContextMenuContext) => any;
childMenus?: IRegisterContextMenu[];
}
])
```
## getContextMenuList
Feature: Get context menu list
Usage:
```javascript
const contextMenuList = await instance.register.getContextMenuList()
```
Remark:
```javascript
// Example of modifying internal contextmenu
contextmenuList.forEach(menu => {
// Find the menu item through the menu key and modify its properties
if (menu.key === INTERNAL_CONTEXT_MENU_KEY.GLOBAL.PASTE) {
menu.when = () => false
}
})
```

View File

@ -0,0 +1,61 @@
# Internal Contextmenu
## Global
- Cut
- Copy
- Paste
- Select all
- Print
## Hyperlinks
- Delete the link
- Unlink
- Edit the link
## Image
- Change the picture
- Save as picture
- Text wrapping
- Embed
- Up down
- Surround
- Float above text
- Float below text
## Table
- Table borders
- All borders
- Borderless
- Dashed border
- Outer border
- Internal border
- Td borders
- Top border
- Right border
- Bottom border
- Left border
- Forward border
- Back border
- Vertical alignment
- Top alignment
- Center vertically
- Bottom end alignment
- Insert rows and columns
- Insert 1 row above
- Insert 1 row below
- Insert 1 column on the left
- Insert 1 column on the right side
- Delete rows and columns
- Delete 1 row
- Remove 1 column
- Delete the entire table
- Merge cells
- Cancel the merge
## control
- Delete the control

234
docs/en/guide/eventbus.md Normal file
View File

@ -0,0 +1,234 @@
# Event Listening(eventBus)
## How to Use
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
// register
instance.eventBus.on<K keyof EventMap>(
eventName: K,
callback: EventMap[K]
)
// remove
instance.eventBus.off<K keyof EventMap>(
eventName: K,
callback: EventMap[K]
)
```
## rangeStyleChange
Feature: The selection style changes
Usage:
```javascript
instance.eventBus.on('rangeStyleChange', (payload: IRangeStyle) => void)
```
## visiblePageNoListChange
Feature: The visible page changes
Usage:
```javascript
instance.eventBus.on('visiblePageNoListChange', (payload: number[]) => void)
```
## intersectionPageNoChange
Feature: The current page changes
Usage:
```javascript
instance.eventBus.on('intersectionPageNoChange', (payload: number) => void)
```
## pageSizeChange
Feature: The current number of pages has changed
Usage:
```javascript
instance.eventBus.on('pageSizeChange', (payload: number) => void)
```
## pageScaleChange
Feature: The current page scaling has changed
Usage:
```javascript
instance.eventBus.on('pageScaleChange', (payload: number) => void)
```
## contentChange
Feature: The current content has changed
Usage:
```javascript
instance.eventBus.on('contentChange', () => void)
```
## controlChange
Feature: The control where the current cursor is located changes
Usage:
```javascript
instance.eventBus.on('controlChange', (payload: IControlChangeResult) => void)
```
## controlContentChange
Feature: The control content changes
Usage:
```javascript
instance.eventBus.on('controlContentChange', (payload: IControlContentChangeResult) => void)
```
## pageModeChange
Feature: The page mode changes
Usage:
```javascript
instance.eventBus.on('pageModeChange', (payload: PageMode) => void)
```
## saved
Feature: Document saved
Usage:
```javascript
instance.eventBus.on('saved', (payload: IEditorResult) => void)
```
## zoneChange
Feature: The zone changes
Usage:
```javascript
instance.eventBus.on('zoneChange', (payload: EditorZone) => void)
```
## mousemove
Feature: Editor mousemove event
Usage:
```javascript
instance.eventBus.on('mousemove', (evt: MouseEvent) => void)
```
## mouseenter
Feature: Editor mouseenter event
Usage:
```javascript
instance.eventBus.on('mouseenter', (evt: MouseEvent) => void)
```
## mouseleave
Feature: Editor mouseleave event
Usage:
```javascript
instance.eventBus.on('mouseleave', (evt: MouseEvent) => void)
```
## mousedown
Feature: Editor mousedown event
Usage:
```javascript
instance.eventBus.on('mousedown', (evt: MouseEvent) => void)
```
## mouseup
Feature: Editor mouseup event
Usage:
```javascript
instance.eventBus.on('mouseup', (evt: MouseEvent) => void)
```
## click
Feature: Editor click event
Usage:
```javascript
instance.eventBus.on('click', (evt: MouseEvent) => void)
```
## input
Feature: Editor input event
Usage:
```javascript
instance.eventBus.on('input', (evt: Event) => void)
```
## positionContextChange
Feature: The position context change
Usage:
```javascript
instance.eventBus.on('positionContextChange', (payload: IPositionContextChangePayload) => void)
```
## imageSizeChange
Feature: The image size change
Usage:
```javascript
instance.eventBus.on('imageSizeChange', (payload: { element: IElement }) => void)
```
## imageMousedown
Feature: The image mousedown event
Usage:
```javascript
instance.eventBus.on('imageMousedown', (payload: {
evt: MouseEvent
element: IElement
}) => void)
```

112
docs/en/guide/i18n.md Normal file
View File

@ -0,0 +1,112 @@
# i18n
## How to Use
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
// register
instance.register.langMap(locale: string, lang: ILang)
// set locale
instance.command.executeSetLocale(locale)
```
## ILang
```typescript
interface ILang {
contextmenu: {
global: {
cut: string
copy: string
paste: string
selectAll: string
print: string
}
control: {
delete: string
}
hyperlink: {
delete: string
cancel: string
edit: string
}
image: {
change: string
saveAs: string
textWrap: string
textWrapType: {
embed: string
upDown: string
surround: string
floatTop: string
floatBottom: string
}
}
table: {
insertRowCol: string
insertTopRow: string
insertBottomRow: string
insertLeftCol: string
insertRightCol: string
deleteRowCol: string
deleteRow: string
deleteCol: string
deleteTable: string
mergeCell: string
mergeCancelCell: string
verticalAlign: string
verticalAlignTop: string
verticalAlignMiddle: string
verticalAlignBottom: string
border: string
borderAll: string
borderEmpty: string
borderDash: string
borderExternal: string
borderInternal: string
borderTd: string
borderTdTop: string
borderTdRight: string
borderTdBottom: string
borderTdLeft: string
borderTdForward: string
borderTdBack: string
}
}
datePicker: {
now: string
confirm: string
return: string
timeSelect: string
weeks: {
sun: string
mon: string
tue: string
wed: string
thu: string
fri: string
sat: string
}
year: string
month: string
hour: string
minute: string
second: string
}
frame: {
header: string
footer: string
}
pageBreak: {
displayName: string
}
zone: {
headerTip: string
footerTip: string
}
}
```

126
docs/en/guide/listener.md Normal file
View File

@ -0,0 +1,126 @@
# Event Listening(listener)
::: warning
The listener can only respond to one method, and no new listening methods will be added in the future. It is recommended to use eventBus for event listening.
:::
## How to Use
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
instance.listener.eventName = ()=>{}
```
## rangeStyleChange
Feature: The selection style changes
Usage:
```javascript
instance.listener.rangeStyleChange = (payload: IRangeStyle) => {}
```
## visiblePageNoListChange
Feature: The visible page changes
Usage:
```javascript
instance.listener.visiblePageNoListChange = (payload: number[]) => {}
```
## intersectionPageNoChange
Feature: The current page changes
Usage:
```javascript
instance.listener.intersectionPageNoChange = (payload: number) => {}
```
## pageSizeChange
Feature: The current page size has changed
Usage:
```javascript
instance.listener.pageSizeChange = (payload: number) => {}
```
## pageScaleChange
Feature: The current page scaling has changed
Usage:
```javascript
instance.listener.pageScaleChange = (payload: number) => {}
```
## contentChange
Feature: The current content has changed
Usage:
```javascript
instance.listener.contentChange = () => {}
```
## controlChange
Feature: The control where the current cursor is located changes
Usage:
```javascript
instance.listener.controlChange = (payload: IControlChangeResult) => {}
```
## controlContentChange
Feature: The control content changes
Usage:
```javascript
instance.listener.controlContentChange = (
payload: IControlContentChangeResult
) => {}
```
## pageModeChange
Feature: The page mode changes
Usage:
```javascript
instance.listener.pageModeChange = (payload: PageMode) => {}
```
## saved
Feature: Document saved
Usage:
```javascript
instance.listener.saved = (payload: IEditorResult) => {}
```
## zoneChange
Feature: 区域发生改变
Usage:
```javascript
instance.listener.zoneChange = (payload: EditorZone) => {}
```

189
docs/en/guide/option.md Normal file
View File

@ -0,0 +1,189 @@
# Configuration
## How to Use?
```javascript
import Editor from "@hufe921/canvas-editor"
new Editor(container, IEditorData | IElement[], {
// option
})
```
## Complete Configuration
```typescript
interface IEditorOption {
mode?: EditorMode // Editor mode: Edit, Clean (Visual aids are not displayed, For example: page break), ReadOnly, Form (Only editable within the control), Print (Visual aids are not displayed, Unwritten content control), Design (Do not handle configurations such as non deletable and read-only). default: Edit
locale?: string // Language. default: zhCN
defaultType?: string // Default element type. default: TEXT
defaultColor?: string // Default color. default: #000000
defaultFont?: string // Default font. default: Microsoft YaHei
defaultSize?: number // Default font size. default: 16
minSize?: number // Min font size。default: 5
maxSize?: number // Max font size。default: 72
defaultBasicRowMarginHeight?: number // Default line height。default: 8
defaultRowMargin?: number // Default line spacing. default: 1
defaultTabWidth?: number // Default tab width. default: 32
width?: number // Paper width. default: 794
height?: number // Paper height. default: 1123
scale?: number // scaling. default: 1
pageGap?: number // Paper spacing. default: 20
underlineColor?: string // Underline color. default: #000000
strikeoutColor?: string // Strikeout color. default: #FF0000
rangeColor?: string // Range color. default: #AECBFA
rangeAlpha?: number // Range transparency. default: 0.6
rangeMinWidth?: number // Range min width. default: 5
searchMatchColor?: string // Search for highlight color. default: #FFFF00
searchNavigateMatchColor?: string // Search navigation highlighted color.default: #AAD280
searchMatchAlpha?: number // Search for highlight transparency. default: 0.6
highlightAlpha?: number // Highlight element transparency. default: 0.6
highlightMarginHeight?: number // Highlight element margin height. default: 8
resizerColor?: string // Image sizer color. default: #4182D9
resizerSize?: number // Image sizer size. default: 5
marginIndicatorSize?: number // The margin indicator length. default: 35
marginIndicatorColor?: string // The margin indicator color. default: #BABABA
margins?: IMargin // Page margins. default: [100, 120, 100, 120]
pageMode?: PageMode // Paper mode: Linkage, Pagination. default: Pagination
renderMode?: RenderMode // Render mode: speed(multi words combination rendering), compatibility(word by word rendering:avoid environmental differences such as browse,fonts...). default: speed
defaultHyperlinkColor?: string // Default hyperlink color. default: #0000FF
table?: ITableOption // table configuration {tdPadding?:IPadding; defaultTrMinHeight?:number; defaultColMinWidth?:number}
header?: IHeader // Header information.{top?:number; maxHeightRadio?:MaxHeightRatio;}
footer?: IFooter // Footer information. {bottom?:number; maxHeightRadio?:MaxHeightRatio;}
pageNumber?: IPageNumber // Page number information. {bottom:number; size:number; font:string; color:string; rowFlex:RowFlex; format:string; numberType:NumberType;}
paperDirection?: PaperDirection // Paper orientation: portrait, landscape
inactiveAlpha?: number // When the body content is out of focus, transparency. default: 0.6
historyMaxRecordCount?: number // History (undo redo) maximum number of records. default: 100
printPixelRatio?: number // Print the pixel ratio (larger values are clearer, but larger sizes). default: 3
maskMargin?: IMargin // Masking margins above the editorfor example: menu bar, bottom toolbar。default: [0, 0, 0, 0]
letterClass?: string[] // Alphabet class supported by typesetting. default: a-zA-Z. Built-in alternative alphabet class: LETTER_CLASS
contextMenuDisableKeys?: string[] // Disable context menu keys. default: []
shortcutDisableKeys?: string[] // Disable shortcut keys. default: []
scrollContainerSelector?: string // scroll container selector. default: document
pageOuterSelectionDisable?: boolean // Disable selection when the mouse moves out of the page. default: false
wordBreak?: WordBreak // Word and punctuation breaks: No punctuation in the first line of the BREAK_WORD &The word is not split, and the line is folded after BREAK_ALL full according to the width of the character. default: BREAK_WORD
watermark?: IWatermark // Watermark{data:string; color?:string; opacity?:number; size?:number; font?:string; numberType:NumberType;}
control?: IControlOption // Control {placeholderColor?:string; bracketColor?:string; prefix?:string; postfix?:string; borderWidth?: number; borderColor?: string; activeBackgroundColor?: string; disabledBackgroundColor?: string; existValueBackgroundColor?: string; noValueBackgroundColor?: string;}
checkbox?: ICheckboxOption // Checkbox {width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string; verticalAlign?: VerticalAlign;}
radio?: IRadioOption // Radio {width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string; verticalAlign?: VerticalAlign;}
cursor?: ICursorOption // Cursor style. {width?: number; color?: string; dragWidth?: number; dragColor?: string; dragFloatImageDisabled?: boolean;}
title?: ITitleOption // Title configuration.{ defaultFirstSize?: number; defaultSecondSize?: number; defaultThirdSize?: number defaultFourthSize?: number; defaultFifthSize?: number; defaultSixthSize?: number;}
placeholder?: IPlaceholder // Placeholder text
group?: IGroup // Group option. {opacity?:number; backgroundColor?:string; activeOpacity?:number; activeBackgroundColor?:string; disabled?:boolean; deletable?:boolean;}
pageBreak?: IPageBreak // PageBreak option。{font?:string; fontSize?:number; lineDash?:number[];}
zone?: IZoneOption // Zone option。{tipDisabled?:boolean;}
background?: IBackgroundOption // Background option. {color?:string; image?:string; size?:BackgroundSize; repeat?:BackgroundRepeat; applyPageNumbers?:number[]}。default: {color: '#FFFFFF'}
lineBreak?: ILineBreakOption // LineBreak option. {disabled?:boolean; color?:string; lineWidth?:number;}
separator?: ISeparatorOption // Separator option. {lineWidth?:number; strokeStyle?:string;}
lineNumber?: ILineNumberOption // LineNumber option. {size?:number; font?:string; color?:string; disabled?:boolean; right?:number}
pageBorder?: IPageBorderOption // PageBorder option. {color?:string; lineWidth:number; padding?:IPadding; disabled?:boolean;}
badge?: IBadgeOption // Badge option. {top?:number; left?:number}
modeRule?: IModeRule // mode rule option. {print:{imagePreviewerDisabled?: boolean}; readonly:{imagePreviewerDisabled?: boolean}; form:{controlDeletableDisabled?: boolean}}
}
```
## Table Configuration
```typescript
interface ITableOption {
tdPadding?: IPadding // Cell padding. default: [0, 5, 5, 5]
defaultTrMinHeight?: number // Default table row minimum height. default: 42
defaultColMinWidth?: number // Default minimum width for table columns (applied if the overall width is sufficient, otherwise
}
```
## Header Configuration
```typescript
interface IHeader {
top?: number // Size from the top of the page.default: 30
inactiveAlpha?: number // Transparency during deactivation. default: 1
maxHeightRadio?: MaxHeightRatio // Occupies the maximum height ratio of the page.default: HALF
disabled?: boolean // Whether to disable
editable?: boolean // Disable the header content from being edited
}
```
## Footer Configuration
```typescript
interface IFooter {
bottom?: number // The size from the bottom of the page.default: 30
inactiveAlpha?: number // Transparency during deactivation. default: 1
maxHeightRadio?: MaxHeightRatio // Occupies the maximum height ratio of the page.default: HALF
disabled?: boolean // Whether to disable
editable?: boolean // Disable the footer content from being edited
}
```
## Page Number Configuration
```typescript
interface IPageNumber {
bottom?: number // The size from the bottom of the page.default: 60
size?: number // font size. default: 12
font?: string // font. default: Microsoft YaHei
color?: string // font color. default: #000000
rowFlex?: RowFlex // Line alignment. default: CENTER
format?: string // Page number format. default: {pageNo}。example{pageNo}/{pageCount}
numberType?: NumberType // The numeric type. default: ARABIC
disabled?: boolean // Whether to disable
startPageNo?: number // Start page number.default: 1
fromPageNo?: number // Page numbers appear from page number.default: 0
maxPageNo?: number | null // Max page numberstarting from 0.default: null
}
```
## Watermark Configuration
```typescript
interface IWatermark {
data: string // text.
type?: WatermarkType
width?: number
height?: number
color?: string // color. default: #AEB5C0
opacity?: number // transparency. default: 0.3
size?: number // font size. default: 200
font?: string // font. default: Microsoft YaHei
repeat?: boolean // repeat watermark. default: false
gap?: [horizontal: number, vertical: number] // watermark spacing. default: [10,10]
numberType?: NumberType // The numeric type. default: ARABIC
}
```
## Placeholder Text Configuration
```typescript
interface IPlaceholder {
data: string // text.
color?: string // color. default: #DCDFE6
opacity?: number // transparency. default: 1
size?: number // font size. default: 16
font?: string // font. default: Microsoft YaHei
}
```
## LineNumber Configuration
```typescript
interface ILineNumberOption {
size?: number // font size. default: 12
font?: string // font. default: Microsoft YaHei
color?: string // color. default: #000000
disabled?: boolean // Whether to disable. default: false
right?: number // Distance from the main text. default: 20
type?: LineNumberType // Number type (renumber each page, consecutive numbering). default: continuity
}
```
## PageBorder Configuration
```typescript
interface IPageBorderOption {
color?: string // color. default: #000000
lineWidth?: number // line width. default: 1
padding?: IPadding // padding. default: [0, 0, 0, 0]
disabled?: boolean // Whether to disable. default: true
}
```

47
docs/en/guide/override.md Normal file
View File

@ -0,0 +1,47 @@
# Override
## How to Use
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
instance.override.overrideFunction = () => unknown | IOverrideResult
```
```typescript
interface IOverrideResult {
preventDefault?: boolean // Prevent the execution of internal default method. Default prevent
}
```
## paste
Feature: Override internal paste function
Usage:
```javascript
instance.override.paste = (evt?: ClipboardEvent) => unknown | IOverrideResult
```
## copy
Feature: Override internal copy function
Usage:
```javascript
instance.override.copy = () => unknown | IOverrideResult
```
## drop
Feature: Override internal drop function
Usage:
```javascript
instance.override.drop = (evt: DragEvent) => unknown | IOverrideResult
```

View File

@ -0,0 +1,25 @@
# Custom Plugin
::: tip
Official plugin: https://github.com/Hufe921/canvas-editor-plugin
:::
## Write a Plugin
```javascript
export function myPlugin(editor: Editor, options?: Option) {
// 1. updatesee moresrc/plugins/copy
editor.command.updateFunction = () => {}
// 2. addsee moresrc/plugins/markdown
editor.command.addFunction = () => {}
// 3. listener, eventbus, shortcut, contextmenu, override...
}
```
## Use Plugin
```javascript
instance.use(myPlugin, options?: Option)
```

View File

@ -0,0 +1,125 @@
# Official plugin
::: tip
Official plugin: https://github.com/Hufe921/canvas-editor-plugin
Official plugin demo: https://hufe.club/canvas-editor-plugin
:::
## Barcode1d
```javascript
import Editor from "@hufe921/canvas-editor"
import barcode1DPlugin from "@hufe921/canvas-editor-plugin-barcode1d"
const instance = new Editor()
instance.use(barcode1DPlugin)
instance.executeInsertBarcode1D(
content: string,
width: number,
height: number,
options?: JsBarcode.Options
)
```
## Barcode2d
```javascript
import Editor from "@hufe921/canvas-editor"
import barcode2DPlugin from "@hufe921/canvas-editor-plugin-barcode2d"
const instance = new Editor()
instance.use(barcode2DPlugin, options?: IBarcode2DOption)
instance.executeInsertBarcode2D(
content: string,
width: number,
height: number,
hints?: Map<EncodeHintType, any>
)
```
## Code block
```javascript
import Editor from "@hufe921/canvas-editor"
import codeblockPlugin from "@hufe921/canvas-editor-plugin-codeblock"
const instance = new Editor()
instance.use(codeblockPlugin)
instance.executeInsertCodeblock(content: string)
```
## Word
```javascript
import Editor from '@hufe921/canvas-editor'
import docxPlugin from '@hufe921/canvas-editor-plugin-docx'
const instance = new Editor()
instance.use(docxPlugin)
command.executeImportDocx({
arrayBuffer: buffer
})
instance.executeExportDocx({
fileName: string
})
```
## Excel
```javascript
import Editor from '@hufe921/canvas-editor'
import excelPlugin from '@hufe921/canvas-editor-plugin-excel'
const instance = new Editor()
instance.use(excelPlugin)
command.executeImportExcel({
arrayBuffer: buffer
})
```
## Floating toolbar
```javascript
import Editor from '@hufe921/canvas-editor'
import floatingToolbarPlugin from '@hufe921/canvas-editor-plugin-floating-toolbar'
const instance = new Editor()
instance.use(floatingToolbarPlugin)
```
## Diagram
```javascript
import Editor from '@hufe921/canvas-editor'
import diagramPlugin from '@hufe921/canvas-editor-plugin-diagram'
const instance = new Editor()
instance.use(diagramPlugin)
command.executeLoadDiagram({
lang?: Lang
data?: string
onDestroy?: (message?: any) => void
})
```
## Convert uppercase and lowercase
```javascript
import Editor from '@hufe921/canvas-editor'
import casePlugin from '@hufe921/canvas-editor-plugin-case'
const instance = new Editor()
instance.use(casePlugin)
command.executeUpperCase()
command.executeLowerCase()
```

209
docs/en/guide/schema.md Normal file
View File

@ -0,0 +1,209 @@
# Data Structure
```typescript
interface IElement {
// basic
id?: string;
type?: {
TEXT = 'text',
IMAGE = 'image',
TABLE = 'table',
HYPERLINK = 'hyperlink',
SUPERSCRIPT = 'superscript',
SUBSCRIPT = 'subscript',
SEPARATOR = 'separator',
PAGE_BREAK = 'pageBreak',
CONTROL = 'control',
CHECKBOX = 'checkbox',
RADIO = 'radio',
LATEX = 'latex',
TAB = 'tab',
DATE = 'date',
BLOCK = 'block'
};
value: string;
valueList?: IElement[]; // Use of composite elements (hyperlinks, titles, lists, and so on).
extension?: unknown;
externalId?: string;
hide?: boolean;
// style
font?: string;
size?: number;
width?: number;
height?: number;
bold?: boolean;
color?: string;
highlight?: string;
italic?: boolean;
underline?: boolean;
strikeout?: boolean;
rowFlex?: {
LEFT = 'left',
CENTER = 'center',
RIGHT = 'right',
ALIGNMENT = 'alignment',
JUSTIFY = 'justify'
};
rowMargin?: number;
letterSpacing?: number;
textDecoration?: {
style?: TextDecorationStyle;
};
// groupIds
groupIds?: string[];
// table
conceptId?: string;
colgroup?: {
width: number;
}[];
trList?: {
height: number;
pagingRepeat?: boolean;
extension?: unknown;
externalId?: string;
tdList: {
colspan: number;
rowspan: number;
conceptId?: string;
verticalAlign?: VerticalAlign;
backgroundColor?: string;
borderTypes?: TdBorder[];
slashTypes?: TdSlash[];
value: IElement[];
extension?: unknown;
externalId?: string;
disabled?: boolean;
deletable?: boolean;
}[];
}[];
borderType?: TableBorder;
borderColor?: string;
borderWidth?: number;
borderExternalWidth?: number;
tableToolDisabled?: boolean;
// Hyperlinks
url?: string;
// Superscript and subscript
actualSize?: number;
// Dividing line
dashArray?: number[];
// control
control?: {
type: {
TEXT = 'text',
SELECT = 'select',
CHECKBOX = 'checkbox',
RADIO = 'radio',
DATE = 'date',
NUMBER = 'number'
};
value: IElement[] | null;
placeholder?: string;
groupId?: string;
conceptId?: string;
prefix?: string;
postfix?: string;
preText?: string;
postText?: string;
minWidth?: number;
underline?: boolean;
border?: boolean;
extension?: unknown;
indentation?: ControlIndentation;
rowFlex?: RowFlex
deletable?: boolean;
disabled?: boolean;
pasteDisabled?: boolean;
hide?: boolean;
code: string | null;
min?: number;
max?: number;
flexDirection: FlexDirection;
valueSets: {
value: string;
code: string;
}[];
isMultiSelect?: boolean;
multiSelectDelimiter?: string;
dateFormat?: string;
font?: string;
size?: number;
bold?: boolean;
color?: string;
highlight?: string;
italic?: boolean;
strikeout?: boolean;
selectExclusiveOptions?: {
inputAble?: boolean;
}
};
controlComponent?: {
PREFIX = 'prefix',
POSTFIX = 'postfix',
PLACEHOLDER = 'placeholder',
VALUE = 'value',
CHECKBOX = 'checkbox',
RADIO = 'radio'
};
// checkbox
checkbox?: {
value: boolean | null;
};
// radio
radio?: {
value: boolean | null;
};
// LaTeX
laTexSVG?: string;
// date
dateFormat?: string;
// picture
imgDisplay?: {
INLINE = 'inline',
BLOCK = 'block'
}
imgFloatPosition?: {
x: number;
y: number;
pageNo?: number;
}
imgToolDisabled?: boolean;
// block
block?: {
type: {
IFRAME = 'iframe',
VIDEO = 'video'
};
iframeBlock?: {
src?: string;
srcdoc?: string;
};
videoBlock?: {
src: string;
};
};
// title
level?: TitleLevel;
title?: {
conceptId?: string;
deletable?: boolean;
disabled?: boolean;
};
// list
listType?: ListType;
listStyle?: ListStyle;
listWrap?: boolean;
// area
areaId?: string;
area?: {
extension?: unknown;
top?: number;
hide?: boolean;
borderColor?: string;
backgroundColor?: string;
mode?: AreaMode;
deletable?: boolean;
placeholder?: IPlaceholder;
};
}
```

View File

@ -0,0 +1,22 @@
# Custom shortcut keys
## How to use?
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
instance.register.shortcutList([
{
key: KeyMap;
ctrl?: boolean;
meta?: boolean;
mod?: boolean; // windows:ctrl || mac:command
shift?: boolean;
alt?: boolean;
isGlobal?: boolean;
callback?: (command: Command) => any;
disable?: boolean;
}
])
```

View File

@ -0,0 +1,189 @@
# Internal shortcut keys
## Backspace
Feature: Forward delete
## Delete
Feature: Backward delete
## Enter
Feature: Line break
## Shift + Enter
Feature: Line breaks within the list
## ←
Feature: Move to the left
## Shift + ←
Feature: Zoom the selection to the left
## Ctrl/Cmd + ←
Feature: Move to the left(jump word)
## Ctrl/Cmd + Shift + ←
Feature: Zoom the selection to the left(jump word)
## →
Feature: Move to the right
## Shift + →
Feature: Zoom the selection to the right
## Ctrl/Cmd + →
Feature: Move to the right(jump word)
## Ctrl/Cmd + Shift + →
Feature: Zoom the selection to the right(jump word)
## ↑
Feature: Move up
## Shift + ↑
Feature: Zoom up the selection
## ↓
Feature: Move down
## Shift + ↓
Feature: Zoom down the selection
## Esc
Feature: Exit format brush
## Tab
Feature: Increase indent/Move next control
## Shift + Tab
Feature: Move previous control
## Ctrl/Cmd + Z
Feature: Undo
## Ctrl/Cmd + Y
Feature: Redo
## Ctrl/Cmd + C
Feature: Copy
## Ctrl/Cmd + X
Feature: Cut
## Ctrl/Cmd + A
Feature: Select all
## Ctrl/Cmd + S
Feature: Save
## Ctrl/Cmd + {
Feature: Increase the font
## Ctrl/Cmd + }
Feature: Reduce the font
## Ctrl/Cmd + B
Feature: Bold
## Ctrl/Cmd + I
Feature: Italic
## Ctrl/Cmd + U
Feature: Underline
## Ctrl/Cmd + L
Feature: Line left
## Ctrl/Cmd + E
Feature: Line center
## Ctrl/Cmd + R
Feature: Line right
## Ctrl/Cmd + J
Feature: Both sides are aligned
## Ctrl/Cmd + Shift + J
Feature: Line justify
## Ctrl + Shift + X
Feature: Strikethrough
## Ctrl/Cmd + Shift + >
Feature: Superscript
## Ctrl/Cmd + Shift + <
Feature: Subscript
## Ctrl + Alt/Option + 0
Feature: Main body
## Ctrl + Alt/Option + 1
Feature: Header1
## Ctrl + Alt/Option + 2
Feature: Header2
## Ctrl + Alt/Option + 3
Feature: Header3
## Ctrl + Alt/Option + 4
Feature: Header4
## Ctrl + Alt/Option + 5
Feature: Header5
## Ctrl + Alt/Option + 6
Feature: Header6
## Ctrl/Cmd + Shift + I
Feature: Unordered list
## Ctrl/Cmd + Shift + U
Feature: Ordered list

97
docs/en/guide/start.md Normal file
View File

@ -0,0 +1,97 @@
# Getting Started
> WYSIWYG rich text editor.
Benefit from the complete self-implementation of cursor and text layout. The underlying rendering can also be rendered by svg, See code[feature/svg](https://github.com/Hufe921/canvas-editor/tree/feature/svg); Or complete pdf drawing with pdfjs,See code[feature/pdf](https://github.com/Hufe921/canvas-editor/tree/feature/pdf).
::: warning
The official only provides the editor core layer npm package, the menu bar or other external tools can refer to the document extension, or directly refer the implementation of [official](https://github.com/Hufe921/canvas-editor), See details [demo](https://hufe.club/canvas-editor/).
:::
## Features
- Rich text operations (Undo, Redo, Font, Size, Bold, Italic, Underline, Strikeout, Superscript, Alignment, Title, List, ...)
- Insert elements (Table, Image, Link, Code Block, Page Break, Math Formula, Date Picker, Block, ...)
- Print (Based on canvas to picture, pdf drawing)
- Controls (Select, Text, Date, Radio, Checkbox)
- Right-click menu (Internal, Custom)
- Shortcut keys (Internal, Custom)
- Drag and Drop(Text, Element, Control)
- Header, Footer, Page Number
- Page Margin
- Watermark
- Pagination
- Comment
- Catalog
- [Plugin](https://github.com/Hufe921/canvas-editor-plugin)
## TODO
- Computational performance
- Control rule
- Table paging
- Out of the box version for vue, react and other frameworks
## Step. 1: Download NPM Package
```sh
npm i @hufe921/canvas-editor --save
```
## Step. 2: Prepare Container
```html
<div class="canvas-editor"></div>
```
## Step. 3: Instantiate Editor
- Examples that only the body content is included
```javascript
import Editor from '@hufe921/canvas-editor'
new Editor(
document.querySelector('.canvas-editor'),
[
{
value: 'Hello World'
}
],
{}
)
```
- Examples that contain body, header, footer content
```javascript
import Editor from '@hufe921/canvas-editor'
new Editor(
document.querySelector('.canvas-editor'),
{
header: [
{
value: 'Header',
rowFlex: RowFlex.CENTER
}
],
main: [
{
value: 'Hello World'
}
],
footer: [
{
value: 'canvas-editor',
size: 12
}
]
},
{}
)
```
## Step. 4: Configuration Editor
See the next section for details

43
docs/en/index.md Normal file
View File

@ -0,0 +1,43 @@
---
layout: home
title: canvas-editor
titleTemplate: rich text editor by canvas/svg
hero:
name: canvas-editor
text: canvas/svg based rich text editor
actions:
- theme: brand
text: Get Started
link: /en/guide/start.html
- theme: alt
text: View on Github
link: https://github.com/Hufe921/canvas-editor
features:
- icon: 💡
title: WYSIWYG
details: Similar to word pageable, what you see is what you get
- icon: ⚡️
title: Lightweight Data Structure
details: A piece of JSON can render complex styles
- icon: 🛠️
title: Rich Features
details: Supports familiar rich text operations, tables, watermarks, controls, formulas, etc
- icon: 📦
title: Easy to Use
details: The core npm package is officially released, and the menu bar and toolbar can be maintained by themselves
- icon: 🔩
title: Flexible Development Mechanism
details: Through the interface, you can obtain the life cycle, event callback, custom right-click menu, and shortcut keys
- icon: 🔑
title: Full TypeScript Types API
details: Flexible apis and full TypeScript types
---
<style>
.main>p {
max-width:100% !important;
}
</style>

49
docs/guide/api-common.md Normal file
View File

@ -0,0 +1,49 @@
# 通用 API
## splitText
功能:拆分字符
用法:
```javascript
import { splitText } from '@hufe921/canvas-editor'
splitText(text: string): string[]
```
## createDomFromElementList
功能:根据 elementList 创建 dom 树
用法:
```javascript
import { createDomFromElementList } from '@hufe921/canvas-editor'
createDomFromElementList(elementList: IElement[], options?: IEditorOption): HTMLDivElement
```
## getElementListByHTML
功能:根据 HTML 创建 elementList
用法:
```javascript
import { getElementListByHTML } from '@hufe921/canvas-editor'
getElementListByHTML(htmlText: string, options: IGetElementListByHTMLOption): IElement[]
```
## getTextFromElementList
功能:根据 elementList 创建文本
用法:
```javascript
import { getTextFromElementList } from '@hufe921/canvas-editor'
getTextFromElementList(elementList: IElement[]): string
```

View File

@ -0,0 +1,24 @@
# 实例 API
## 使用方式
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
instance.apiName()
```
## destroy
功能:销毁编辑器
用法:
```javascript
instance.destroy()
```
::: warning
仅销毁编辑器 dom 及相关事件,菜单栏、工具栏、外部变量等需自行处理。
:::

File diff suppressed because it is too large Load Diff

331
docs/guide/command-get.md Normal file
View File

@ -0,0 +1,331 @@
# 获取数据命令
## 使用方式
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
const value = instance.command.commandName()
```
## getValue
功能:获取当前文档信息
用法:
```javascript
const {
version: string
data: IEditorData
options: IEditorOption
} = instance.command.getValue(options?: IGetValueOption)
```
## getValueAsync
功能:获取当前文档信息(异步)
用法:
```javascript
const {
version: string
data: IEditorData
options: IEditorOption
} = await instance.command.getValueAsync(options?: IGetValueOption)
```
## getImage
功能:获取当前页面图片 base64 字符串
用法:
```javascript
const base64StringList = await instance.command.getImage(option?: IGetImageOption)
```
## getOptions
功能:获取编辑器配置
用法:
```javascript
const editorOption = await instance.command.getOptions()
```
## getWordCount
功能:获取文档字数
用法:
```javascript
const wordCount = await instance.command.getWordCount()
```
## getCursorPosition
功能: 获取光标位置坐标
用法:
```javascript
const range = instance.command.getCursorPosition()
```
## getRange
功能:获取选区
用法:
```javascript
const range = instance.command.getRange()
```
## getRangeText
功能:获取选区文本
用法:
```javascript
const rangeText = instance.command.getRangeText()
```
## getRangeContext
功能:获取选区上下文
用法:
```javascript
const rangeContext = instance.command.getRangeContext()
```
## getRangeRow
功能:获取选区所在行元素列表
用法:
```javascript
const rowElementList = instance.command.getRangeRow()
```
## getKeywordRangeList
功能:获取关键词所在选区列表
用法:
```javascript
const rangeList = instance.command.getKeywordRangeList()
```
## getKeywordContext
功能:获取关键词所在上下文本信息
用法:
```javascript
const keywordContextList = instance.command.getKeywordContext(payload: string)
```
## getRangeParagraph
功能:获取选区所在段落元素列表
用法:
```javascript
const paragraphElementList = instance.command.getRangeParagraph()
```
## getPaperMargin
功能:获取页边距
用法:
```javascript
const [top: number, right: number, bottom: number, left: number] =
instance.command.getPaperMargin()
```
## getSearchNavigateInfo
功能:获取搜索导航信息
用法:
```javascript
const {
index: number;
count: number;
} = instance.command.getSearchNavigateInfo()
```
## getCatalog
功能:获取目录
用法:
```javascript
const catalog = await instance.command.getCatalog()
```
## getHTML
功能:获取 HTML
用法:
```javascript
const {
header: string
main: string
footer: string
} = await instance.command.getHTML()
```
## getText
功能:获取文本
用法:
```javascript
const {
header: string
main: string
footer: string
} = await instance.command.getText()
```
## getLocale
功能:获取当前语言
用法:
```javascript
const locale = await instance.command.getLocale()
```
## getGroupIds
功能:获取所有成组 id
用法:
```javascript
const groupIds = await instance.command.getGroupIds()
```
## getControlValue
功能:获取控件值
用法:
```javascript
const {
value: string | null
innerText: string | null
zone: EditorZone
elementList?: IElement[]
}[] = await instance.command.getControlValue(payload: IGetControlValueOption)
```
## getControlList
功能:获取所有控件
用法:
```javascript
const controlList = await instance.command.getControlList()
```
## getContainer
功能:获取编辑器容器
用法:
```javascript
const container = await instance.command.getContainer()
```
## getTitleValue
功能:获取标题值
用法:
```javascript
const {
value: string | null
elementList: IElement[]
zone: EditorZone
}[] = await instance.command.getTitleValue(payload: IGetTitleValueOption)
```
## getPositionContextByEvent
功能:获取位置上下文信息通过鼠标事件
用法:
```javascript
const {
pageNo: number
element: IElement | null
rangeRect: RangeRect | null
tableInfo: ITableInfoByEvent | null
}[] = await instance.command.getPositionContextByEvent(evt: MouseEvent, options?: IPositionContextByEventOption)
```
示例:
```javascript
instance.eventBus.on(
'mousemove',
debounce(evt => {
const positionContext = instance.command.getPositionContextByEvent(evt)
console.log(positionContext)
}, 200)
)``
```
## getElementById
功能:根据 id 获取元素
用法:
```javascript
const elementList = await instance.command.getElementById(payload: IGetElementByIdOption)
```
## getAreaValue
功能: 获取区域数据
用法:
```js
const {
id?: string
area: IArea
value: IElement[]
startPageNo: number
endPageNo: number
} = instance.command.getAreaValue(options: IGetAreaValueOption)
```

View File

@ -0,0 +1,44 @@
# 自定义右键菜单
## 使用方式
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
instance.register.contextMenuList([
{
key?: string;
isDivider?: boolean;
icon?: string;
name?: string; // 使用%s代表选区文本。示例搜索%s
shortCut?: string;
disable?: boolean;
when?: (payload: IContextMenuContext) => boolean;
callback?: (command: Command, context: IContextMenuContext) => any;
childMenus?: IRegisterContextMenu[];
}
])
```
## getContextMenuList
功能:获取注册的右键菜单列表
用法:
```javascript
const contextMenuList = await instance.register.getContextMenuList()
```
备注:
```javascript
// 修改内部右键菜单示例
contextmenuList.forEach(menu => {
// 通过菜单key找到菜单项后进行属性修改
if (menu.key === INTERNAL_CONTEXT_MENU_KEY.GLOBAL.PASTE) {
menu.when = () => false
}
})
```

View File

@ -0,0 +1,61 @@
# 内部右键菜单
## 全局
- 剪切
- 复制
- 粘贴
- 全选
- 打印
## 超链接
- 删除链接
- 取消链接
- 编辑链接
## 图片
- 更改图片
- 另存为图片
- 文字环绕
- 嵌入型
- 上下型环绕
- 四周型环绕
- 浮于文字上方
- 衬于文字下方
## 表格
- 表格边框
- 所有框线
- 无框线
- 虚框线
- 外侧框线
- 内侧框线
- 单元格边框
- 上边框
- 右边框
- 下边框
- 左边框
- 正斜线
- 反斜线
- 垂直对齐
- 顶端对齐
- 垂直居中
- 底端对齐
- 插入行列
- 上方插入 1 行
- 下方插入 1 行
- 左侧插入 1 列
- 右侧插入 1 列
- 删除行列
- 删除 1 行
- 删除 1 列
- 删除整个表格
- 合并单元格
- 取消合并
## 控件
- 删除控件

234
docs/guide/eventbus.md Normal file
View File

@ -0,0 +1,234 @@
# 事件监听(eventBus)
## 使用方式
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
// 注册
instance.eventBus.on<K keyof EventMap>(
eventName: K,
callback: EventMap[K]
)
// 移除
instance.eventBus.off<K keyof EventMap>(
eventName: K,
callback: EventMap[K]
)
```
## rangeStyleChange
功能:选区样式发生改变
用法:
```javascript
instance.eventBus.on('rangeStyleChange', (payload: IRangeStyle) => void)
```
## visiblePageNoListChange
功能:可见页发生改变
用法:
```javascript
instance.eventBus.on('visiblePageNoListChange', (payload: number[]) => void)
```
## intersectionPageNoChange
功能:当前页发生改变
用法:
```javascript
instance.eventBus.on('intersectionPageNoChange', (payload: number) => void)
```
## pageSizeChange
功能:当前页数发生改变
用法:
```javascript
instance.eventBus.on('pageSizeChange', (payload: number) => void)
```
## pageScaleChange
功能:当前页面缩放比例发生改变
用法:
```javascript
instance.eventBus.on('pageScaleChange', (payload: number) => void)
```
## contentChange
功能:当前内容发生改变
用法:
```javascript
instance.eventBus.on('contentChange', () => void)
```
## controlChange
功能:当前光标所在控件发生改变
用法:
```javascript
instance.eventBus.on('controlChange', (payload: IControlChangeResult) => void)
```
## controlContentChange
功能:控件内容发生改变
用法:
```javascript
instance.eventBus.on('controlContentChange', (payload: IControlContentChangeResult) => void)
```
## pageModeChange
功能:页面模式发生改变
用法:
```javascript
instance.eventBus.on('pageModeChange', (payload: PageMode) => void)
```
## saved
功能:文档执行保存
用法:
```javascript
instance.eventBus.on('saved', (payload: IEditorResult) => void)
```
## zoneChange
功能:区域发生改变
用法:
```javascript
instance.eventBus.on('zoneChange', (payload: EditorZone) => void)
```
## mousemove
功能:编辑器 mousemove 事件监听
用法:
```javascript
instance.eventBus.on('mousemove', (evt: MouseEvent) => void)
```
## mouseenter
功能:编辑器 mouseenter 事件监听
用法:
```javascript
instance.eventBus.on('mouseenter', (evt: MouseEvent) => void)
```
## mouseleave
功能:编辑器 mouseleave 事件监听
用法:
```javascript
instance.eventBus.on('mouseleave', (evt: MouseEvent) => void)
```
## mousedown
功能:编辑器 mousedown 事件监听
用法:
```javascript
instance.eventBus.on('mousedown', (evt: MouseEvent) => void)
```
## mouseup
功能:编辑器 mouseup 事件监听
用法:
```javascript
instance.eventBus.on('mouseup', (evt: MouseEvent) => void)
```
## click
功能:编辑器 click 事件监听
用法:
```javascript
instance.eventBus.on('click', (evt: MouseEvent) => void)
```
## input
功能:编辑器 input 事件监听
用法:
```javascript
instance.eventBus.on('input', (evt: Event) => void)
```
## positionContextChange
功能:上下文内容发生改变
用法:
```javascript
instance.eventBus.on('positionContextChange', (payload: IPositionContextChangePayload) => void)
```
## imageSizeChange
功能:图片尺寸发生改变事件
用法:
```javascript
instance.eventBus.on('imageSizeChange', (payload: { element: IElement }) => void)
```
## imageMousedown
功能:图片 mousedown 事件
用法:
```javascript
instance.eventBus.on('imageMousedown', (payload: {
evt: MouseEvent
element: IElement
}) => void)
```

111
docs/guide/i18n.md Normal file
View File

@ -0,0 +1,111 @@
# 国际化
## 使用方式
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
// 注册
instance.register.langMap(locale: string, lang: ILang)
// 设置
instance.command.executeSetLocale(locale)
```
## ILang
```typescript
interface ILang {
contextmenu: {
global: {
cut: string
copy: string
paste: string
selectAll: string
print: string
}
control: {
delete: string
}
hyperlink: {
delete: string
cancel: string
edit: string
}
image: {
change: string
saveAs: string
textWrap: string
textWrapType: {
embed: string
upDown: string
surround: string
floatTop: string
floatBottom: string
}
table: {
insertRowCol: string
insertTopRow: string
insertBottomRow: string
insertLeftCol: string
insertRightCol: string
deleteRowCol: string
deleteRow: string
deleteCol: string
deleteTable: string
mergeCell: string
mergeCancelCell: string
verticalAlign: string
verticalAlignTop: string
verticalAlignMiddle: string
verticalAlignBottom: string
border: string
borderAll: string
borderEmpty: string
borderDash: string
borderExternal: string
borderInternal: string
borderTd: string
borderTdTop: string
borderTdRight: string
borderTdBottom: string
borderTdLeft: string
borderTdForward: string
borderTdBack: string
}
}
datePicker: {
now: string
confirm: string
return: string
timeSelect: string
weeks: {
sun: string
mon: string
tue: string
wed: string
thu: string
fri: string
sat: string
}
year: string
month: string
hour: string
minute: string
second: string
}
frame: {
header: string
footer: string
}
pageBreak: {
displayName: string
}
zone: {
headerTip: string
footerTip: string
}
}
```

126
docs/guide/listener.md Normal file
View File

@ -0,0 +1,126 @@
# 事件监听(listener)
::: warning
listener 只能响应一个方法,后续不再添加新监听方法,推荐使用 eventBus 进行事件监听。
:::
## 使用方式
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
instance.listener.eventName = ()=>{}
```
## rangeStyleChange
功能:选区样式发生改变
用法:
```javascript
instance.listener.rangeStyleChange = (payload: IRangeStyle) => {}
```
## visiblePageNoListChange
功能:可见页发生改变
用法:
```javascript
instance.listener.visiblePageNoListChange = (payload: number[]) => {}
```
## intersectionPageNoChange
功能:当前页发生改变
用法:
```javascript
instance.listener.intersectionPageNoChange = (payload: number) => {}
```
## pageSizeChange
功能:当前页数发生改变
用法:
```javascript
instance.listener.pageSizeChange = (payload: number) => {}
```
## pageScaleChange
功能:当前页面缩放比例发生改变
用法:
```javascript
instance.listener.pageScaleChange = (payload: number) => {}
```
## contentChange
功能:当前内容发生改变
用法:
```javascript
instance.listener.contentChange = () => {}
```
## controlChange
功能:当前光标所在控件发生改变
用法:
```javascript
instance.listener.controlChange = (payload: IControlChangeResult) => {}
```
## controlContentChange
功能:控件内容发生改变
用法:
```javascript
instance.listener.controlContentChange = (
payload: IControlContentChangeResult
) => {}
```
## pageModeChange
功能:页面模式发生改变
用法:
```javascript
instance.listener.pageModeChange = (payload: PageMode) => {}
```
## saved
功能:文档执行保存
用法:
```javascript
instance.listener.saved = (payload: IEditorResult) => {}
```
## zoneChange
功能:区域发生改变
用法:
```javascript
instance.listener.zoneChange = (payload: EditorZone) => {}
```

189
docs/guide/option.md Normal file
View File

@ -0,0 +1,189 @@
# 配置
## 使用方式
```javascript
import Editor from "@hufe921/canvas-editor"
new Editor(container, IEditorData | IElement[], {
// 配置项
})
```
## 完整配置
```typescript
interface IEditorOption {
mode?: EditorMode // 编辑器模式:编辑、清洁(不显示视觉辅助元素。如:分页符)、只读、表单(仅控件内可编辑)、打印(不显示辅助元素、未书写控件及前后括号)、设计模式(不可删除、只读等配置不控制)。默认:编辑
locale?: string // 多语言类型。默认zhCN
defaultType?: string // 默认元素类型。默认TEXT
defaultColor?: string // 默认字体颜色。默认:#000000
defaultFont?: string // 默认字体。默认Microsoft YaHei
defaultSize?: number // 默认字号。默认16
minSize?: number // 最小字号。默认5
maxSize?: number // 最大字号。默认72
defaultBasicRowMarginHeight?: number // 默认行高。默认8
defaultRowMargin?: number // 默认行间距。默认1
defaultTabWidth?: number // 默认tab宽度。默认32
width?: number // 纸张宽度。默认794
height?: number // 纸张高度。默认1123
scale?: number // 缩放比例。默认1
pageGap?: number // 纸张间隔。默认20
underlineColor?: string // 下划线颜色。默认:#000000
strikeoutColor?: string // 删除线颜色。默认:#FF0000
rangeColor?: string // 选区颜色。默认:#AECBFA
rangeAlpha?: number // 选区透明度。默认0.6
rangeMinWidth?: number // 选区最小宽度。默认5
searchMatchColor?: string // 搜索高亮颜色。默认:#FFFF00
searchNavigateMatchColor?: string // 搜索导航高亮颜色。默认:#AAD280
searchMatchAlpha?: number // 搜索高亮透明度。默认0.6
highlightAlpha?: number // 高亮元素透明度。默认0.6
highlightMarginHeight?: number // 高亮元素边距高度。默认8
resizerColor?: string // 图片尺寸器颜色。默认:#4182D9
resizerSize?: number // 图片尺寸器大小。默认5
marginIndicatorSize?: number // 页边距指示器长度。默认35
marginIndicatorColor?: string // 页边距指示器颜色。默认:#BABABA
margins?: IMargin // 页面边距。默认:[100, 120, 100, 120]
pageMode?: PageMode // 纸张模式:连页、分页。默认:分页
renderMode?: RenderMode // 渲染模式:极速(多个字组合渲染)、兼容(逐字渲染:避免浏览器字体等环境差异)。默认:极速
defaultHyperlinkColor?: string // 默认超链接颜色。默认:#0000FF
table?: ITableOption // 表格配置。{tdPadding?:IPadding; defaultTrMinHeight?:number; defaultColMinWidth?:number}
header?: IHeader // 页眉信息。{top?:number; maxHeightRadio?:MaxHeightRatio;}
footer?: IFooter // 页脚信息。{bottom?:number; maxHeightRadio?:MaxHeightRatio;}
pageNumber?: IPageNumber // 页码信息。{bottom:number; size:number; font:string; color:string; rowFlex:RowFlex; format:string; numberType:NumberType;}
paperDirection?: PaperDirection // 纸张方向:纵向、横向
inactiveAlpha?: number // 正文内容失焦时透明度。默认值0.6
historyMaxRecordCount?: number // 历史撤销重做最大记录次数。默认100次
printPixelRatio?: number // 打印像素比率值越大越清晰但尺寸越大。默认3
maskMargin?: IMargin // 编辑器上的遮盖边距(如悬浮到编辑器上的菜单栏、底部工具栏)。默认:[0, 0, 0, 0]
letterClass?: string[] // 排版支持的字母类。默认a-zA-Z。内置可选择的字母表类LETTER_CLASS
contextMenuDisableKeys?: string[] // 禁用的右键菜单。默认:[]
shortcutDisableKeys?: string[] // 禁用的快捷键。默认:[]
scrollContainerSelector?: string // 滚动区域选择器。默认document
pageOuterSelectionDisable?: boolean // 鼠标移出页面时选区禁用。默认false
wordBreak?: WordBreak // 单词与标点断行BREAK_WORD首行不出现标点&单词不拆分、BREAK_ALL按字符宽度撑满后折行。默认BREAK_WORD
watermark?: IWatermark // 水印信息。{data:string; color?:string; opacity?:number; size?:number; font?:string; numberType:NumberType;}
control?: IControlOption // 控件信息。 {placeholderColor?:string; bracketColor?:string; prefix?:string; postfix?:string; borderWidth?: number; borderColor?: string; activeBackgroundColor?: string; disabledBackgroundColor?: string; existValueBackgroundColor?: string; noValueBackgroundColor?: string;}
checkbox?: ICheckboxOption // 复选框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string; verticalAlign?: VerticalAlign;}
radio?: IRadioOption // 单选框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string; verticalAlign?: VerticalAlign;}
cursor?: ICursorOption // 光标样式。{width?: number; color?: string; dragWidth?: number; dragColor?: string; dragFloatImageDisabled?: boolean;}
title?: ITitleOption // 标题配置。{ defaultFirstSize?: number; defaultSecondSize?: number; defaultThirdSize?: number defaultFourthSize?: number; defaultFifthSize?: number; defaultSixthSize?: number;}
placeholder?: IPlaceholder // 编辑器空白占位文本
group?: IGroup // 成组配置。{opacity?:number; backgroundColor?:string; activeOpacity?:number; activeBackgroundColor?:string; disabled?:boolean; deletable?:boolean;}
pageBreak?: IPageBreak // 分页符配置。{font?:string; fontSize?:number; lineDash?:number[];}
zone?: IZoneOption // 编辑器区域配置。{tipDisabled?:boolean;}
background?: IBackgroundOption // 背景配置。{color?:string; image?:string; size?:BackgroundSize; repeat?:BackgroundRepeat; applyPageNumbers?:number[]}。默认:{color: '#FFFFFF'}
lineBreak?: ILineBreakOption // 换行符配置。{disabled?:boolean; color?:string; lineWidth?:number;}
separator?: ISeparatorOption // 分隔符配置。{lineWidth?:number; strokeStyle?:string;}
lineNumber?: ILineNumberOption // 行号配置。{size?:number; font?:string; color?:string; disabled?:boolean; right?:number}
pageBorder?: IPageBorderOption // 页面边框配置。{color?:string; lineWidth:number; padding?:IPadding; disabled?:boolean;}
badge?: IBadgeOption // 徽章配置。{top?:number; left?:number}
modeRule?: IModeRule // 编辑器模式规则配置。{print:{imagePreviewerDisabled?: boolean}; readonly:{imagePreviewerDisabled?: boolean}; form:{controlDeletableDisabled?: boolean}}
}
```
## 表格配置
```typescript
interface ITableOption {
tdPadding?: IPadding // 单元格内边距。默认:[0, 5, 5, 5]
defaultTrMinHeight?: number // 默认表格行最小高度。默认42
defaultColMinWidth?: number // 默认表格列最小宽度整体宽度足够时应用否则会按比例缩小。默认40
}
```
## 页眉配置
```typescript
interface IHeader {
top?: number // 距离页面顶部大小。默认30
inactiveAlpha?: number // 失活时透明度。默认1
maxHeightRadio?: MaxHeightRatio // 占页面最大高度比。默认HALF
disabled?: boolean // 是否禁用
editable?: boolean // 禁止编辑标题内容
}
```
## 页脚配置
```typescript
interface IFooter {
bottom?: number // 距离页面底部大小。默认30
inactiveAlpha?: number // 失活时透明度。默认1
maxHeightRadio?: MaxHeightRatio // 占页面最大高度比。默认HALF
disabled?: boolean // 是否禁用
editable?: boolean // 禁止编辑页脚内容
}
```
## 页码配置
```typescript
interface IPageNumber {
bottom?: number // 距离页面底部大小。默认60
size?: number // 字体大小。默认12
font?: string // 字体。默认Microsoft YaHei
color?: string // 字体颜色。默认:#000000
rowFlex?: RowFlex // 行对齐方式。默认CENTER
format?: string // 页码格式。默认:{pageNo}。示例:第{pageNo}页/共{pageCount}页
numberType?: NumberType // 数字类型。默认ARABIC
disabled?: boolean // 是否禁用
startPageNo?: number // 起始页码。默认1
fromPageNo?: number // 从第几页开始出现页码。默认0
maxPageNo?: number | null // 最大页码从0开始。默认null
}
```
## 水印配置
```typescript
interface IWatermark {
data: string // 文本。
type?: WatermarkType
width?: number
height?: number
color?: string // 颜色。默认:#AEB5C0
opacity?: number // 透明度。默认0.3
size?: number // 字体大小。默认200
font?: string // 字体。默认Microsoft YaHei
repeat?: boolean // 重复水印。默认false
gap?: [horizontal: number, vertical: number] // 水印间距。默认:[10,10]
numberType: NumberType.ARABIC // 页码格式。默认:{pageNo}。示例:第{pageNo}页/共{pageCount}页
}
```
## 占位文本配置
```typescript
interface IPlaceholder {
data: string // 文本。
color?: string // 颜色。默认:#DCDFE6
opacity?: number // 透明度。默认1
size?: number // 字体大小。默认16
font?: string // 字体。默认Microsoft YaHei
}
```
## 行号配置
```typescript
interface ILineNumberOption {
size?: number // 字体大小。默认12
font?: string // 字体。默认Microsoft YaHei
color?: string // 颜色。默认:#000000
disabled?: boolean // 是否禁用。默认true
right?: number // 距离正文距离。默认20
type?: LineNumberType // 编号类型(每页重新编号、连续编号)。默认:连续编号
}
```
## 页面边框配置
```typescript
interface IPageBorderOption {
color?: string // 颜色。默认:#000000
lineWidth?: number // 宽度。默认1
padding?: IPadding // 距离正文内边距。默认:[0, 5, 0, 5]
disabled?: boolean // 是否禁用。默认true
}
```

47
docs/guide/override.md Normal file
View File

@ -0,0 +1,47 @@
# 重写方法
## 使用方式
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
instance.override.overrideFunction = () => unknown | IOverrideResult
```
```typescript
interface IOverrideResult {
preventDefault?: boolean // 阻止执行内部默认方法。默认阻止
}
```
## paste
功能:重写粘贴方法
用法:
```javascript
instance.override.paste = (evt?: ClipboardEvent) => unknown | IOverrideResult
```
## copy
功能:重写复制方法
用法:
```javascript
instance.override.copy = () => unknown | IOverrideResult
```
## drop
功能:重写拖放方法
用法:
```javascript
instance.override.drop = (evt: DragEvent) => unknown | IOverrideResult
```

View File

@ -0,0 +1,25 @@
# 自定义插件
::: tip
官方维护插件仓库https://github.com/Hufe921/canvas-editor-plugin
:::
## 开发插件
```javascript
export function myPlugin(editor: Editor, options?: Option) {
// 1. 修改方法详见src/plugins/copy
editor.command.updateFunction = () => {}
// 2. 增加方法详见src/plugins/markdown
editor.command.addFunction = () => {}
// 3. 事件监听、快捷键、右键菜单、重写方法等组合处理
}
```
## 使用插件
```javascript
instance.use(myPlugin, options?: Option)
```

View File

@ -0,0 +1,125 @@
# 官方插件
::: tip
官方维护插件仓库https://github.com/Hufe921/canvas-editor-plugin
官方维护插件演示地址https://hufe.club/canvas-editor-plugin
:::
## 条形码
```javascript
import Editor from "@hufe921/canvas-editor"
import barcode1DPlugin from "@hufe921/canvas-editor-plugin-barcode1d"
const instance = new Editor()
instance.use(barcode1DPlugin)
instance.executeInsertBarcode1D(
content: string,
width: number,
height: number,
options?: JsBarcode.Options
)
```
## 二维码
```javascript
import Editor from "@hufe921/canvas-editor"
import barcode2DPlugin from "@hufe921/canvas-editor-plugin-barcode2d"
const instance = new Editor()
instance.use(barcode2DPlugin, options?: IBarcode2DOption)
instance.executeInsertBarcode2D(
content: string,
width: number,
height: number,
hints?: Map<EncodeHintType, any>
)
```
## 代码块
```javascript
import Editor from "@hufe921/canvas-editor"
import codeblockPlugin from "@hufe921/canvas-editor-plugin-codeblock"
const instance = new Editor()
instance.use(codeblockPlugin)
instance.executeInsertCodeblock(content: string)
```
## Word
```javascript
import Editor from '@hufe921/canvas-editor'
import docxPlugin from '@hufe921/canvas-editor-plugin-docx'
const instance = new Editor()
instance.use(docxPlugin)
command.executeImportDocx({
arrayBuffer: buffer
})
instance.executeExportDocx({
fileName: string
})
```
## Excel
```javascript
import Editor from '@hufe921/canvas-editor'
import excelPlugin from '@hufe921/canvas-editor-plugin-excel'
const instance = new Editor()
instance.use(excelPlugin)
command.executeImportExcel({
arrayBuffer: buffer
})
```
## 悬浮工具
```javascript
import Editor from '@hufe921/canvas-editor'
import floatingToolbarPlugin from '@hufe921/canvas-editor-plugin-floating-toolbar'
const instance = new Editor()
instance.use(floatingToolbarPlugin)
```
## 流程图
```javascript
import Editor from '@hufe921/canvas-editor'
import diagramPlugin from '@hufe921/canvas-editor-plugin-diagram'
const instance = new Editor()
instance.use(diagramPlugin)
command.executeLoadDiagram({
lang?: Lang
data?: string
onDestroy?: (message?: any) => void
})
```
## 大小写转换
```javascript
import Editor from '@hufe921/canvas-editor'
import casePlugin from '@hufe921/canvas-editor-plugin-case'
const instance = new Editor()
instance.use(casePlugin)
command.executeUpperCase()
command.executeLowerCase()
```

209
docs/guide/schema.md Normal file
View File

@ -0,0 +1,209 @@
# 数据结构
```typescript
interface IElement {
// 基础
id?: string;
type?: {
TEXT = 'text',
IMAGE = 'image',
TABLE = 'table',
HYPERLINK = 'hyperlink',
SUPERSCRIPT = 'superscript',
SUBSCRIPT = 'subscript',
SEPARATOR = 'separator',
PAGE_BREAK = 'pageBreak',
CONTROL = 'control',
CHECKBOX = 'checkbox',
RADIO = 'radio',
LATEX = 'latex',
TAB = 'tab',
DATE = 'date',
BLOCK = 'block'
};
value: string;
valueList?: IElement[]; // 复合元素(超链接、标题、列表等)使用
extension?: unknown;
externalId?: string;
hide?: boolean;
// 样式
font?: string;
size?: number;
width?: number;
height?: number;
bold?: boolean;
color?: string;
highlight?: string;
italic?: boolean;
underline?: boolean;
strikeout?: boolean;
rowFlex?: {
LEFT = 'left',
CENTER = 'center',
RIGHT = 'right',
ALIGNMENT = 'alignment',
JUSTIFY = 'justify'
};
rowMargin?: number;
letterSpacing?: number;
textDecoration?: {
style?: TextDecorationStyle;
};
// 组信息-可用于批注等其他成组使用场景
groupIds?: string[];
// 表格
conceptId?: string;
colgroup?: {
width: number;
}[];
trList?: {
height: number;
pagingRepeat?: boolean;
extension?: unknown;
externalId?: string;
tdList: {
colspan: number;
rowspan: number;
conceptId?: string;
verticalAlign?: VerticalAlign;
backgroundColor?: string;
borderTypes?: TdBorder[];
slashTypes?: TdSlash[];
value: IElement[];
extension?: unknown;
externalId?: string;
disabled?: boolean;
deletable?: boolean;
}[];
}[];
borderType?: TableBorder;
borderColor?: string;
borderWidth?: number;
borderExternalWidth?: number;
tableToolDisabled?: boolean;
// 超链接
url?: string;
// 上下标
actualSize?: number;
// 分割线
dashArray?: number[];
// 控件
control?: {
type: {
TEXT = 'text',
SELECT = 'select',
CHECKBOX = 'checkbox',
RADIO = 'radio'
DATE = 'date',
NUMBER = 'number'
};
value: IElement[] | null;
placeholder?: string;
groupId?: string;
conceptId?: string;
prefix?: string;
postfix?: string;
preText?: string;
postText?: string;
minWidth?: number;
underline?: boolean;
border?: boolean;
extension?: unknown;
indentation?: ControlIndentation;
rowFlex?: RowFlex
deletable?: boolean;
disabled?: boolean;
pasteDisabled?: boolean;
hide?: boolean;
code: string | null;
min?: number;
max?: number;
flexDirection: FlexDirection;
valueSets: {
value: string;
code: string;
}[];
isMultiSelect?: boolean;
multiSelectDelimiter?: string;
dateFormat?: string;
font?: string;
size?: number;
bold?: boolean;
color?: string;
highlight?: string;
italic?: boolean;
strikeout?: boolean;
selectExclusiveOptions?: {
inputAble?: boolean;
}
};
controlComponent?: {
PREFIX = 'prefix',
POSTFIX = 'postfix',
PLACEHOLDER = 'placeholder',
VALUE = 'value',
CHECKBOX = 'checkbox',
RADIO = 'radio'
};
// 复选框
checkbox?: {
value: boolean | null;
};
// 单选框
radio?: {
value: boolean | null;
};
// LaTeX
laTexSVG?: string;
// 日期
dateFormat?: string;
// 图片
imgDisplay?: {
INLINE = 'inline',
BLOCK = 'block'
}
imgFloatPosition?: {
x: number;
y: number;
pageNo?: number;
}
imgToolDisabled?: boolean;
// 内容块
block?: {
type: {
IFRAME = 'iframe',
VIDEO = 'video'
};
iframeBlock?: {
src?: string;
srcdoc?: string;
};
videoBlock?: {
src: string;
};
};
// 标题
level?: TitleLevel;
title?: {
conceptId?: string;
deletable?: boolean;
disabled?: boolean;
};
// 列表
listType?: ListType;
listStyle?: ListStyle;
listWrap?: boolean;
// 区域
areaId?: string;
area?: {
extension?: unknown;
top?: number;
hide?: boolean;
borderColor?: string;
backgroundColor?: string;
mode?: AreaMode;
deletable?: boolean;
placeholder?: IPlaceholder;
};
}
```

View File

@ -0,0 +1,22 @@
# 自定义快捷键
## 使用方式
```javascript
import Editor from "@hufe921/canvas-editor"
const instance = new Editor(container, <IElement[]>data, options)
instance.register.shortcutList([
{
key: KeyMap;
ctrl?: boolean;
meta?: boolean;
mod?: boolean; // windows:ctrl || mac:command
shift?: boolean;
alt?: boolean;
isGlobal?: boolean;
callback?: (command: Command) => any;
disable?: boolean;
}
])
```

View File

@ -0,0 +1,189 @@
# 内部快捷键
## Backspace
功能:向前删除
## Delete
功能:向后删除
## Enter
功能:换行
## Shift + Enter
功能:列表内换行
## ←
功能:向左移动
## Shift + ←
功能:向左缩放选区
## Ctrl/Cmd + ←
功能:向左移动(跳单词)
## Ctrl/Cmd + Shift + ←
功能:向左缩放选区(跳单词)
## →
功能:向右移动
## Shift + →
功能:向右缩放选区
## Ctrl/Cmd + →
功能:向右移动(跳单词)
## Ctrl/Cmd + Shift + →
功能:向右缩放选区(跳单词)
## ↑
功能:向上移动
## Shift + ↑
功能:向上缩放选区
## ↓
功能:向下移动
## Shift + ↓
功能:向下缩放选区
## Esc
功能:退出格式刷
## Tab
功能:增加缩进/移动到下一个控件
## Shift + Tab
功能:移动到上一个控件
## Ctrl/Cmd + Z
功能:撤销
## Ctrl/Cmd + Y
功能:重做
## Ctrl/Cmd + C
功能:复制
## Ctrl/Cmd + X
功能:剪切
## Ctrl/Cmd + A
功能:全选
## Ctrl/Cmd + S
功能:保存
## Ctrl/Cmd + {
功能:增大字体
## Ctrl/Cmd + }
功能:减小字体
## Ctrl/Cmd + B
功能:加粗
## Ctrl/Cmd + I
功能:斜体
## Ctrl/Cmd + U
功能:下划线
## Ctrl/Cmd + L
功能:行居左
## Ctrl/Cmd + E
功能:行居中
## Ctrl/Cmd + R
功能:行居右
## Ctrl/Cmd + J
功能:两端对齐
## Ctrl/Cmd + Shift + J
功能:分散对齐
## Ctrl + Shift + X
功能:删除线
## Ctrl/Cmd + Shift + >
功能:上标
## Ctrl/Cmd + Shift + <
功能:下标
## Ctrl + Alt/Option + 0
功能:正文
## Ctrl + Alt/Option + 1
功能:标题一
## Ctrl + Alt/Option + 2
功能:标题二
## Ctrl + Alt/Option + 3
功能:标题三
## Ctrl + Alt/Option + 4
功能:标题四
## Ctrl + Alt/Option + 5
功能:标题五
## Ctrl + Alt/Option + 6
功能:标题六
## Ctrl/Cmd + Shift + I
功能:无序列表
## Ctrl/Cmd + Shift + U
功能:有序列表

97
docs/guide/start.md Normal file
View File

@ -0,0 +1,97 @@
# 入门
> 所见即所得的富文本编辑器。
得益于光标及文字排版的完全自行实现。绘制底层也可由 svg 渲染,详见代码:[feature/svg](https://github.com/Hufe921/canvas-editor/tree/feature/svg);或借助 pdfjs 可以完成 pdf 的绘制,详见代码:[feature/pdf](https://github.com/Hufe921/canvas-editor/tree/feature/pdf)。
::: warning
官方仅提供编辑器核心层 npm 包,菜单栏或其他外部工具可自行参考文档扩展,或直接参考[官方](https://github.com/Hufe921/canvas-editor)实现,详见[demo](https://hufe.club/canvas-editor/)。
:::
## 功能点
- 富文本操作(撤销、重做、字体、字号、加粗、斜体、下划线、删除线、上下标、对齐方式、标题、列表.....
- 插入元素表格、图片、链接、代码块、分页符、Math 公式、日期选择器、内容块......
- 打印(基于 canvas 转图片、pdf 绘制)
- 控件(单选、文本、日期、单选框组、复选框组)
- 右键菜单(内部、自定义)
- 快捷键(内部、自定义)
- 拖拽(文字、元素、控件)
- 页眉、页脚、页码
- 页边距
- 分页
- 水印
- 批注
- 目录
- [插件](https://github.com/Hufe921/canvas-editor-plugin)
## 待开发
- 计算性能
- 控件规则
- 表格分页
- vue、react 等框架开箱即用版
## Step. 1: 下载 npm 包
```sh
npm i @hufe921/canvas-editor --save
```
## Step. 2: 准备一个容器
```html
<div class="canvas-editor"></div>
```
## Step. 3: 实例化编辑器
- 仅包含正文内容
```javascript
import Editor from '@hufe921/canvas-editor'
new Editor(
document.querySelector('.canvas-editor'),
[
{
value: 'Hello World'
}
],
{}
)
```
- 包含正文、页眉、页脚内容
```javascript
import Editor from '@hufe921/canvas-editor'
new Editor(
document.querySelector('.canvas-editor'),
{
header: [
{
value: 'Header',
rowFlex: RowFlex.CENTER
}
],
main: [
{
value: 'Hello World'
}
],
footer: [
{
value: 'canvas-editor',
size: 12
}
]
},
{}
)
```
## Step. 4: 配置编辑器
详见下一节

43
docs/index.md Normal file
View File

@ -0,0 +1,43 @@
---
layout: home
title: canvas-editor
titleTemplate: rich text editor by canvas/svg
hero:
name: canvas-editor
text: 基于canvas/svg的富文本编辑器
actions:
- theme: brand
text: 开始
link: /guide/start.html
- theme: alt
text: 在 GitHub 上查看
link: https://github.com/Hufe921/canvas-editor
features:
- icon: 💡
title: 所见即所得
details: 类word可分页所见即所得
- icon: ⚡️
title: 轻量的数据结构
details: 一段JSON即可呈现复杂样式
- icon: 🛠️
title: 丰富的功能
details: 支持常见富文本操作、表格、水印、控件、公式等
- icon: 📦
title: 使用方便
details: 官方发布核心npm包菜单栏、工具栏可自行维护
- icon: 🔩
title: 灵活的开发机制
details: 通过接口可获取生命周期、事件回调、自定义右键菜单、快捷键等
- icon: 🔑
title: 完全类型化的API
details: 灵活的 API 和完整的 TypeScript 类型
---
<style>
.main>p {
max-width:100% !important;
}
</style>

BIN
docs/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

421
index.html Normal file
View File

@ -0,0 +1,421 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>canvas-editor</title>
</head>
<body>
<div id="app">
<div class="menu" editor-component="menu">
<div class="menu-item">
<div class="menu-item__undo">
<i></i>
</div>
<div class="menu-item__redo">
<i></i>
</div>
<div class="menu-item__painter" title="格式刷(双击可连续使用)">
<i></i>
</div>
<div class="menu-item__format" title="清除格式">
<i></i>
</div>
</div>
<div class="menu-divider"></div>
<div class="menu-item">
<div class="menu-item__font">
<span class="select" title="字体">微软雅黑</span>
<div class="options">
<ul>
<li data-family="Microsoft YaHei" style="font-family:'Microsoft YaHei';">微软雅黑</li>
<li data-family="华文宋体" style="font-family:'华文宋体';">华文宋体</li>
<li data-family="华文黑体" style="font-family:'华文黑体';">华文黑体</li>
<li data-family="华文仿宋" style="font-family:'华文仿宋';">华文仿宋</li>
<li data-family="华文楷体" style="font-family:'华文楷体';">华文楷体</li>
<li data-family="华文琥珀" style="font-family:'华文琥珀';">华文琥珀</li>
<li data-family="华文楷体" style="font-family:'华文楷体';">华文楷体</li>
<li data-family="华文隶书" style="font-family:'华文隶书';">华文隶书</li>
<li data-family="华文新魏" style="font-family:'华文新魏';">华文新魏</li>
<li data-family="华文行楷" style="font-family:'华文行楷';">华文行楷</li>
<li data-family="华文中宋" style="font-family:'华文中宋';">华文中宋</li>
<li data-family="华文彩云" style="font-family:'华文彩云';">华文彩云</li>
<li data-family="Arial" style="font-family:'Arial';">Arial</li>
<li data-family="Segoe UI" style="font-family:'Segoe UI';">Segoe UI</li>
<li data-family="Ink Free" style="font-family:'Ink Free';">Ink Free</li>
<li data-family="Fantasy" style="font-family:'Fantasy';">Fantasy</li>
</ul>
</div>
</div>
<div class="menu-item__size">
<span class="select" title="字体">小四</span>
<div class="options">
<ul>
<li data-size="56">初号</li>
<li data-size="48">小初</li>
<li data-size="34">一号</li>
<li data-size="32">小一</li>
<li data-size="29">二号</li>
<li data-size="24">小二</li>
<li data-size="21">三号</li>
<li data-size="20">小三</li>
<li data-size="18">四号</li>
<li data-size="16">小四</li>
<li data-size="14">五号</li>
<li data-size="12">小五</li>
<li data-size="10">六号</li>
<li data-size="8">小六</li>
<li data-size="7">七号</li>
<li data-size="6">八号</li>
</ul>
</div>
</div>
<div class="menu-item__size-add">
<i></i>
</div>
<div class="menu-item__size-minus">
<i></i>
</div>
<div class="menu-item__bold">
<i></i>
</div>
<div class="menu-item__italic">
<i></i>
</div>
<div class="menu-item__underline">
<i></i>
<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">
<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">
<ul>
<li data-rowmargin='1'>1</li>
<li data-rowmargin="1.25">1.25</li>
<li data-rowmargin="1.5">1.5</li>
<li data-rowmargin="1.75">1.75</li>
<li data-rowmargin="2">2</li>
<li data-rowmargin="2.5">2.5</li>
<li data-rowmargin="3">3</li>
</ul>
</div>
</div>
<div class="menu-item__list">
<i></i>
<div class="options">
<ul>
<li>
<label>取消列表</label>
</li>
<li data-list-type="ol" data-list-style='decimal'>
<label>有序列表:</label>
<ol>
<li>________</li>
</ol>
</li>
<li data-list-type="ul" data-list-style='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: '☐ ';">
<li>________</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
<div class="menu-divider"></div>
<div class="menu-item">
<div class="menu-item__table">
<i title="表格"></i>
</div>
<div class="menu-item__table__collapse">
<div class="table-close">×</div>
<div class="table-title">
<span class="table-select">插入</span>
<span>表格</span>
</div>
<div class="table-panel"></div>
</div>
<div class="menu-item__image">
<i title="图片"></i>
<input type="file" id="image" accept=".png, .jpg, .jpeg, .svg, .gif">
</div>
<div class="menu-item__hyperlink">
<i title="超链接"></i>
</div>
<div class="menu-item__separator">
<i title="分割线"></i>
<div class="options">
<ul>
<li data-separator='0,0'>
<i></i>
</li>
<li data-separator="1,1">
<i></i>
</li>
<li data-separator="3,1">
<i></i>
</li>
<li data-separator="4,4">
<i></i>
</li>
<li data-separator="7,3,3,3">
<i></i>
</li>
<li data-separator="6,2,2,2,2,2">
<i></i>
</li>
</ul>
</div>
</div>
<div class="menu-item__watermark">
<i title="水印(添加、删除)"></i>
<div class="options">
<ul>
<li data-menu="add">添加水印</li>
<li data-menu="delete">删除水印</li>
</ul>
</div>
</div>
<div class="menu-item__codeblock" title="代码块">
<i></i>
</div>
<div class="menu-item__page-break" title="分页符">
<i></i>
</div>
<div class="menu-item__control">
<i title="控件"></i>
<div class="options">
<ul>
<li data-control='text'>文本</li>
<li data-control="number">数值</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">
<i></i>
</div>
<div class="menu-item__date">
<i title="日期"></i>
<div class="options">
<ul>
<li data-format="yyyy-MM-dd"></li>
<li data-format="yyyy-MM-dd hh:mm:ss"></li>
</ul>
</div>
</div>
<div class="menu-item__block" title="内容块">
<i></i>
</div>
</div>
<div class="menu-divider"></div>
<div class="menu-item">
<div class="menu-item__search" data-menu="search">
<i></i>
</div>
<div class="menu-item__search__collapse" data-menu="search">
<div class="menu-item__search__collapse__search">
<input type="text" />
<label class="search-result"></label>
<div class="arrow-left">
<i></i>
</div>
<div class="arrow-right">
<i></i>
</div>
<span>×</span>
</div>
<div class="menu-item__search__collapse__replace">
<input type="text">
<button>替换</button>
</div>
</div>
<div class="menu-item__print" data-menu="print">
<i></i>
</div>
</div>
</div>
<div class="catalog" editor-component="catalog">
<div class="catalog__header">
<span>目录</span>
<div class="catalog__header__close">
<i></i>
</div>
</div>
<div class="catalog__main"></div>
</div>
<div class="editor"></div>
<div class="comment" editor-component="comment"></div>
<div class="footer" editor-component="footer">
<div>
<div class="catalog-mode" title="目录">
<i></i>
</div>
<div class="page-mode">
<i title="页面模式(分页、连页)"></i>
<div class="options">
<ul>
<li data-page-mode="paging" class="active">分页</li>
<li data-page-mode="continuity">连页</li>
</ul>
</div>
</div>
<span>可见页码:<span class="page-no-list">1</span></span>
<span>页面:<span class="page-no">1</span>/<span class="page-size">1</span></span>
<span>字数:<span class="word-count">0</span></span>
<span>行:<span class="row-no">0</span></span>
<span>列:<span class="col-no">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>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

74
package.json Normal file
View File

@ -0,0 +1,74 @@
{
"name": "@hufe921/canvas-editor",
"author": "Hufe",
"license": "MIT",
"version": "0.9.116",
"description": "rich text editor by canvas/svg",
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
},
"files": [
"dist",
"README.md",
"CHANGELOG.md",
"LICENSE",
"package.json"
],
"typings": "./dist/src/editor/index.d.ts",
"main": "./dist/canvas-editor.umd.js",
"module": "./dist/canvas-editor.es.js",
"homepage": "https://github.com/Hufe921/canvas-editor",
"repository": {
"type": "git",
"url": "https://github.com/Hufe921/canvas-editor.git"
},
"keywords": [
"canvas-editor",
"editor",
"wysiwyg",
"emr"
],
"engines": {
"node": ">=16.9.1"
},
"type": "module",
"scripts": {
"dev": "vite",
"lib": "npm run lint && tsc && vite build --mode lib",
"build": "npm run lint && tsc && vite build --mode app",
"serve": "vite preview",
"lint": "eslint .",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"type:check": "tsc --noEmit",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs",
"postinstall": "simple-git-hooks",
"release": "node scripts/release.js"
},
"devDependencies": {
"@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",
"cypress": "13.6.0",
"cypress-file-upload": "^5.0.8",
"eslint": "7.32.0",
"simple-git-hooks": "^2.8.1",
"typescript": "4.9.5",
"vite": "^2.4.2",
"vite-plugin-css-injected-by-js": "^2.1.1",
"vitepress": "1.0.0-beta.6",
"vue": "^3.2.45"
},
"dependencies": {
"prismjs": "^1.27.0"
},
"simple-git-hooks": {
"pre-commit": "npm run lint && npm run type:check",
"commit-msg": "node scripts/verifyCommit.js"
}
}

3742
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

5
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,5 @@
onlyBuiltDependencies:
- cypress
- esbuild
- simple-git-hooks
- vue-demi

29
scripts/release.js Normal file
View File

@ -0,0 +1,29 @@
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
const pkgPath = path.resolve('package.json')
// 校验包合法性
fs.accessSync(path.resolve('dist'), fs.constants.F_OK)
fs.accessSync(path.resolve('dist/canvas-editor.es.js'), fs.constants.F_OK)
fs.accessSync(path.resolve('dist/canvas-editor.umd.js'), fs.constants.F_OK)
// 缓存项目package.json
const sourcePkg = fs.readFileSync(pkgPath, 'utf-8')
// 删除无用属性
const targetPkg = JSON.parse(sourcePkg)
Reflect.deleteProperty(targetPkg, 'dependencies')
Reflect.deleteProperty(targetPkg.scripts, 'postinstall')
fs.writeFileSync(pkgPath, JSON.stringify(targetPkg, null, 2))
// 发布包
try {
execSync('npm publish')
} catch (error) {
throw new Error(error)
} finally {
// 还原
fs.writeFileSync(pkgPath, sourcePkg)
}

19
scripts/verifyCommit.js Normal file
View File

@ -0,0 +1,19 @@
// @ts-check
import { readFileSync } from 'fs'
import path from 'path'
const msgPath = path.resolve('.git/COMMIT_EDITMSG')
const msg = readFileSync(msgPath, 'utf-8').trim()
const commitRE =
/^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release|improve)(\(.+\))?: .{1,50}/
if (!commitRE.test(msg)) {
console.error(
`invalid commit message format.\n\n` +
` Proper commit message format is required for automated changelog generation. Examples:\n\n` +
` feat: add page header\n` +
` fix: IME position error #155\n`
)
process.exit(1)
}

View File

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M2 13h12v1H2v-1zm0-3h12v1H2v-1zm0-3h12v1H2V7zm0-6h12v1H2V1zm0 3h12v1H2V4z" fill="#3D4757" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 209 B

View File

@ -0,0 +1 @@
<svg width="4" height="7" xmlns="http://www.w3.org/2000/svg"><path fill="#6F6F6F" d="M0 3.5L4 0v7z" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 127 B

View File

@ -0,0 +1 @@
<svg width="4" height="7" xmlns="http://www.w3.org/2000/svg"><path fill="#6F6F6F" d="M4 3.5L0 0v7z" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 127 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757"><path d="M8.923 11v1h-2A2 2 0 015 10.55c.328-.15.638-.335.923-.55a1 1 0 001 1h2zm0-6h-2a1 1 0 00-1 1A4.997 4.997 0 005 5.45 2 2 0 016.923 4h2v1z"/><path d="M4 10a2 2 0 100-4 2 2 0 000 4zm0 1a3 3 0 110-6 3 3 0 010 6zm6-9a1 1 0 00-1 1v3a1 1 0 001 1h3a1 1 0 001-1V3a1 1 0 00-1-1h-3zm0-1h3a2 2 0 012 2v3a2 2 0 01-2 2h-3a2 2 0 01-2-2V3a2 2 0 012-2zm0 10a1 1 0 00-1 1v1a1 1 0 001 1h3a1 1 0 001-1v-1a1 1 0 00-1-1h-3zm0-1h3a2 2 0 012 2v1a2 2 0 01-2 2h-3a2 2 0 01-2-2v-1a2 2 0 012-2z"/></g></svg>

After

Width:  |  Height:  |  Size: 568 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M8.131 6.9c2.035 0 2.569-.9 2.569-1.869 0-.968-.64-1.831-2.623-1.831H5.2v3.7h2.931zm.524 5.9c2.045 0 2.545-1.305 2.545-2.3 0-.985-.506-2.4-2.81-2.4H5.2v4.7h3.455zM4 2h4.71c2.367 0 3.19 1.583 3.19 3s-.325 1.852-1.1 2.5c1.2.5 1.569 1.379 1.6 3 .03 1.606-.586 3.5-3.769 3.5H4V2z" fill="#3D4757" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 411 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757"><path d="M13 0c.552 0 1 .48 1 1.071V13.93c0 .59-.448 1.07-1 1.07H3c-.552 0-1-.48-1-1.071V1.07C2 .48 2.448 0 3 0h10zm0 1H3v13h10V1z"/><path d="M5 10v1H4v-1h1zm6 0v1H6v-1h5zM5 7v1H4V7h1zm6 0v1H6V7h5zM5 4v1H4V4h1zm6 0v1H6V4h5z"/></g></svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M2 13h12v1H2v-1zm2-3h8v1H4v-1zM2 7h12v1H2V7zm0-6h12v1H2V1zm2 3h8v1H4V4z" fill="#3D4757" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 207 B

View File

@ -0,0 +1 @@
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 16 16" xml:space="preserve"><style>.st0{fill:#3d4757}</style><g id="_x30_1-文字_x2F_01开始_x2F_任务列表-16px"><path id="合并形状" class="st0" d="M10.1 2H2v11h11V8.7l1-1V13c0 .6-.4 1-1 1H2c-.6 0-1-.4-1-1V2c0-.6.4-1 1-1h9.1l-1 1z"/><path id="路径" class="st0" d="M7.7 8.5l5.7-5.8.9.8-6.1 5.9-.5.5-3.9-3.4.8-.7z"/></g></svg>

After

Width:  |  Height:  |  Size: 428 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M8.9 8.192l4.242 4.243-.707.707L8.192 8.9 3.95 13.142l-.707-.707 4.242-4.243L3.243 3.95l.707-.707 4.242 4.242 4.243-4.242.707.707L8.9 8.192z" fill="#6A6A6A" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -0,0 +1 @@
<svg width="14" height="14" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path stroke="#525C6F" stroke-linejoin="round" d="M4 4.5L1.5 7 4 9.5M10 4.5L12.5 7 10 9.5"></path><rect fill="#525C6F" transform="scale(1 -1) rotate(70 16.711 0)" x="2.671" y="6.53" width="8" height="1" rx=".2"></rect></g></svg>

After

Width:  |  Height:  |  Size: 326 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M7.997 3.429L6.398 8h3.2L7.997 3.429zM8.497 2L12 12h-1L9.949 9h-3.9L5 12H4L7.496 2h1z" fill="#3D4757" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 221 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M9.793 1.5H3a.5.5 0 00-.5.5v12a.5.5 0 00.5.5h9a.5.5 0 00.5-.5V4.207L9.793 1.5z" stroke="#3D4757"/><g fill="#3D4757"><path d="M7 7h1v5H7z"/><path d="M5 7h5v1H5z"/></g><path fill="#3D4757" fill-rule="nonzero" d="M9 2h1v3H9z"/><path fill="#3D4757" fill-rule="nonzero" d="M9 4h3v1H9z"/></g></svg>

After

Width:  |  Height:  |  Size: 399 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M7.5 14.154a5.654 5.654 0 100-11.308 5.654 5.654 0 000 11.308zm0 .846a6.5 6.5 0 110-13 6.5 6.5 0 010 13z" fill-rule="nonzero"/><path d="M8 8h4v1H7V4h1v4z"/></g></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M9 9h1v4H9z"/><path d="M9 9h4v1H9zM7 7H6V3h1z"/><path d="M7 7H3V6h4z"/></g></svg>

After

Width:  |  Height:  |  Size: 191 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M8.213 13H6.8l6.636-6.636-4.243-4.243-7.07 7.071L5.928 13H4.515L1.06 9.546a.5.5 0 010-.707L8.839 1.06a.5.5 0 01.707 0l4.95 4.95a.5.5 0 010 .707L8.213 13z" fill-rule="nonzero"/><path d="M4.536 6.364l4.95 4.95-.707.707-4.95-4.95zM4.521 13h10.03v1H5.496z"/></g></svg>

After

Width:  |  Height:  |  Size: 394 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M13.31 5h-1.92a2.203 2.203 0 00-.39-.034c-.135 0-.27.012-.402.034H10v.18c-.578.256-1 .714-1 1.214v1.928c0 .196.117.43.3.658v1.23a3.543 3.543 0 01-.3-.4V11H8V1h1v4.265C9 4.763 10 4 10.942 4c1.19 0 1.92.422 2.367 1zM2 6c-.03-.498-.175-2 2.5-2C7.11 4 7 5 7 6.902V11H5.984v-.993c.38.662-.115.993-1.484.993C2.708 11 2 9.931 2 9c0-1.428.447-2 2.5-2h1.484c0-1 .031-2-1.484-2-1.533 0-1.577.485-1.577 1H2zm2.5 2C3.601 8 3 7.768 3 9c0 1.31.438 1 1.5 1 .617 0 1.484-.665 1.484-1.847V8H4.5z"/><path d="M13.085 6.316l-2.814 3a1 1 0 101.458 1.368l2.815-3a1 1 0 00-1.459-1.368z" fill-rule="nonzero"/></g></svg>

After

Width:  |  Height:  |  Size: 725 B

Some files were not shown because too many files have changed in this diff Show More