Browse Source

change word

master
godo 7 months ago
parent
commit
7b5995074a
  1. 2
      packages/word/.github/FUNDING.yml
  2. 71
      packages/word/.github/ISSUE_TEMPLATE/bug_report.yml
  3. 5
      packages/word/.github/ISSUE_TEMPLATE/config.yml
  4. 34
      packages/word/.github/ISSUE_TEMPLATE/feature_request.yml
  5. 42
      packages/word/.github/workflows/build.yml
  6. 30
      packages/word/.github/workflows/cypress.yml
  7. 33
      packages/word/.github/workflows/docs.yml
  8. 4
      packages/word/index.html
  9. 6348
      packages/word/package-lock.json
  10. 16
      packages/word/package.json
  11. 111
      packages/word/src/main.ts
  12. 461
      packages/word/src/mock.ts
  13. 44
      packages/word/src/plugins/barcode1d/index.ts
  14. 100
      packages/word/src/plugins/barcode2d/index.ts
  15. 232
      packages/word/src/plugins/docx/exportDocx.ts
  16. 76
      packages/word/src/plugins/docx/importDocx.ts
  17. 11
      packages/word/src/plugins/docx/index.ts
  18. 7
      packages/word/src/plugins/docx/utils.ts
  19. 150
      packages/word/src/plugins/excel/importExcel.ts
  20. 8
      packages/word/src/plugins/excel/index.ts
  21. 1
      packages/word/src/plugins/floatingToolbar/constant/index.ts
  22. 10
      packages/word/src/plugins/floatingToolbar/enum/index.ts
  23. 1
      packages/word/src/plugins/floatingToolbar/icons/bold.svg
  24. 1
      packages/word/src/plugins/floatingToolbar/icons/color.svg
  25. 1
      packages/word/src/plugins/floatingToolbar/icons/highlight.svg
  26. 1
      packages/word/src/plugins/floatingToolbar/icons/italic.svg
  27. 1
      packages/word/src/plugins/floatingToolbar/icons/size-add.svg
  28. 1
      packages/word/src/plugins/floatingToolbar/icons/size-minus.svg
  29. 1
      packages/word/src/plugins/floatingToolbar/icons/strikeout.svg
  30. 1
      packages/word/src/plugins/floatingToolbar/icons/underline.svg
  31. 207
      packages/word/src/plugins/floatingToolbar/index.ts
  32. 8
      packages/word/src/plugins/floatingToolbar/interface/index.ts
  33. 100
      packages/word/src/plugins/floatingToolbar/style/index.scss
  34. 2239
      packages/word/yarn.lock

2
packages/word/.github/FUNDING.yml

@ -1,2 +0,0 @@
github: Hufe921
custom: https://hufe.club/donate.jpg

71
packages/word/.github/ISSUE_TEMPLATE/bug_report.yml

@ -1,71 +0,0 @@
name: "\U0001F41E Bug report"
description: Create a report to help us improve
body:
- type: markdown
attributes:
value: |
**Before You Start...**
This form is only for submitting bug reports. If you have a usage question
or are unsure if this is really a bug, make sure to:
- Read the [docs](https://hufe.club/canvas-editor-docs/)
Also try to search for your issue - it may have already been answered or even fixed in the development branch.
However, if you find that an old, closed issue still persists in the latest version,
you should open a new issue using the form below instead of commenting on the old issue.
- type: input
id: version
attributes:
label: version
validations:
required: true
- type: input
id: reproduction-link
attributes:
label: Link to minimal reproduction
description: |
Provide a link address that can reproduce the bug. The following link is recommended:
https://codesandbox.io/
https://codepen.io/
https://stackblitz.com/
placeholder: Reproduction Link
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: |
What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.
placeholder: Steps to reproduce
validations:
required: true
- type: textarea
id: expected
attributes:
label: What is expected?
validations:
required: true
- type: textarea
id: actually-happening
attributes:
label: What is actually happening?
validations:
required: true
- type: textarea
id: editor-value
attributes:
label: Editor Value
description: Output of `instance.command.getValue()`
render: json
- type: textarea
id: system-info
attributes:
label: System Info
description: Output of `npx envinfo --system --binaries --browsers`
render: shell
placeholder: System, Binaries, Browsers
- type: textarea
id: additional-comments
attributes:
label: Any additional comments?
description: e.g. some background/context of how you ran into this bug.

5
packages/word/.github/ISSUE_TEMPLATE/config.yml

@ -1,5 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: docs
url: https://hufe.club/canvas-editor-docs/
about: Official documents

34
packages/word/.github/ISSUE_TEMPLATE/feature_request.yml

@ -1,34 +0,0 @@
name: "\U0001F680 New feature proposal"
description: Suggest an idea for this project
labels: [":sparkles: feature request"]
body:
- type: markdown
attributes:
value: |
**Before You Start...**
This form is only for submitting feature requests. If you have a usage question
or are unsure if this is really a bug, make sure to:
- Read the [docs](https://hufe.club/canvas-editor-docs/)
Also try to search for your issue - another user may have already requested something similar!
- type: textarea
id: problem-description
attributes:
label: What problem does this feature solve?
description: |
Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature?
placeholder: Problem description
validations:
required: true
- type: textarea
id: proposed-API
attributes:
label: What does the proposed API look like?
description: |
Describe how you propose to solve the problem and provide code samples of how the API would work once implemented. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format your code blocks.
placeholder: Steps to reproduce
validations:
required: true

42
packages/word/.github/workflows/build.yml

@ -1,42 +0,0 @@
name: build
on:
push:
branches: [main]
paths:
- '.github/workflows/build.yml'
- 'src/**'
- 'index.html'
- 'package.json'
- 'vite.config.ts'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm i yarn -g
- run: yarn
- run: yarn run build
- run: mv dist canvas-editor
- name: Copy folder content recursively to remote
uses: appleboy/scp-action@v0.1.7
with:
source: canvas-editor
target: ${{ secrets.PATH }}
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
overwrite: true
- name: Executing remote ssh commands
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
script: sed -i 's/<\/body>/${{ secrets.SCRIPT }}<\/body>/g' ${{ secrets.PATH }}/canvas-editor/index.html

30
packages/word/.github/workflows/cypress.yml

@ -1,30 +0,0 @@
name: cypress
on:
push:
branches: [main]
paths:
- '.github/workflows/*'
- 'cypress/**'
- 'src/**'
- 'index.html'
- 'package.json'
- 'vite.config.ts'
jobs:
cypress:
runs-on: ubuntu-latest
container: cypress/browsers:node16.17.0-chrome106
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm i yarn serve@14.0.1 -g
- run: yarn
- run: yarn run build
- run: mv dist canvas-editor
- run: serve . -l 3000 &
- run: yarn run cypress:run

33
packages/word/.github/workflows/docs.yml

@ -1,33 +0,0 @@
name: docs
on:
push:
branches: [main]
paths:
- '.github/workflows/docs.yml'
- 'docs/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm i yarn -g
- run: yarn
- run: yarn run docs:build
- run: mv ./docs/.vitepress/dist ./docs/.vitepress/canvas-editor-docs
- name: Copy folder content recursively to remote
uses: appleboy/scp-action@v0.1.7
with:
source: docs/.vitepress/canvas-editor-docs
target: ${{ secrets.DOCS_PATH }}
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
overwrite: true
strip_components: 2

4
packages/word/index.html

@ -412,6 +412,10 @@
</div>
</div>
</div>
<div style="display: none;">
<input type="file" name="file-docx" id="file-docx" accept=".docx" />
<input type="file" name="file-excel" id="file-excel" accept=".xlsx" />
</div>
<script type="module" src="/src/main.ts"></script>
</body>

6348
packages/word/package-lock.json

File diff suppressed because it is too large

16
packages/word/package.json

@ -50,6 +50,8 @@
},
"devDependencies": {
"@rollup/plugin-typescript": "^10.0.1",
"@types/color": "^4.2.0",
"@types/lodash": "^4.17.13",
"@types/node": "16.18.96",
"@types/prismjs": "^1.26.0",
"@typescript-eslint/eslint-plugin": "5.62.0",
@ -57,6 +59,7 @@
"cypress": "13.6.0",
"cypress-file-upload": "^5.0.8",
"eslint": "7.32.0",
"sass": "^1.80.7",
"simple-git-hooks": "^2.8.1",
"typescript": "4.9.5",
"vite": "^2.4.2",
@ -65,10 +68,15 @@
"vue": "^3.2.45"
},
"dependencies": {
"@simonwep/pickr": "^1.9.1",
"@zxing/browser": "^0.1.5",
"@zxing/library": "^0.21.3",
"color": "^4.2.3",
"docx": "^9.0.3",
"exceljs": "^4.4.0",
"jsbarcode": "^3.11.6",
"lodash": "^4.17.21",
"mammoth": "^1.8.0",
"prismjs": "^1.27.0"
},
"simple-git-hooks": {
"pre-commit": "npm run lint && npm run type:check",
"commit-msg": "node scripts/verifyCommit.js"
}
}

111
packages/word/src/main.ts

@ -25,7 +25,11 @@ import { Dialog } from './components/dialog/Dialog'
import { formatPrismToken } from './utils/prism'
import { Signature } from './components/signature/Signature'
import { debounce, nextTick, scrollIntoView } from './utils'
import barcode1DPlugin from "./plugins/barcode1d"
import barcode2dPlugin from "./plugins/barcode2d"
import floatingToolbarPlugin from "./plugins/floatingToolbar"
import excelPlugin from "./plugins/excel"
import docxPlugin from './plugins/docx'
window.onload = function () {
const isApple =
typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent)
@ -35,33 +39,25 @@ window.onload = function () {
const instance = new Editor(
container,
{
header: [
{
value: '第一人民医院',
size: 32,
rowFlex: RowFlex.CENTER
},
{
value: '\n门诊病历',
size: 18,
rowFlex: RowFlex.CENTER
},
{
value: '\n',
type: ElementType.SEPARATOR
}
],
header: [],
main: <IElement[]>data,
footer: [
{
value: 'canvas-editor',
value: '',
size: 12
}
]
},
options
)
console.log('实例: ', instance)
//console.log('实例: ', instance)
instance.use(barcode1DPlugin)
instance.use(barcode2dPlugin)
instance.use(floatingToolbarPlugin)
instance.use(excelPlugin)
instance.use(docxPlugin)
const docxFileInput:any = document.querySelector("#file-docx");
const excelFileInput:any = document.querySelector("#file-excel");
// cypress使用
Reflect.set(window, 'editor', instance)
@ -1788,6 +1784,49 @@ window.onload = function () {
// 9. 右键菜单注册
instance.register.contextMenuList([
{
name: "插入条形码",
when: (payload) => {
return !payload.isReadonly && payload.editorTextFocus;
},
callback: (command) => {
const content:any = window.prompt("请输入内容");
command.executeInsertBarcode1D(content, 200, 100);
},
},
{
name: "插入二维码",
when: (payload) => {
return !payload.isReadonly && payload.editorTextFocus;
},
callback: (command) => {
const content:any = window.prompt("请输入内容");
command.executeInsertBarcode2D(content, 200, 200);
},
},
{
name: "导出文档",
when: (payload) => true,
callback: (command) => {
command.executeExportDocx({
fileName: "canvas-editor",
});
},
},
{
name: "导入文档",
when: (payload) => true,
callback: (command) => {
docxFileInput.click();
},
},
{
name: "导入excel",
when: (payload) => true,
callback: (command) => {
excelFileInput.click();
},
},
{
name: '批注',
when: payload => {
@ -1818,7 +1857,7 @@ window.onload = function () {
commentList.push({
id: groupId,
content: value,
userName: 'Hufe',
userName: 'godoos',
rangeText: command.getRangeText(),
createdDate: new Date().toLocaleString()
})
@ -1850,6 +1889,7 @@ window.onload = function () {
})
}
},
{
name: '格式整理',
icon: 'word-tool',
@ -1861,7 +1901,36 @@ window.onload = function () {
}
}
])
excelFileInput.onchange = () => {
const file = excelFileInput?.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const buffer = event?.target?.result;
if (buffer instanceof ArrayBuffer) {
instance.command.executeImportExcel({
arrayBuffer: buffer,
});
}
excelFileInput.value = "";
};
reader.readAsArrayBuffer(file);
};
docxFileInput.onchange = () => {
const file = docxFileInput?.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const buffer = event?.target?.result;
if (buffer instanceof ArrayBuffer) {
instance.command.executeImportDocx({
arrayBuffer: buffer,
});
}
docxFileInput.value = "";
};
reader.readAsArrayBuffer(file);
};
// 10. 快捷键注册
instance.register.shortcutList([
{

461
packages/word/src/mock.ts

File diff suppressed because one or more lines are too long

44
packages/word/src/plugins/barcode1d/index.ts

@ -0,0 +1,44 @@
import { Editor, ElementType } from '../../editor'
import JsBarcode from 'jsbarcode'
function convertSvgElementToBase64(svgElement: HTMLElement | SVGSVGElement) {
return `data:image/svg+xml;base64,${btoa(
decodeURIComponent(svgElement.outerHTML)
)}`
}
declare module '../../editor' {
interface Command {
executeInsertBarcode1D(
content: string,
width: number,
height: number,
options?: JsBarcode.Options
): void
}
}
export default function barcodePlugin(editor: Editor) {
const command = editor.command
// 条形码
command.executeInsertBarcode1D = (
content: string,
width: number,
height: number,
options?: JsBarcode.Options
) => {
const svgElement = document.createElement('svg')
// 解析一维码
JsBarcode(svgElement, content, options)
// 插入
command.executeInsertElementList([
{
type: ElementType.IMAGE,
value: convertSvgElementToBase64(svgElement),
width,
height
}
])
}
}

100
packages/word/src/plugins/barcode2d/index.ts

@ -0,0 +1,100 @@
import { Editor, ElementType } from '../../editor'
import { BrowserQRCodeReader, BrowserQRCodeSvgWriter } from '@zxing/browser'
import { EncodeHintType } from '@zxing/library'
import { Dialog } from '../../components/dialog/Dialog'
function convertSvgElementToBase64(svgElement: HTMLElement | SVGSVGElement) {
return `data:image/svg+xml;base64,${btoa(
decodeURIComponent(svgElement.outerHTML)
)}`
}
declare module '../../editor' {
interface Command {
executeInsertBarcode2D(
content: string,
width: number,
height: number,
hints?: Map<EncodeHintType, any>
): void
}
}
export interface IBarcode2DOption {
isRegisterDetectorContextMenu?: boolean
}
export default function barcode2DPlugin(
editor: Editor,
options: IBarcode2DOption = {}
) {
const command = editor.command
// 插入二维码
command.executeInsertBarcode2D = (
content: string,
width: number,
height: number,
hints?: Map<EncodeHintType, any>
) => {
const codeWriter = new BrowserQRCodeSvgWriter()
// 配置默认边距
if (!hints) {
hints = new Map<EncodeHintType, any>()
}
if (!hints.has(EncodeHintType.MARGIN)) {
hints.set(EncodeHintType.MARGIN, 0)
}
// 生成svg元素并增加命名空间
const svgElement = codeWriter.write(content, width, height, hints)
svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
// 插入
command.executeInsertElementList([
{
type: ElementType.IMAGE,
value: convertSvgElementToBase64(svgElement),
width,
height
}
])
}
// 识别二维码
//if (options.isRegisterDetectorContextMenu) {
editor.register.contextMenuList([
{
name: '识别二维码链接',
icon: 'qrcode',
when: payload => {
return (
payload.startElement === payload.endElement &&
payload.startElement?.type === ElementType.IMAGE
)
},
callback: async (_, context) => {
const value = context.startElement?.value
if (!value) return
const codeReader = new BrowserQRCodeReader()
const result = await codeReader.decodeFromImageUrl(value)
const text = result.getText()
// 如果是超链接则跳转
// if (text && /^(http|https):.*/.test(text)) {
// window.open(text, '_blank')
// }
new Dialog({
title: '识别内容',
data: [
{
type: 'text',
label: '识别内容',
name: 'name',
placeholder: '请输入文本',
value: text
},
]
})
}
}
])
//}
}

232
packages/word/src/plugins/docx/exportDocx.ts

@ -0,0 +1,232 @@
import Color from 'color'
import {
IElement,
ElementType,
TitleLevel,
ListStyle,
Command
} from '../../editor'
import {
Document,
Packer,
Paragraph,
Header,
Footer,
Table,
HeadingLevel,
ParagraphChild,
TextRun,
Tab,
ExternalHyperlink,
ImageRun,
WidthType,
TableRow,
TableCell,
MathRun
} from 'docx'
import { saveAs } from './utils'
// 标题映射
const titleLevelToHeadingLevel = {
[TitleLevel.FIRST]: HeadingLevel.HEADING_1,
[TitleLevel.SECOND]: HeadingLevel.HEADING_2,
[TitleLevel.THIRD]: HeadingLevel.HEADING_3,
[TitleLevel.FOURTH]: HeadingLevel.HEADING_4,
[TitleLevel.FIFTH]: HeadingLevel.HEADING_5,
[TitleLevel.SIXTH]: HeadingLevel.HEADING_6
}
function convertElementToParagraphChild(element: IElement): ParagraphChild {
if (element.type === ElementType.IMAGE) {
return new ImageRun({
data: element.value,
transformation: {
width: element.width!,
height: element.height!
},
SvgMediaOptions:{
width: element.width!,
height: element.height!
}
})
}
if (element.type === ElementType.HYPERLINK) {
return new ExternalHyperlink({
children: [
new TextRun({
text: element.valueList?.map(child => child.value).join(''),
style: 'Hyperlink'
})
],
link: element.url!
})
}
if (element.type === ElementType.TAB) {
return new TextRun({
children: [new Tab()]
})
}
if (element.type === ElementType.LATEX) {
return new MathRun(element.value)
}
return new TextRun({
font: element.font,
text: element.value,
bold: element.bold,
size: `${(element.size || 16) / 0.75}pt`,
color: Color(element.color).hex() || '#000000',
italics: element.italic,
strike: element.strikeout,
highlight: element.highlight ? Color(element.highlight).hex() : undefined,
superScript: element.type === ElementType.SUPERSCRIPT,
subScript: element.type === ElementType.SUBSCRIPT,
underline: element.underline ? {} : undefined
})
}
type DocxChildren = (Paragraph | Table)[]
function convertElementListToDocxChildren(
elementList: IElement[]
): DocxChildren {
const children: DocxChildren = []
let paragraphChild: ParagraphChild[] = []
function appendParagraph() {
if (paragraphChild.length) {
children.push(
new Paragraph({
children: paragraphChild
})
)
paragraphChild = []
}
}
for (let e = 0; e < elementList.length; e++) {
const element = elementList[e]
if (element.type === ElementType.TITLE) {
appendParagraph()
children.push(
new Paragraph({
heading: titleLevelToHeadingLevel[element.level!],
children:
element.valueList?.map(child =>
convertElementToParagraphChild(child)
) || []
})
)
} else if (element.type === ElementType.LIST) {
appendParagraph()
// 拆分列表
const listChildren =
element.valueList
?.map(item => item.value)
.join('')
.split('\n')
.map(
(text, index) =>
new Paragraph({
children: [
new TextRun({
text: `${
!element.listStyle ||
element.listStyle === ListStyle.DECIMAL
? `${index + 1}. `
: ``
}${text}`
})
]
})
) || []
children.push(...listChildren)
} else if (element.type === ElementType.TABLE) {
appendParagraph()
const { trList } = element
const tableRowList: TableRow[] = []
for (let r = 0; r < trList!.length; r++) {
const tdList = trList![r].tdList
const tableCellList: TableCell[] = []
for (let c = 0; c < tdList.length; c++) {
const td = tdList[c]
tableCellList.push(
new TableCell({
columnSpan: td.colspan,
rowSpan: td.rowspan,
children: convertElementListToDocxChildren(td.value) || []
})
)
}
tableRowList.push(
new TableRow({
children: tableCellList
})
)
}
children.push(
new Table({
rows: tableRowList,
width: {
size: '100%',
type: WidthType.PERCENTAGE
}
})
)
} else if (element.type === ElementType.DATE) {
paragraphChild.push(
...(element.valueList?.map(child =>
convertElementToParagraphChild(child)
) || [])
)
} else {
if (/^\n/.test(element.value)) {
appendParagraph()
element.value = element.value.replace(/^\n/, '')
}
paragraphChild.push(convertElementToParagraphChild(element))
}
}
appendParagraph()
return children
}
export interface IExportDocxOption {
fileName: string
}
declare module '../../editor' {
interface Command {
executeExportDocx(options: IExportDocxOption): void
}
}
export default function (command: Command) {
return function (options: IExportDocxOption) {
const { fileName } = options
const {
data: { header, main, footer }
} = command.getValue()
const doc = new Document({
sections: [
{
headers: {
default: new Header({
children: convertElementListToDocxChildren(header || [])
})
},
footers: {
default: new Footer({
children: convertElementListToDocxChildren(footer || [])
})
},
children: convertElementListToDocxChildren(main || [])
}
]
})
Packer.toBlob(doc).then(blob => {
saveAs(blob, `${fileName}.docx`)
})
}
}

76
packages/word/src/plugins/docx/importDocx.ts

@ -0,0 +1,76 @@
import { Command } from '../../editor'
import mammoth from 'mammoth'
import _ from 'lodash'
declare module '../../editor' {
interface Command {
executeImportDocx(options: IImportDocxOption): void
}
}
export interface IImportDocxOption {
arrayBuffer: ArrayBuffer
}
function transformElement(element:any) {
if (element.children) {
var children = _.map(element.children, transformElement);
element = {...element, children: children};
}
if (element.type === "paragraph") {
element = transformParagraph(element);
}
return element;
}
function transformParagraph(element:any) {
if (element.alignment === "center" && !element.styleId) {
return {...element, styleId: "Heading2"};
} else {
return element;
}
}
export default function (command: Command) {
return async function (options: IImportDocxOption) {
const { arrayBuffer } = options
const result = await mammoth.convertToHtml({
arrayBuffer,
}, {
transformDocument: transformElement,
styleMap: [
"p[style-name='Normal'] => p",
"p[style-name='Heading 1'] => h1:fresh",
"p[style-name='Heading 2'] => h2:fresh",
"p[style-name='Heading 3'] => h3:fresh",
"p[style-name='Heading 4'] => h4:fresh",
"p[style-name='Heading 5'] => h5:fresh",
"p[style-name='Heading 6'] => h6:fresh",
"p[style-name='Title'] => h1:fresh",
"p[style-name='Subtitle'] => h2:fresh",
"p[style-name='Quote'] => blockquote",
"p[style-name='List Paragraph'] => li",
"r[style-name='Emphasis'] => em",
"r[style-name='Strong'] => strong",
"r[style-name='Underline'] => u",
"r[style-name='Strikethrough'] => del",
"r[style-name='Comment Reference'] => sup",
"b => strong",
"i => em",
"u => u",
"strike => del",
"comment-reference => sup"
],
convertImage: mammoth.images.imgElement(function (image) {
return image.read("base64").then(function (imageBuffer) {
return {
src: "data:" + image.contentType + ";base64," + imageBuffer
};
});
})
})
command.executeSetHTML({
main: result.value
})
}
}

11
packages/word/src/plugins/docx/index.ts

@ -0,0 +1,11 @@
import Editor from '../../editor'
import exportDocx from './exportDocx'
import importDocx from './importDocx'
export default function docxPlugin(editor: Editor) {
const command = editor.command
// 导入文档
command.executeImportDocx = importDocx(command)
// 导出文档
command.executeExportDocx = exportDocx(command)
}

7
packages/word/src/plugins/docx/utils.ts

@ -0,0 +1,7 @@
export function saveAs(blob: Blob, name: string) {
const a = document.createElement('a')
a.href = window.URL.createObjectURL(blob)
a.download = name
a.click()
window.URL.revokeObjectURL(a.href)
}

150
packages/word/src/plugins/excel/importExcel.ts

@ -0,0 +1,150 @@
import {
Command,
ElementType,
IElement,
VerticalAlign
} from '../../editor'
import { IColgroup } from '../../editor/interface/table/Colgroup'
import { ITd } from '../../editor/interface/table/Td'
import ExcelJS, { CellRichTextValue } from 'exceljs'
declare module '../../editor' {
interface Command {
executeImportExcel(options: IImportExcelOption): void
}
}
declare module 'exceljs' {
interface WorksheetModel {
rows: Row[]
}
interface Row {
cells: Cell[]
}
}
export interface IImportExcelOption {
arrayBuffer: ArrayBuffer
}
const ANCHOR_COL_WIDTH = 6 // excel是以字符串”0“,字体大小11作为宽度基础设置
const DEFAULT_COL_WIDTH_COUNT = 8.6 // 默认8.6个字符宽度
const DEFAULT_COL_HEIGHT = 40 // 默认40px
// 垂直布局映射
const EXCEL_EDITOR_VERTICAL_MAPPING = {
top: VerticalAlign.TOP,
middle: VerticalAlign.MIDDLE,
bottom: VerticalAlign.BOTTOM,
distributed: VerticalAlign.TOP,
justify: VerticalAlign.TOP
}
export default function (command: Command) {
return async function (options: IImportExcelOption) {
const workbook = new ExcelJS.Workbook()
await workbook.xlsx.load(options.arrayBuffer)
const elementList: IElement[] = []
// 循环每个sheet
workbook.eachSheet(function (worksheet) {
// 列宽
const colgroup: IColgroup[] = worksheet.columns.map(col => ({
width: (col.width || DEFAULT_COL_WIDTH_COUNT) * ANCHOR_COL_WIDTH
}))
const tableElement: IElement = {
type: ElementType.TABLE,
value: '',
colgroup,
trList: []
}
// 按列处理
worksheet.eachRow((row, rowNumber) => {
const rowIndex = rowNumber - 1
const model = row.model
const tdList: ITd[] = []
if (!Array.isArray(row.values)) return
// 行处理
for (let v = 1; v < row.values.length; v++) {
const cell = model?.cells?.[v - 1]
if (cell?.master) continue
const cellStyle = cell?.style
const cellValue = cell?.value
// 垂直布局
const verticalAlign =
EXCEL_EDITOR_VERTICAL_MAPPING[
cellStyle?.alignment?.vertical || 'top'
]
// 数据样式
const value: IElement[] = []
const richtext = (<CellRichTextValue>cellValue)?.richText
if (richtext) {
richtext.forEach(item => {
value.push({
value: item.text,
bold: item.font?.bold,
italic: item.font?.italic,
size: item.font?.size,
strikeout: item.font?.strike,
underline: !!item.font?.underline
})
})
} else {
value.push({
value: cellValue?.toString() || '',
bold: cellStyle?.font?.bold,
italic: cellStyle?.font?.italic,
size: cellStyle?.font?.size,
strikeout: cellStyle?.font?.strike
})
}
// 合并单元格信息
let colspan = 1
let rowspan = 1
const rowList = worksheet.model.rows
for (let r = rowIndex; r < rowList.length; r++) {
const nextCells = rowList[r].cells
for (let c = 0; c < nextCells.length; c++) {
// 忽略当前元素
if (r === rowIndex && c === v - 1) continue
const nextCell = nextCells[c]
// 附属单元格
if (nextCell.master === cell?.address) {
if (r === rowIndex) {
// 相同行则为跨列
colspan += 1
} else if (r > rowIndex && c === v - 1) {
// 下一行则为跨行
rowspan += 1
}
}
}
}
tdList.push({
colspan,
rowspan,
verticalAlign,
value
})
}
if (tdList.length) {
// 行高
const height = model?.height || DEFAULT_COL_HEIGHT
tableElement.trList!.push({
height,
minHeight: height,
tdList
})
}
})
if (tableElement.trList?.length) {
elementList.push(tableElement)
}
})
// 设置值
if (elementList.length) {
command.executeSetValue({
main: elementList
})
}
}
}

8
packages/word/src/plugins/excel/index.ts

@ -0,0 +1,8 @@
import Editor from '../../editor'
import importExcel from './importExcel'
export default function excelPlugin(editor: Editor) {
const command = editor.command
command.executeImportExcel = importExcel(command)
}

1
packages/word/src/plugins/floatingToolbar/constant/index.ts

@ -0,0 +1 @@
export const PLUGIN_PREFIX = 'ce'

10
packages/word/src/plugins/floatingToolbar/enum/index.ts

@ -0,0 +1,10 @@
export enum ToolbarType {
SIZE_ADD = 'size-add',
SIZE_MINUS = 'size-minus',
BOLD = 'bold',
ITALIC = 'italic',
UNDERLINE = 'underline',
STRIKEOUT = 'strikeout',
COLOR = 'color',
HIGHLIGHT = 'highlight'
}

1
packages/word/src/plugins/floatingToolbar/icons/bold.svg

@ -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

1
packages/word/src/plugins/floatingToolbar/icons/color.svg

@ -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

1
packages/word/src/plugins/floatingToolbar/icons/highlight.svg

@ -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

1
packages/word/src/plugins/floatingToolbar/icons/italic.svg

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M10.017 3L8.08 13H9v1H6v-1h1.182L9 3H8V2h3v1h-.983z" fill="#3D4757"/></svg>

After

Width:  |  Height:  |  Size: 167 B

1
packages/word/src/plugins/floatingToolbar/icons/size-add.svg

@ -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="M6.215 3.29H7.64L11.855 14H10.52l-1.14-3H4.46l-1.14 3H2L6.215 3.29zM4.85 9.965h4.14L6.965 4.61h-.06L4.85 9.965z"/><path d="M12 4V2h1v2h2v1h-2v2h-1V5h-2V4h2z" fill-rule="nonzero"/></g></svg>

After

Width:  |  Height:  |  Size: 319 B

1
packages/word/src/plugins/floatingToolbar/icons/size-minus.svg

@ -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 fill-rule="nonzero" d="M11 4h4v1h-4z"/><path d="M6.215 3.29H7.64L11.855 14H10.52l-1.14-3H4.46l-1.14 3H2L6.215 3.29zM4.85 9.965h4.14L6.965 4.61h-.06L4.85 9.965z"/></g></svg>

After

Width:  |  Height:  |  Size: 299 B

1
packages/word/src/plugins/floatingToolbar/icons/strikeout.svg

@ -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="M10.42 7.903H6.692a9.182 9.182 0 01-.41-.172 5.54 5.54 0 01-.814-.447 2.955 2.955 0 01-.655-.595 2.728 2.728 0 01-.44-.777 2.877 2.877 0 01-.162-1.006c0-.472.094-.888.282-1.25.188-.36.453-.663.793-.907s.747-.43 1.22-.558A5.97 5.97 0 018.063 2c.504 0 .95.049 1.337.147.387.097.725.23 1.013.398.287.169.53.365.73.59a3.337 3.337 0 01.772 1.486c.03.13.054.255.073.379h-1.276a2.393 2.393 0 00-.22-.615 2.315 2.315 0 00-.59-.724 2.467 2.467 0 00-.834-.44 3.376 3.376 0 00-1.005-.146 4.69 4.69 0 00-.958.097 2.77 2.77 0 00-.839.314 1.765 1.765 0 00-.597.566c-.152.233-.229.518-.229.854 0 .348.086.642.258.884.171.241.401.449.689.622.287.174.615.323.983.448s.749.247 1.142.367c.31.097.62.196.934.297a8.439 8.439 0 01.973.38zm1.376 1c.175.217.315.466.418.746.105.285.158.612.158.98 0 .554-.104 1.041-.312 1.462-.207.42-.496.772-.867 1.054-.37.282-.81.495-1.32.64A6.12 6.12 0 018.205 14c-.543 0-1.071-.09-1.586-.273a4.44 4.44 0 01-1.374-.773 3.873 3.873 0 01-.97-1.217 3.695 3.695 0 01-.395-1.612h1.27c.028.407.122.78.282 1.12a2.835 2.835 0 001.581 1.465c.363.138.76.207 1.192.207.387 0 .758-.042 1.112-.126a2.85 2.85 0 00.938-.399 2.01 2.01 0 00.647-.708c.16-.29.241-.642.241-1.054 0-.337-.087-.623-.261-.86a2.333 2.333 0 00-.69-.61 4.651 4.651 0 00-.495-.257h2.099z"/><path d="M3 7h10v1H3z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
packages/word/src/plugins/floatingToolbar/icons/underline.svg

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M5 2v6a3 3 0 106 0V2h1v6a4 4 0 11-8 0V2h1zM4 13h8v1H4z" fill="#3D4757"/></svg>

After

Width:  |  Height:  |  Size: 170 B

207
packages/word/src/plugins/floatingToolbar/index.ts

@ -0,0 +1,207 @@
import '@simonwep/pickr/dist/themes/nano.min.css'
import Pickr from '@simonwep/pickr'
import Editor from '../../editor'
import './style/index.scss'
import { ToolbarType } from './enum'
import { IToolbarRegister } from './interface'
import { PLUGIN_PREFIX } from './constant'
function createPickerToolbar(
container: HTMLDivElement,
toolbarType: ToolbarType,
changed: (color: string) => void
) {
const toolbarItem = document.createElement('div')
toolbarItem.classList.add(`${PLUGIN_PREFIX}-picker`)
toolbarItem.classList.add(`${PLUGIN_PREFIX}-${toolbarType}`)
// 颜色选择容器
const pickerContainer = document.createElement('div')
pickerContainer.classList.add(`${PLUGIN_PREFIX}-picker-container`)
const pickerDom = document.createElement('div')
pickerContainer.append(pickerDom)
toolbarItem.append(pickerContainer)
container.append(toolbarItem)
// 实例化颜色选择器
const currentColor = '#000000'
const pickr = new Pickr({
el: pickerDom,
theme: 'nano',
useAsButton: true,
inline: true,
default: currentColor,
i18n: {
'btn:save': '✓'
},
components: {
preview: true,
opacity: true,
hue: true,
interaction: {
input: true,
save: true
}
}
})
const icon = document.createElement('i')
toolbarItem.append(icon)
const colorBar = document.createElement('span')
colorBar.style.backgroundColor = currentColor
toolbarItem.append(colorBar)
toolbarItem.onclick = evt => {
const target = evt.target as HTMLElement
if (pickerContainer !== target && !pickerContainer.contains(target)) {
pickr.show()
}
}
pickr.on('save', (cb: any) => {
pickr.hide()
const color = cb.toHEXA().toString()
colorBar.style.backgroundColor = color
changed(color)
})
}
// 工具栏列表
const toolbarRegisterList: IToolbarRegister[] = [
{
key: ToolbarType.SIZE_ADD,
callback(editor) {
editor.command.executeSizeAdd()
}
},
{
key: ToolbarType.SIZE_MINUS,
callback(editor) {
editor.command.executeSizeMinus()
}
},
{
isDivider: true
},
{
key: ToolbarType.BOLD,
callback(editor) {
editor.command.executeBold()
}
},
{
key: ToolbarType.ITALIC,
callback(editor) {
editor.command.executeItalic()
}
},
{
key: ToolbarType.UNDERLINE,
callback(editor) {
editor.command.executeUnderline()
}
},
{
key: ToolbarType.STRIKEOUT,
callback(editor) {
editor.command.executeStrikeout()
}
},
{
isDivider: true
},
{
render(container, editor) {
createPickerToolbar(container, ToolbarType.COLOR, color => {
editor.command.executeColor(color)
})
}
},
{
render(container, editor) {
createPickerToolbar(container, ToolbarType.HIGHLIGHT, color => {
editor.command.executeHighlight(color)
})
}
}
]
function createToolbar(editor: Editor): HTMLDivElement {
const toolbarContainer = document.createElement('div')
toolbarContainer.classList.add(`${PLUGIN_PREFIX}-floating-toolbar`)
for (const toolbar of toolbarRegisterList) {
if (toolbar.render) {
toolbar.render(toolbarContainer, editor)
} else if (toolbar.isDivider) {
const divider = document.createElement('div')
divider.classList.add(`${PLUGIN_PREFIX}-divider`)
toolbarContainer.append(divider)
} else {
const { key, callback } = toolbar
const toolbarItem = document.createElement('div')
toolbarItem.classList.add(`${PLUGIN_PREFIX}-${key}`)
const icon = document.createElement('i')
toolbarItem.append(icon)
toolbarItem.onclick = () => {
callback?.(editor)
}
toolbarContainer.append(toolbarItem)
}
}
return toolbarContainer
}
function toggleToolbarVisible(toolbar: HTMLDivElement, visible: boolean) {
visible ? toolbar.classList.remove('hide') : toolbar.classList.add('hide')
}
function toggleToolbarItemActive(toolbarItem: HTMLDivElement, active: boolean) {
active
? toolbarItem.classList.add('active')
: toolbarItem.classList.remove('active')
}
export default function floatingToolbarPlugin(editor: Editor) {
// 创建工具栏
const toolbarContainer = createToolbar(editor)
const editorContainer = editor.command.getContainer()
editorContainer.append(toolbarContainer)
// 监听选区样式变化
editor.eventBus.on('rangeStyleChange', rangeStyle => {
if (rangeStyle.type === null) {
toggleToolbarVisible(toolbarContainer, false)
return
}
const context = editor.command.getRangeContext()
if (!context || context.isCollapsed || !context.rangeRects[0]) {
toggleToolbarVisible(toolbarContainer, false)
return
}
// 定位
const position = context.rangeRects[0]
toolbarContainer.style.left = `${position.x}px`
toolbarContainer.style.top = `${position.y + position.height}px`
// 样式回显
const boldDom = toolbarContainer.querySelector<HTMLDivElement>(
`.${PLUGIN_PREFIX}-bold`
)
if (boldDom) {
toggleToolbarItemActive(boldDom, rangeStyle.bold)
}
const italicDom = toolbarContainer.querySelector<HTMLDivElement>(
`.${PLUGIN_PREFIX}-italic`
)
if (italicDom) {
toggleToolbarItemActive(italicDom, rangeStyle.italic)
}
const underlineDom = toolbarContainer.querySelector<HTMLDivElement>(
`.${PLUGIN_PREFIX}-underline`
)
if (underlineDom) {
toggleToolbarItemActive(underlineDom, rangeStyle.underline)
}
const strikeoutDom = toolbarContainer.querySelector<HTMLDivElement>(
`.${PLUGIN_PREFIX}-strikeout`
)
if (strikeoutDom) {
toggleToolbarItemActive(strikeoutDom, rangeStyle.strikeout)
}
toggleToolbarVisible(toolbarContainer, true)
})
}

8
packages/word/src/plugins/floatingToolbar/interface/index.ts

@ -0,0 +1,8 @@
import Editor from '@hufe921/canvas-editor'
export interface IToolbarRegister {
key?: string
isDivider?: boolean
render?: (container: HTMLDivElement, editor: Editor) => void
callback?: (editor: Editor) => void
}

100
packages/word/src/plugins/floatingToolbar/style/index.scss

@ -0,0 +1,100 @@
.ce-floating-toolbar {
display: flex;
position: absolute;
z-index: 9;
align-items: center;
height: 36px;
background: #ffffff;
transition: all 0.3s ease-in-out;
box-shadow: 0 2px 8px 0 rgba(68, 73, 77, 0.16);
border-radius: 4px;
padding: 0 8px;
&.hide {
display: none;
}
>div {
min-width: 20px;
margin: 0 3px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
&:hover,
&.active {
background-color: rgba(25, 55, 88, .07);
}
i {
width: 16px;
height: 16px;
display: inline-block;
}
&.ce-divider {
padding: 0;
min-width: unset;
width: 1px;
height: 16px;
margin: 0 3px;
display: inline-block;
background-color: #cfd2d8;
}
&.ce-size-add i {
background: url(../icons/size-add.svg);
}
&.ce-size-minus i {
background: url(../icons/size-minus.svg);
}
&.ce-bold i {
background: url(../icons/bold.svg);
}
&.ce-italic i {
background: url(../icons/italic.svg);
}
&.ce-underline i {
background: url(../icons/underline.svg);
}
&.ce-strikeout i {
background: url(../icons/strikeout.svg);
}
&.ce-picker {
height: 20px;
display: flex;
flex-direction: column;
>span {
width: 16px;
height: 3px;
display: inline-block;
border: 1px solid #e2e6ed;
}
.ce-picker-container {
position: relative;
.pcr-app {
position: absolute !important;
}
}
}
&.ce-color i {
background: url(../icons/color.svg);
}
&.ce-highlight i {
background: url(../icons/highlight.svg);
}
}
}

2239
packages/word/yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save