├── .editorconfig ├── .eslintrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ ├── build.yml │ ├── cypress.yml │ └── docs.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cypress.config.ts ├── cypress ├── e2e │ ├── control │ │ ├── checkbox.cy.ts │ │ ├── select.cy.ts │ │ └── text.cy.ts │ ├── editor.cy.ts │ └── menus │ │ ├── block.cy.ts │ │ ├── checkbox.cy.ts │ │ ├── codeblock.cy.ts │ │ ├── date.cy.ts │ │ ├── format.cy.ts │ │ ├── hyperlink.cy.ts │ │ ├── image.cy.ts │ │ ├── latex.cy.ts │ │ ├── pagebreak.cy.ts │ │ ├── painter.cy.ts │ │ ├── print.cy.ts │ │ ├── row.cy.ts │ │ ├── search.cy.ts │ │ ├── separator.cy.ts │ │ ├── table.cy.ts │ │ ├── text.cy.ts │ │ ├── title.cy.ts │ │ ├── undoRedo.cy.ts │ │ └── watermark.cy.ts ├── fixtures │ ├── example.json │ └── test.png ├── global.d.ts ├── support │ ├── commands.ts │ └── e2e.ts └── tsconfig.json ├── docs ├── .vitepress │ └── config.ts ├── en │ ├── guide │ │ ├── api-common.md │ │ ├── api-instance.md │ │ ├── command-execute.md │ │ ├── command-get.md │ │ ├── contextmenu-custom.md │ │ ├── contextmenu-internal.md │ │ ├── eventbus.md │ │ ├── i18n.md │ │ ├── listener.md │ │ ├── option.md │ │ ├── override.md │ │ ├── plugin-custom.md │ │ ├── plugin-internal.md │ │ ├── schema.md │ │ ├── shortcut-custom.md │ │ ├── shortcut-internal.md │ │ └── start.md │ └── index.md ├── guide │ ├── api-common.md │ ├── api-instance.md │ ├── command-execute.md │ ├── command-get.md │ ├── contextmenu-custom.md │ ├── contextmenu-internal.md │ ├── eventbus.md │ ├── i18n.md │ ├── listener.md │ ├── option.md │ ├── override.md │ ├── plugin-custom.md │ ├── plugin-internal.md │ ├── schema.md │ ├── shortcut-custom.md │ ├── shortcut-internal.md │ └── start.md ├── index.md └── public │ └── favicon.png ├── favicon.png ├── index.html ├── package.json ├── scripts ├── release.js └── verifyCommit.js ├── src ├── assets │ ├── images │ │ ├── alignment.svg │ │ ├── arrow-left.svg │ │ ├── arrow-right.svg │ │ ├── block.svg │ │ ├── bold.svg │ │ ├── catalog.svg │ │ ├── center.svg │ │ ├── checkbox.svg │ │ ├── close.svg │ │ ├── codeblock.svg │ │ ├── color.svg │ │ ├── control.svg │ │ ├── date.svg │ │ ├── exit-fullscreen.svg │ │ ├── format.svg │ │ ├── highlight.svg │ │ ├── hyperlink.svg │ │ ├── image.svg │ │ ├── italic.svg │ │ ├── justify.svg │ │ ├── latex.svg │ │ ├── left.svg │ │ ├── line-dash-dot-dot.svg │ │ ├── line-dash-dot.svg │ │ ├── line-dash-large-gap.svg │ │ ├── line-dash-small-gap.svg │ │ ├── line-dot.svg │ │ ├── line-double.svg │ │ ├── line-single.svg │ │ ├── line-wavy.svg │ │ ├── list.svg │ │ ├── option.svg │ │ ├── page-break.svg │ │ ├── page-mode.svg │ │ ├── page-scale-add.svg │ │ ├── page-scale-minus.svg │ │ ├── painter.svg │ │ ├── paper-direction.svg │ │ ├── paper-margin.svg │ │ ├── paper-size.svg │ │ ├── print.svg │ │ ├── radio.svg │ │ ├── redo.svg │ │ ├── request-fullscreen.svg │ │ ├── right.svg │ │ ├── row-margin.svg │ │ ├── search.svg │ │ ├── separator.svg │ │ ├── signature-undo.svg │ │ ├── signature.svg │ │ ├── size-add.svg │ │ ├── size-minus.svg │ │ ├── strikeout.svg │ │ ├── subscript.svg │ │ ├── superscript.svg │ │ ├── table.svg │ │ ├── title.svg │ │ ├── trash.svg │ │ ├── underline.svg │ │ ├── undo.svg │ │ ├── watermark.svg │ │ └── word-tool.svg │ └── snapshots │ │ ├── main_v0.2.1.png │ │ ├── main_v0.2.2.png │ │ ├── main_v0.3.0.png │ │ ├── main_v0.3.1.png │ │ ├── main_v0.5.0.png │ │ ├── main_v0.5.1.png │ │ ├── main_v0.6.0.png │ │ ├── main_v0.6.1.png │ │ ├── main_v0.7.0.png │ │ ├── main_v0.7.1.png │ │ ├── main_v0.7.2.png │ │ ├── main_v0.7.3.png │ │ ├── main_v0.7.4.png │ │ ├── main_v0.7.6.png │ │ ├── main_v0.7.7.png │ │ ├── main_v0.8.0.png │ │ ├── main_v0.8.5.png │ │ ├── main_v0.8.6.png │ │ ├── main_v0.8.7.png │ │ ├── main_v0.8.8.png │ │ ├── main_v0.9.0.png │ │ ├── main_v0.9.1.png │ │ ├── main_v0.9.2.png │ │ ├── main_v0.9.23.png │ │ ├── main_v0.9.28.png │ │ ├── main_v0.9.29.png │ │ ├── main_v0.9.3.png │ │ ├── main_v0.9.30.png │ │ ├── main_v0.9.32.png │ │ ├── main_v0.9.35.png │ │ ├── main_v0.9.4.png │ │ ├── main_v0.9.5.png │ │ ├── main_v0.9.6.png │ │ └── main_v0.9.8.png ├── components │ ├── dialog │ │ ├── Dialog.ts │ │ └── dialog.css │ └── signature │ │ ├── Signature.ts │ │ └── signature.css ├── editor │ ├── assets │ │ ├── css │ │ │ ├── block │ │ │ │ └── block.css │ │ │ ├── contextmenu │ │ │ │ └── contextmenu.css │ │ │ ├── control │ │ │ │ └── select.css │ │ │ ├── date │ │ │ │ └── datePicker.css │ │ │ ├── hyperlink │ │ │ │ └── hyperlink.css │ │ │ ├── index.css │ │ │ ├── previewer │ │ │ │ └── previewer.css │ │ │ ├── resizer │ │ │ │ └── resizer.css │ │ │ ├── table │ │ │ │ └── table.css │ │ │ └── zone │ │ │ │ └── zone.css │ │ └── images │ │ │ ├── close.svg │ │ │ ├── delete-col.svg │ │ │ ├── delete-row-col.svg │ │ │ ├── delete-row.svg │ │ │ ├── delete-table.svg │ │ │ ├── image-change.svg │ │ │ ├── image-download.svg │ │ │ ├── image.svg │ │ │ ├── insert-bottom-row.svg │ │ │ ├── insert-left-col.svg │ │ │ ├── insert-right-col.svg │ │ │ ├── insert-row-col.svg │ │ │ ├── insert-top-row.svg │ │ │ ├── merge-cancel-cell.svg │ │ │ ├── merge-cell.svg │ │ │ ├── original-size.svg │ │ │ ├── print.svg │ │ │ ├── rotate.svg │ │ │ ├── submenu-dropdown.svg │ │ │ ├── table-border-all.svg │ │ │ ├── table-border-dash.svg │ │ │ ├── table-border-empty.svg │ │ │ ├── table-border-external.svg │ │ │ ├── table-border-internal.svg │ │ │ ├── table-border-td-back.svg │ │ │ ├── table-border-td-bottom.svg │ │ │ ├── table-border-td-forward.svg │ │ │ ├── table-border-td-left.svg │ │ │ ├── table-border-td-right.svg │ │ │ ├── table-border-td-top.svg │ │ │ ├── table-border-td.svg │ │ │ ├── vertical-align-bottom.svg │ │ │ ├── vertical-align-middle.svg │ │ │ ├── vertical-align-top.svg │ │ │ ├── vertical-align.svg │ │ │ ├── zoom-in.svg │ │ │ └── zoom-out.svg │ ├── core │ │ ├── actuator │ │ │ ├── Actuator.ts │ │ │ └── handlers │ │ │ │ └── positionContextChange.ts │ │ ├── command │ │ │ ├── Command.ts │ │ │ └── CommandAdapt.ts │ │ ├── contextmenu │ │ │ ├── ContextMenu.ts │ │ │ └── menus │ │ │ │ ├── controlMenus.ts │ │ │ │ ├── globalMenus.ts │ │ │ │ ├── hyperlinkMenus.ts │ │ │ │ ├── imageMenus.ts │ │ │ │ └── tableMenus.ts │ │ ├── cursor │ │ │ ├── Cursor.ts │ │ │ └── CursorAgent.ts │ │ ├── draw │ │ │ ├── Draw.ts │ │ │ ├── control │ │ │ │ ├── Control.ts │ │ │ │ ├── checkbox │ │ │ │ │ └── CheckboxControl.ts │ │ │ │ ├── date │ │ │ │ │ └── DateControl.ts │ │ │ │ ├── interactive │ │ │ │ │ └── ControlSearch.ts │ │ │ │ ├── number │ │ │ │ │ └── NumberControl.ts │ │ │ │ ├── radio │ │ │ │ │ └── RadioControl.ts │ │ │ │ ├── richtext │ │ │ │ │ └── Border.ts │ │ │ │ ├── select │ │ │ │ │ └── SelectControl.ts │ │ │ │ └── text │ │ │ │ │ └── TextControl.ts │ │ │ ├── frame │ │ │ │ ├── Background.ts │ │ │ │ ├── Badge.ts │ │ │ │ ├── Footer.ts │ │ │ │ ├── Header.ts │ │ │ │ ├── LineNumber.ts │ │ │ │ ├── Margin.ts │ │ │ │ ├── PageBorder.ts │ │ │ │ ├── PageNumber.ts │ │ │ │ ├── Placeholder.ts │ │ │ │ └── Watermark.ts │ │ │ ├── interactive │ │ │ │ ├── Area.ts │ │ │ │ ├── Group.ts │ │ │ │ └── Search.ts │ │ │ ├── particle │ │ │ │ ├── CheckboxParticle.ts │ │ │ │ ├── HyperlinkParticle.ts │ │ │ │ ├── ImageParticle.ts │ │ │ │ ├── LineBreakParticle.ts │ │ │ │ ├── ListParticle.ts │ │ │ │ ├── PageBreakParticle.ts │ │ │ │ ├── RadioParticle.ts │ │ │ │ ├── SeparatorParticle.ts │ │ │ │ ├── SubscriptParticle.ts │ │ │ │ ├── SuperscriptParticle.ts │ │ │ │ ├── TextParticle.ts │ │ │ │ ├── block │ │ │ │ │ ├── BlockParticle.ts │ │ │ │ │ └── modules │ │ │ │ │ │ ├── BaseBlock.ts │ │ │ │ │ │ ├── IFrameBlock.ts │ │ │ │ │ │ └── VideoBlock.ts │ │ │ │ ├── date │ │ │ │ │ ├── DateParticle.ts │ │ │ │ │ └── DatePicker.ts │ │ │ │ ├── latex │ │ │ │ │ ├── LaTexParticle.ts │ │ │ │ │ └── utils │ │ │ │ │ │ ├── LaTexUtils.ts │ │ │ │ │ │ ├── hershey.ts │ │ │ │ │ │ └── symbols.ts │ │ │ │ ├── previewer │ │ │ │ │ └── Previewer.ts │ │ │ │ └── table │ │ │ │ │ ├── TableOperate.ts │ │ │ │ │ ├── TableParticle.ts │ │ │ │ │ └── TableTool.ts │ │ │ └── richtext │ │ │ │ ├── AbstractRichText.ts │ │ │ │ ├── Highlight.ts │ │ │ │ ├── Strikeout.ts │ │ │ │ └── Underline.ts │ │ ├── event │ │ │ ├── CanvasEvent.ts │ │ │ ├── GlobalEvent.ts │ │ │ ├── eventbus │ │ │ │ └── EventBus.ts │ │ │ └── handlers │ │ │ │ ├── click.ts │ │ │ │ ├── composition.ts │ │ │ │ ├── copy.ts │ │ │ │ ├── cut.ts │ │ │ │ ├── drag.ts │ │ │ │ ├── drop.ts │ │ │ │ ├── input.ts │ │ │ │ ├── keydown │ │ │ │ ├── backspace.ts │ │ │ │ ├── delete.ts │ │ │ │ ├── enter.ts │ │ │ │ ├── index.ts │ │ │ │ ├── left.ts │ │ │ │ ├── right.ts │ │ │ │ ├── tab.ts │ │ │ │ └── updown.ts │ │ │ │ ├── mousedown.ts │ │ │ │ ├── mouseleave.ts │ │ │ │ ├── mousemove.ts │ │ │ │ ├── mouseup.ts │ │ │ │ └── paste.ts │ │ ├── history │ │ │ └── HistoryManager.ts │ │ ├── i18n │ │ │ ├── I18n.ts │ │ │ └── lang │ │ │ │ ├── en.json │ │ │ │ └── zh-CN.json │ │ ├── listener │ │ │ └── Listener.ts │ │ ├── observer │ │ │ ├── ImageObserver.ts │ │ │ ├── MouseObserver.ts │ │ │ ├── ScrollObserver.ts │ │ │ └── SelectionObserver.ts │ │ ├── override │ │ │ └── Override.ts │ │ ├── plugin │ │ │ └── Plugin.ts │ │ ├── position │ │ │ └── Position.ts │ │ ├── range │ │ │ └── RangeManager.ts │ │ ├── register │ │ │ └── Register.ts │ │ ├── shortcut │ │ │ ├── Shortcut.ts │ │ │ └── keys │ │ │ │ ├── listKeys.ts │ │ │ │ ├── richtextKeys.ts │ │ │ │ └── titleKeys.ts │ │ ├── worker │ │ │ ├── WorkerManager.ts │ │ │ └── works │ │ │ │ ├── catalog.ts │ │ │ │ ├── group.ts │ │ │ │ ├── value.ts │ │ │ │ └── wordCount.ts │ │ └── zone │ │ │ ├── Zone.ts │ │ │ └── ZoneTip.ts │ ├── dataset │ │ ├── constant │ │ │ ├── Background.ts │ │ │ ├── Badge.ts │ │ │ ├── Checkbox.ts │ │ │ ├── Common.ts │ │ │ ├── ContextMenu.ts │ │ │ ├── Control.ts │ │ │ ├── Cursor.ts │ │ │ ├── Editor.ts │ │ │ ├── Element.ts │ │ │ ├── Footer.ts │ │ │ ├── Group.ts │ │ │ ├── Header.ts │ │ │ ├── LineBreak.ts │ │ │ ├── LineNumber.ts │ │ │ ├── List.ts │ │ │ ├── PageBorder.ts │ │ │ ├── PageBreak.ts │ │ │ ├── PageNumber.ts │ │ │ ├── Placeholder.ts │ │ │ ├── Radio.ts │ │ │ ├── Regular.ts │ │ │ ├── Separator.ts │ │ │ ├── Table.ts │ │ │ ├── Title.ts │ │ │ ├── Watermark.ts │ │ │ └── Zone.ts │ │ └── enum │ │ │ ├── Area.ts │ │ │ ├── Background.ts │ │ │ ├── Block.ts │ │ │ ├── Common.ts │ │ │ ├── Control.ts │ │ │ ├── Editor.ts │ │ │ ├── Element.ts │ │ │ ├── ElementStyle.ts │ │ │ ├── Event.ts │ │ │ ├── KeyMap.ts │ │ │ ├── LineNumber.ts │ │ │ ├── List.ts │ │ │ ├── Observer.ts │ │ │ ├── Row.ts │ │ │ ├── Text.ts │ │ │ ├── Title.ts │ │ │ ├── VerticalAlign.ts │ │ │ └── table │ │ │ ├── Table.ts │ │ │ └── TableTool.ts │ ├── index.ts │ ├── interface │ │ ├── Area.ts │ │ ├── Background.ts │ │ ├── Badge.ts │ │ ├── Block.ts │ │ ├── Catalog.ts │ │ ├── Checkbox.ts │ │ ├── Command.ts │ │ ├── Common.ts │ │ ├── Control.ts │ │ ├── Cursor.ts │ │ ├── Draw.ts │ │ ├── Editor.ts │ │ ├── Element.ts │ │ ├── Event.ts │ │ ├── EventBus.ts │ │ ├── Footer.ts │ │ ├── Group.ts │ │ ├── Header.ts │ │ ├── LineBreak.ts │ │ ├── LineNumber.ts │ │ ├── Listener.ts │ │ ├── Margin.ts │ │ ├── PageBorder.ts │ │ ├── PageBreak.ts │ │ ├── PageNumber.ts │ │ ├── Placeholder.ts │ │ ├── Plugin.ts │ │ ├── Position.ts │ │ ├── Previewer.ts │ │ ├── Radio.ts │ │ ├── Range.ts │ │ ├── Row.ts │ │ ├── Search.ts │ │ ├── Separator.ts │ │ ├── Text.ts │ │ ├── Title.ts │ │ ├── Watermark.ts │ │ ├── Zone.ts │ │ ├── contextmenu │ │ │ └── ContextMenu.ts │ │ ├── i18n │ │ │ └── I18n.ts │ │ ├── shortcut │ │ │ └── Shortcut.ts │ │ └── table │ │ │ ├── Colgroup.ts │ │ │ ├── Table.ts │ │ │ ├── Td.ts │ │ │ └── Tr.ts │ ├── types │ │ └── index.d.ts │ └── utils │ │ ├── clipboard.ts │ │ ├── element.ts │ │ ├── hotkey.ts │ │ ├── index.ts │ │ ├── option.ts │ │ ├── print.ts │ │ └── ua.ts ├── main.ts ├── mock.ts ├── plugins │ ├── copy │ │ └── index.ts │ └── markdown │ │ └── index.ts ├── style.css ├── utils │ ├── index.ts │ └── prism.ts └── vite-env.d.ts ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "env": { 14 | "browser": true 15 | }, 16 | "globals": { 17 | "process": true 18 | }, 19 | "rules": { 20 | "linebreak-style": 0, 21 | "no-console": 0, 22 | "no-debugger": 0, 23 | "no-useless-escape": "off", 24 | "@typescript-eslint/no-explicit-any": 0, 25 | "@typescript-eslint/no-empty-interface": 0, 26 | "@typescript-eslint/no-this-alias": 0, 27 | "@typescript-eslint/ban-ts-comment": 0, 28 | "@typescript-eslint/explicit-module-boundary-types": 0, 29 | "@typescript-eslint/no-non-null-assertion": 0, 30 | "@typescript-eslint/ban-types": [1, { 31 | "types": { 32 | "Function": false, 33 | "{}": false 34 | }, 35 | "extendDefaults": true 36 | }], 37 | "no-constant-condition": ["error", { 38 | "checkLoops": false 39 | }], 40 | "semi": [1, "never"], 41 | "quotes": [1, "single", { 42 | "allowTemplateLiterals": true 43 | }] 44 | }, 45 | "ignorePatterns": ["node_modules", "dist", "index.html"] 46 | } 47 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Hufe921 2 | custom: https://hufe.club/donate.jpg 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: docs 4 | url: https://hufe.club/canvas-editor-docs/ 5 | about: Official documents -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 New feature proposal" 2 | description: Suggest an idea for this project 3 | labels: [":sparkles: feature request"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **Before You Start...** 9 | 10 | This form is only for submitting feature requests. If you have a usage question 11 | or are unsure if this is really a bug, make sure to: 12 | 13 | - Read the [docs](https://hufe.club/canvas-editor-docs/) 14 | 15 | Also try to search for your issue - another user may have already requested something similar! 16 | 17 | 作者牺牲业余时间,非全职开发此项目。您的[赞助](https://hufe.club/donate.jpg)不仅能支持项目的持续发展,还能激励作者加快此功能的完善。赞助时可以备注需求id 18 | - type: textarea 19 | id: problem-description 20 | attributes: 21 | label: What problem does this feature solve? 22 | description: | 23 | 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? 24 | placeholder: Problem description 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: proposed-API 29 | attributes: 30 | label: What does the proposed API look like? 31 | description: | 32 | 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. 33 | placeholder: Steps to reproduce 34 | validations: 35 | required: true -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - '.github/workflows/build.yml' 8 | - 'src/**' 9 | - 'index.html' 10 | - 'package.json' 11 | - 'vite.config.ts' 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm i yarn -g 24 | - run: yarn 25 | - run: yarn run build 26 | - run: mv dist canvas-editor 27 | - name: Copy folder content recursively to remote 28 | uses: appleboy/scp-action@v0.1.7 29 | with: 30 | source: canvas-editor 31 | target: ${{ secrets.PATH }} 32 | host: ${{ secrets.HOST }} 33 | username: ${{ secrets.USERNAME }} 34 | password: ${{ secrets.PASSWORD }} 35 | overwrite: true 36 | - name: Executing remote ssh commands 37 | uses: appleboy/ssh-action@v1.0.3 38 | with: 39 | host: ${{ secrets.HOST }} 40 | username: ${{ secrets.USERNAME }} 41 | password: ${{ secrets.PASSWORD }} 42 | script: sed -i 's/<\/body>/${{ secrets.SCRIPT }}<\/body>/g' ${{ secrets.PATH }}/canvas-editor/index.html 43 | -------------------------------------------------------------------------------- /.github/workflows/cypress.yml: -------------------------------------------------------------------------------- 1 | name: cypress 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - '.github/workflows/*' 8 | - 'cypress/**' 9 | - 'src/**' 10 | - 'index.html' 11 | - 'package.json' 12 | - 'vite.config.ts' 13 | 14 | jobs: 15 | cypress: 16 | runs-on: ubuntu-latest 17 | container: cypress/browsers:node16.17.0-chrome106 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - run: npm i yarn serve@14.0.1 -g 26 | - run: yarn 27 | - run: yarn run build 28 | - run: mv dist canvas-editor 29 | - run: serve . -l 3000 & 30 | - run: yarn run cypress:run -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - '.github/workflows/docs.yml' 8 | - 'docs/**' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm i yarn -g 21 | - run: yarn 22 | - run: yarn run docs:build 23 | - run: mv ./docs/.vitepress/dist ./docs/.vitepress/canvas-editor-docs 24 | - name: Copy folder content recursively to remote 25 | uses: appleboy/scp-action@v0.1.7 26 | with: 27 | source: docs/.vitepress/canvas-editor-docs 28 | target: ${{ secrets.DOCS_PATH }} 29 | host: ${{ secrets.HOST }} 30 | username: ${{ secrets.USERNAME }} 31 | password: ${{ secrets.PASSWORD }} 32 | overwrite: true 33 | strip_components: 2 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_Store 4 | dist 5 | dist-ssr 6 | *.local 7 | cache 8 | .temp -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "trailingComma": "none", 6 | "arrowParens": "avoid", 7 | "endOfLine": "lf" 8 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "atrule", 4 | "Chainable", 5 | "colspan", 6 | "compositionend", 7 | "compositionstart", 8 | "contenteditable", 9 | "contextmenu", 10 | "CRDT", 11 | "deletable", 12 | "dppx", 13 | "esbenp", 14 | "eventbus", 15 | "inputarea", 16 | "keyof", 17 | "linebreak", 18 | "noopener", 19 | "Parens", 20 | "prismjs", 21 | "resizer", 22 | "richtext", 23 | "rowmargin", 24 | "rowspan", 25 | "srcdoc", 26 | "TEXTLIKE", 27 | "trlist", 28 | "updown", 29 | "vite", 30 | "vitepress", 31 | "Yahei" 32 | ], 33 | "cSpell.ignorePaths": [ 34 | ".github", 35 | "dist", 36 | "node_modules", 37 | "yarn.lock", 38 | "src/editor/core/draw/particle/latex/utils" 39 | ], 40 | "typescript.tsdk": "node_modules/typescript/lib", 41 | "editor.codeActionsOnSave": { 42 | "source.fixAll.eslint": "explicit" 43 | }, 44 | "[typescript]": { 45 | "editor.defaultFormatter": "esbenp.prettier-vscode" 46 | }, 47 | "[javascript]": { 48 | "editor.defaultFormatter": "esbenp.prettier-vscode" 49 | }, 50 | "[json]": { 51 | "editor.defaultFormatter": "esbenp.prettier-vscode" 52 | } 53 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-present, hufe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | video: false, 5 | viewportWidth: 1366, 6 | viewportHeight: 720, 7 | e2e: { 8 | experimentalRunAllSpecs: true 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /cypress/e2e/control/checkbox.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor, { ControlType, ElementType } from '../../../src/editor' 2 | 3 | describe('控件-复选框', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | const elementType: ElementType = 'control' 11 | const controlType: ControlType = 'checkbox' 12 | 13 | it('复选框', () => { 14 | cy.getEditor().then((editor: Editor) => { 15 | editor.command.executeSelectAll() 16 | 17 | editor.command.executeBackspace() 18 | 19 | editor.command.executeInsertElementList([ 20 | { 21 | type: elementType, 22 | value: '', 23 | control: { 24 | code: '98175', 25 | type: controlType, 26 | value: null, 27 | valueSets: [ 28 | { 29 | value: '有', 30 | code: '98175' 31 | }, 32 | { 33 | value: '无', 34 | code: '98176' 35 | } 36 | ] 37 | } 38 | } 39 | ]) 40 | 41 | const data = editor.command.getValue().data.main[0] 42 | 43 | expect(data.control!.code).to.be.eq('98175') 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /cypress/e2e/control/select.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor, { ControlType, ElementType } from '../../../src/editor' 2 | 3 | describe('控件-列举型', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | const text = `有` 11 | const elementType: ElementType = 'control' 12 | const controlType: ControlType = 'select' 13 | 14 | it('列举型', () => { 15 | cy.getEditor().then((editor: Editor) => { 16 | editor.command.executeSelectAll() 17 | 18 | editor.command.executeBackspace() 19 | 20 | editor.command.executeInsertElementList([ 21 | { 22 | type: elementType, 23 | value: '', 24 | control: { 25 | type: controlType, 26 | value: null, 27 | placeholder: '列举型', 28 | valueSets: [ 29 | { 30 | value: '有', 31 | code: '98175' 32 | }, 33 | { 34 | value: '无', 35 | code: '98176' 36 | } 37 | ] 38 | } 39 | } 40 | ]) 41 | 42 | cy.get('@canvas').type(`{leftArrow}`) 43 | 44 | cy.get('.ce-select-control-popup li') 45 | .eq(0) 46 | .click() 47 | .then(() => { 48 | const data = editor.command.getValue().data.main[0] 49 | 50 | expect(data.control!.value![0].value).to.be.eq(text) 51 | 52 | expect(data.control!.code).to.be.eq('98175') 53 | }) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /cypress/e2e/control/text.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor, { ControlType, ElementType } from '../../../src/editor' 2 | 3 | describe('控件-文本型', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | const text = `canvas-editor` 11 | const elementType: ElementType = 'control' 12 | const controlType: ControlType = 'text' 13 | 14 | it('文本型', () => { 15 | cy.getEditor().then((editor: Editor) => { 16 | editor.command.executeSelectAll() 17 | 18 | editor.command.executeBackspace() 19 | 20 | editor.command.executeInsertElementList([ 21 | { 22 | type: elementType, 23 | value: '', 24 | control: { 25 | type: controlType, 26 | value: null, 27 | placeholder: '文本型' 28 | } 29 | } 30 | ]) 31 | 32 | cy.get('@canvas').type(`{leftArrow}`) 33 | 34 | cy.get('.ce-inputarea') 35 | .type(text) 36 | .then(() => { 37 | const data = editor.command.getValue().data.main[0] 38 | 39 | expect(data.control!.value![0].value).to.be.eq(text) 40 | }) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /cypress/e2e/editor.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../src/editor' 2 | 3 | describe('基础功能', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | const text = 'canvas-editor' 11 | 12 | it('编辑保存', () => { 13 | cy.getEditor().then((editor: Editor) => { 14 | editor.command.executeSelectAll() 15 | 16 | editor.command.executeBackspace() 17 | 18 | cy.get('@canvas') 19 | .type(text) 20 | .then(() => { 21 | const data = editor.command.getValue().data.main 22 | 23 | expect(data[0].value).to.eq(text) 24 | }) 25 | }) 26 | }) 27 | 28 | it('模式切换', () => { 29 | cy.get('@canvas').click() 30 | 31 | cy.get('.ce-cursor').should('have.css', 'display', 'block') 32 | 33 | cy.get('.editor-mode').click().click() 34 | 35 | cy.get('.editor-mode').contains('只读') 36 | 37 | cy.get('@canvas').click() 38 | 39 | cy.get('.ce-cursor').should('have.css', 'display', 'none') 40 | }) 41 | 42 | it('页面缩放', () => { 43 | cy.get('.page-scale-add').click() 44 | 45 | cy.get('.page-scale-percentage').contains('110%') 46 | 47 | cy.get('.page-scale-minus').click().click() 48 | 49 | cy.get('.page-scale-percentage').contains('90%') 50 | }) 51 | 52 | it('字数统计', () => { 53 | cy.getEditor().then((editor: Editor) => { 54 | editor.command.executeSelectAll() 55 | 56 | editor.command.executeBackspace() 57 | 58 | editor.command.executeInsertElementList([ 59 | { 60 | value: 'canvas-editor 2022 编辑器' 61 | } 62 | ]) 63 | 64 | cy.get('.word-count').contains('7') 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /cypress/e2e/menus/block.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/editor' 2 | 3 | describe('菜单-内容块', () => { 4 | const url = 'http://localhost:3000/canvas-editor/' 5 | 6 | beforeEach(() => { 7 | cy.visit(url) 8 | 9 | cy.get('canvas').first().as('canvas').should('have.length', 1) 10 | }) 11 | 12 | it('内容块', () => { 13 | cy.getEditor().then((editor: Editor) => { 14 | editor.command.executeSelectAll() 15 | 16 | editor.command.executeBackspace() 17 | 18 | cy.get('.menu-item__block').click() 19 | 20 | cy.get('.dialog-option__item [name="width"]').type('500') 21 | 22 | cy.get('.dialog-option__item [name="height"]').type('300') 23 | 24 | cy.get('.dialog-option__item [name="src"]').type(url) 25 | 26 | cy.get('.dialog-menu button') 27 | .eq(1) 28 | .click() 29 | .then(() => { 30 | const data = editor.command.getValue().data.main 31 | 32 | expect(data[0].type).to.eq('block') 33 | 34 | expect(data[0].block?.iframeBlock?.src).to.eq(url) 35 | }) 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /cypress/e2e/menus/checkbox.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor, { ElementType } from '../../../src/editor' 2 | 3 | describe('菜单-复选框', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | const type: ElementType = 'checkbox' 11 | 12 | it('代码块', () => { 13 | cy.getEditor().then((editor: Editor) => { 14 | editor.command.executeSelectAll() 15 | 16 | editor.command.executeBackspace() 17 | 18 | editor.command.executeInsertElementList([ 19 | { 20 | type, 21 | value: '', 22 | checkbox: { 23 | value: true 24 | } 25 | } 26 | ]) 27 | 28 | const data = editor.command.getValue().data.main[0] 29 | 30 | expect(data.checkbox?.value).to.eq(true) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /cypress/e2e/menus/codeblock.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/editor' 2 | 3 | describe('菜单-代码块', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | const text = `console.log('canvas-editor')` 11 | 12 | it('代码块', () => { 13 | cy.getEditor().then((editor: Editor) => { 14 | editor.command.executeSelectAll() 15 | 16 | editor.command.executeBackspace() 17 | 18 | cy.get('.menu-item__codeblock').click() 19 | 20 | cy.get('.dialog-option [name="codeblock"]').type(text) 21 | 22 | cy.get('.dialog-menu button') 23 | .eq(1) 24 | .click() 25 | .then(() => { 26 | const data = editor.command.getValue().data.main[2] 27 | 28 | expect(data.value).to.eq('log') 29 | 30 | expect(data.color).to.eq('#b9a40a') 31 | }) 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /cypress/e2e/menus/date.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/editor' 2 | 3 | describe('菜单-日期选择器', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | it('LaTeX', () => { 11 | cy.getEditor().then((editor: Editor) => { 12 | editor.command.executeSelectAll() 13 | 14 | editor.command.executeBackspace() 15 | 16 | cy.get('.menu-item__date').click() 17 | 18 | cy.get('.menu-item__date li') 19 | .first() 20 | .click() 21 | .then(() => { 22 | const data = editor.command.getValue().data.main 23 | 24 | expect(data[0].type).to.eq('date') 25 | }) 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /cypress/e2e/menus/format.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/editor' 2 | 3 | describe('菜单-清除格式', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | const text = 'canvas-editor' 11 | const textLength = text.length 12 | 13 | it('清除格式', () => { 14 | cy.getEditor().then((editor: Editor) => { 15 | editor.command.executeSelectAll() 16 | 17 | editor.command.executeBackspace() 18 | 19 | editor.command.executeInsertElementList([ 20 | { 21 | value: text, 22 | bold: true, 23 | italic: true 24 | } 25 | ]) 26 | 27 | editor.command.executeSetRange(0, textLength) 28 | 29 | cy.get('.menu-item__format') 30 | .click() 31 | .then(() => { 32 | const data = editor.command.getValue().data.main 33 | 34 | expect(data[0].italic).to.eq(undefined) 35 | 36 | expect(data[0].bold).to.eq(undefined) 37 | }) 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /cypress/e2e/menus/hyperlink.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/editor' 2 | 3 | describe('菜单-超链接', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | const text = 'canvas-editor' 11 | const url = 'https://hufe.club/canvas-editor' 12 | 13 | it('超链接', () => { 14 | cy.getEditor().then((editor: Editor) => { 15 | editor.command.executeSelectAll() 16 | 17 | editor.command.executeBackspace() 18 | 19 | cy.get('.menu-item__hyperlink').click() 20 | 21 | cy.get('.dialog-option__item [name="name"]').type(text) 22 | 23 | cy.get('.dialog-option__item [name="url"]').type(url) 24 | 25 | cy.get('.dialog-menu button') 26 | .eq(1) 27 | .click() 28 | .then(() => { 29 | const data = editor.command.getValue().data.main 30 | 31 | expect(data[0].type).to.eq('hyperlink') 32 | 33 | expect(data[0].url).to.eq(url) 34 | 35 | expect(data[0]?.valueList?.[0].value).to.eq(text) 36 | }) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /cypress/e2e/menus/image.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/editor' 2 | 3 | describe('菜单-图片', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | it('图片', () => { 11 | cy.getEditor().then((editor: Editor) => { 12 | editor.command.executeSelectAll() 13 | 14 | editor.command.executeBackspace() 15 | 16 | cy.get('#image').attachFile('test.png') 17 | 18 | cy.wait(200).then(() => { 19 | const data = editor.command.getValue().data.main 20 | 21 | expect(data[0].type).to.eq('image') 22 | }) 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /cypress/e2e/menus/latex.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/editor' 2 | 3 | describe('菜单-LaTeX', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | const text = 'canvas-editor' 11 | 12 | it('LaTeX', () => { 13 | cy.getEditor().then((editor: Editor) => { 14 | editor.command.executeSelectAll() 15 | 16 | editor.command.executeBackspace() 17 | 18 | cy.get('.menu-item__latex').click() 19 | 20 | cy.get('.dialog-option__item [name="value"]').type(text) 21 | 22 | cy.get('.dialog-menu button') 23 | .eq(1) 24 | .click() 25 | .then(() => { 26 | const data = editor.command.getValue().data.main 27 | 28 | expect(data[0].type).to.eq('latex') 29 | 30 | expect(data[0].value).to.eq(text) 31 | }) 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /cypress/e2e/menus/pagebreak.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/editor' 2 | 3 | describe('菜单-分页符', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | it('分页符', () => { 11 | cy.getEditor().then((editor: Editor) => { 12 | editor.command.executeSelectAll() 13 | 14 | editor.command.executeBackspace() 15 | 16 | cy.get('.menu-item__page-break').click().click() 17 | 18 | cy.get('canvas').should('have.length', 2) 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /cypress/e2e/menus/painter.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/editor' 2 | 3 | describe('菜单-格式刷', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | const text = 'canvas-editor' 11 | const textLength = text.length 12 | 13 | it('格式刷', () => { 14 | cy.getEditor().then((editor: Editor) => { 15 | editor.command.executeSelectAll() 16 | 17 | editor.command.executeBackspace() 18 | 19 | editor.command.executeInsertElementList([ 20 | { 21 | value: text, 22 | bold: true, 23 | italic: true 24 | } 25 | ]) 26 | 27 | editor.command.executeInsertElementList([ 28 | { 29 | value: text 30 | } 31 | ]) 32 | 33 | editor.command.executeSetRange(0, textLength) 34 | 35 | cy.get('.menu-item__painter') 36 | .click() 37 | .wait(300) 38 | .then(() => { 39 | editor.command.executeSetRange(textLength, 2 * textLength) 40 | 41 | editor.command.executeApplyPainterStyle() 42 | 43 | const data = editor.command.getValue().data.main 44 | 45 | expect(data.length).to.eq(1) 46 | 47 | expect(data[0].italic).to.eq(true) 48 | 49 | expect(data[0].bold).to.eq(true) 50 | }) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /cypress/e2e/menus/print.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/editor' 2 | 3 | describe('菜单-打印', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').should('have.length', 2) 8 | }) 9 | 10 | it('打印', () => { 11 | cy.getEditor().then(async (editor: Editor) => { 12 | const imageList2 = await editor.command.getImage() 13 | expect(imageList2.length).to.eq(2) 14 | 15 | editor.command.executeSelectAll() 16 | 17 | editor.command.executeBackspace() 18 | 19 | cy.wait(200).then(async () => { 20 | const imageList1 = await editor.command.getImage() 21 | expect(imageList1.length).to.eq(1) 22 | }) 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /cypress/e2e/menus/separator.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/editor' 2 | 3 | describe('菜单-分割线', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | it('分割线', () => { 11 | cy.getEditor().then((editor: Editor) => { 12 | editor.command.executeSelectAll() 13 | 14 | editor.command.executeBackspace() 15 | 16 | cy.get('.menu-item__separator').click() 17 | 18 | cy.get('.menu-item__separator li') 19 | .eq(1) 20 | .click() 21 | .then(() => { 22 | const data = editor.command.getValue().data.main 23 | 24 | expect(data[0].type).to.eq('separator') 25 | 26 | expect(data[0]?.dashArray?.[0]).to.eq(1) 27 | 28 | expect(data[0]?.dashArray?.[1]).to.eq(1) 29 | }) 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /cypress/e2e/menus/table.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/editor' 2 | 3 | describe('菜单-表格', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | it('表格', () => { 11 | cy.getEditor().then((editor: Editor) => { 12 | editor.command.executeSelectAll() 13 | 14 | editor.command.executeBackspace() 15 | 16 | editor.command.executeInsertTable(8, 8) 17 | 18 | const data = editor.command.getValue().data.main 19 | 20 | expect(data[0].type).to.eq('table') 21 | 22 | expect(data[0].trList?.length).to.eq(8) 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /cypress/e2e/menus/title.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor, { ElementType, TitleLevel } from '../../../src/editor' 2 | 3 | describe('菜单-标题', () => { 4 | const url = 'http://localhost:3000/canvas-editor/' 5 | 6 | beforeEach(() => { 7 | cy.visit(url) 8 | 9 | cy.get('canvas').first().as('canvas').should('have.length', 1) 10 | }) 11 | 12 | const text = 'canvas-editor' 13 | const elementType = 'title' 14 | const level = 'first' 15 | 16 | it('标题', () => { 17 | cy.getEditor().then((editor: Editor) => { 18 | editor.command.executeSelectAll() 19 | 20 | editor.command.executeBackspace() 21 | 22 | editor.command.executeInsertElementList([ 23 | { 24 | value: text 25 | } 26 | ]) 27 | 28 | cy.get('.menu-item__title').as('title').click() 29 | 30 | cy.get('@title') 31 | .find('li') 32 | .eq(1) 33 | .click() 34 | .then(() => { 35 | const data = editor.command.getValue().data.main 36 | 37 | expect(data[0].type).to.eq(elementType) 38 | 39 | expect(data[0].level).to.eq(level) 40 | }) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /cypress/e2e/menus/undoRedo.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/editor' 2 | 3 | describe('菜单-撤销&重做', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | const text = 'canvas-editor' 11 | 12 | it('撤销', () => { 13 | cy.getEditor().then((editor: Editor) => { 14 | editor.command.executeSelectAll() 15 | 16 | editor.command.executeBackspace() 17 | 18 | cy.get('@canvas').type(`${text}1`) 19 | 20 | cy.get('.menu-item__undo') 21 | .click() 22 | .then(() => { 23 | const data = editor.command.getValue().data.main 24 | 25 | expect(data[0].value).to.eq(text) 26 | }) 27 | }) 28 | }) 29 | 30 | it('重做', () => { 31 | cy.getEditor().then((editor: Editor) => { 32 | editor.command.executeSelectAll() 33 | 34 | editor.command.executeBackspace() 35 | 36 | cy.get('@canvas').type(`${text}1`) 37 | 38 | cy.get('.menu-item__undo').click() 39 | 40 | cy.get('.menu-item__redo') 41 | .click() 42 | .then(() => { 43 | const data = editor.command.getValue().data.main 44 | 45 | expect(data[0].value).to.eq(`${text}1`) 46 | }) 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /cypress/e2e/menus/watermark.cy.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/editor' 2 | 3 | describe('菜单-水印', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/canvas-editor/') 6 | 7 | cy.get('canvas').first().as('canvas').should('have.length', 1) 8 | }) 9 | 10 | const text = 'canvas-editor' 11 | const size = 80 12 | 13 | it('添加水印', () => { 14 | cy.getEditor().then((editor: Editor) => { 15 | cy.get('.menu-item__watermark').click() 16 | 17 | cy.get('.menu-item__watermark li').eq(0).click() 18 | 19 | cy.get('.dialog-option [name="data"]').type(text) 20 | 21 | cy.get('.dialog-option [name="size"]').as('size') 22 | 23 | cy.get('@size').clear() 24 | 25 | cy.get('@size').type(`${size}`) 26 | 27 | cy.get('.dialog-menu button') 28 | .eq(1) 29 | .click() 30 | .then(() => { 31 | const payload = editor.command.getValue() 32 | 33 | const { 34 | options: { watermark } 35 | } = payload 36 | 37 | expect(watermark?.data).to.eq(text) 38 | 39 | expect(watermark?.size).to.eq(size) 40 | }) 41 | }) 42 | }) 43 | 44 | it('删除水印', () => { 45 | cy.getEditor().then((editor: Editor) => { 46 | cy.get('.menu-item__watermark').click() 47 | 48 | cy.get('.menu-item__watermark li') 49 | .eq(1) 50 | .click() 51 | .then(() => { 52 | const payload = editor.command.getValue() 53 | 54 | const { 55 | options: { watermark } 56 | } = payload 57 | 58 | expect(watermark?.data).to.eq('') 59 | 60 | expect(watermark?.size).to.eq(200) 61 | }) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "canvas-editor" 3 | } 4 | -------------------------------------------------------------------------------- /cypress/fixtures/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/cypress/fixtures/test.png -------------------------------------------------------------------------------- /cypress/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Editor { 4 | import('../src/editor/index') 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | import Editor from '../src/editor/index' 7 | } 8 | 9 | declare namespace Cypress { 10 | interface Chainable { 11 | getEditor(): Chainable 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | import 'cypress-file-upload' 2 | 3 | Cypress.Commands.add('getEditor', () => { 4 | return cy.window().its('editor') 5 | }) 6 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | import './commands' 2 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es2015", "dom", "esnext"], 5 | "types": ["cypress", "cypress-file-upload"], 6 | "isolatedModules": false, 7 | "allowJs": true, 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | }, 18 | "include": [ 19 | "./**/*.ts" 20 | ] 21 | } -------------------------------------------------------------------------------- /docs/en/guide/api-common.md: -------------------------------------------------------------------------------- 1 | # Common API 2 | 3 | ## splitText 4 | 5 | Feature: split text 6 | 7 | Usage: 8 | 9 | ```javascript 10 | import { splitText } from '@hufe921/canvas-editor' 11 | 12 | splitText(text: string): string[] 13 | ``` 14 | 15 | ## createDomFromElementList 16 | 17 | Feature: Create a DOM tree based on the elementList 18 | 19 | Usage: 20 | 21 | ```javascript 22 | import { createDomFromElementList } from '@hufe921/canvas-editor' 23 | 24 | createDomFromElementList(elementList: IElement[], options?: IEditorOption): HTMLDivElement 25 | ``` 26 | 27 | ## getElementListByHTML 28 | 29 | Feature: Create an elementList based on HTML 30 | 31 | Usage: 32 | 33 | ```javascript 34 | import { getElementListByHTML } from '@hufe921/canvas-editor' 35 | 36 | getElementListByHTML(htmlText: string, options: IGetElementListByHTMLOption): IElement[] 37 | ``` 38 | 39 | ## getTextFromElementList 40 | 41 | Feature: Create text based on elementList 42 | 43 | Usage: 44 | 45 | ```javascript 46 | import { getTextFromElementList } from '@hufe921/canvas-editor' 47 | 48 | getTextFromElementList(elementList: IElement[]): string 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/en/guide/api-instance.md: -------------------------------------------------------------------------------- 1 | # Instance API 2 | 3 | ## How to Use 4 | 5 | ```javascript 6 | import Editor from "@hufe921/canvas-editor" 7 | 8 | const instance = new Editor(container, data, options) 9 | instance.apiName() 10 | ``` 11 | 12 | ## destroy 13 | 14 | Feature: Destroy the editor 15 | 16 | Usage: 17 | 18 | ```javascript 19 | instance.destroy() 20 | ``` 21 | 22 | ::: warning 23 | Only destroy the editor DOM and related events, menu bars, toolbars, external variables, etc. need to be handled by themselves. 24 | ::: 25 | -------------------------------------------------------------------------------- /docs/en/guide/contextmenu-custom.md: -------------------------------------------------------------------------------- 1 | # Customize Contextmenu 2 | 3 | ## How to Use 4 | 5 | ```javascript 6 | import Editor from "@hufe921/canvas-editor" 7 | 8 | const instance = new Editor(container, data, options) 9 | instance.register.contextMenuList([ 10 | { 11 | key?: string; 12 | isDivider?: boolean; 13 | icon?: string; 14 | name?: string; // Use %s for selection text. Example: Search: %s 15 | shortCut?: string; 16 | disable?: boolean; 17 | when?: (payload: IContextMenuContext) => boolean; 18 | callback?: (command: Command, context: IContextMenuContext) => any; 19 | childMenus?: IRegisterContextMenu[]; 20 | } 21 | ]) 22 | ``` 23 | 24 | ## getContextMenuList 25 | 26 | Feature: Get context menu list 27 | 28 | Usage: 29 | 30 | ```javascript 31 | const contextMenuList = await instance.register.getContextMenuList() 32 | ``` 33 | 34 | Remark: 35 | 36 | ```javascript 37 | // Example of modifying internal contextmenu 38 | contextmenuList.forEach(menu => { 39 | // Find the menu item through the menu key and modify its properties 40 | if (menu.key === INTERNAL_CONTEXT_MENU_KEY.GLOBAL.PASTE) { 41 | menu.when = () => false 42 | } 43 | }) 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/en/guide/contextmenu-internal.md: -------------------------------------------------------------------------------- 1 | # Internal Contextmenu 2 | 3 | ## Global 4 | 5 | - Cut 6 | - Copy 7 | - Paste 8 | - Select all 9 | - Print 10 | 11 | ## Hyperlinks 12 | 13 | - Delete the link 14 | - Unlink 15 | - Edit the link 16 | 17 | ## Image 18 | 19 | - Change the picture 20 | - Save as picture 21 | - Text wrapping 22 | - Embed 23 | - Up down 24 | - Surround 25 | - Float above text 26 | - Float below text 27 | 28 | ## Table 29 | 30 | - Table borders 31 | - All borders 32 | - Borderless 33 | - Dashed border 34 | - Outer border 35 | - Internal border 36 | - Td borders 37 | - Top border 38 | - Right border 39 | - Bottom border 40 | - Left border 41 | - Forward border 42 | - Back border 43 | - Vertical alignment 44 | - Top alignment 45 | - Center vertically 46 | - Bottom end alignment 47 | - Insert rows and columns 48 | - Insert 1 row above 49 | - Insert 1 row below 50 | - Insert 1 column on the left 51 | - Insert 1 column on the right side 52 | - Delete rows and columns 53 | - Delete 1 row 54 | - Remove 1 column 55 | - Delete the entire table 56 | - Merge cells 57 | - Cancel the merge 58 | 59 | ## control 60 | 61 | - Delete the control 62 | -------------------------------------------------------------------------------- /docs/en/guide/override.md: -------------------------------------------------------------------------------- 1 | # Override 2 | 3 | ## How to Use 4 | 5 | ```javascript 6 | import Editor from "@hufe921/canvas-editor" 7 | 8 | const instance = new Editor(container, data, options) 9 | 10 | instance.override.overrideFunction = () => unknown | IOverrideResult 11 | ``` 12 | 13 | ```typescript 14 | interface IOverrideResult { 15 | preventDefault?: boolean // Prevent the execution of internal default method. Default prevent 16 | } 17 | ``` 18 | 19 | ## paste 20 | 21 | Feature: Override internal paste function 22 | 23 | Usage: 24 | 25 | ```javascript 26 | instance.override.paste = (evt?: ClipboardEvent) => unknown | IOverrideResult 27 | ``` 28 | 29 | ## copy 30 | 31 | Feature: Override internal copy function 32 | 33 | Usage: 34 | 35 | ```javascript 36 | instance.override.copy = () => unknown | IOverrideResult 37 | ``` 38 | 39 | ## drop 40 | 41 | Feature: Override internal drop function 42 | 43 | Usage: 44 | 45 | ```javascript 46 | instance.override.drop = (evt: DragEvent) => unknown | IOverrideResult 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/en/guide/plugin-custom.md: -------------------------------------------------------------------------------- 1 | # Custom Plugin 2 | 3 | ::: tip 4 | Official plugin: https://github.com/Hufe921/canvas-editor-plugin 5 | ::: 6 | 7 | ## Write a Plugin 8 | 9 | ```javascript 10 | export function myPlugin(editor: Editor, options?: Option) { 11 | // 1. update,see more:src/plugins/copy 12 | editor.command.updateFunction = () => {} 13 | 14 | // 2. add,see more:src/plugins/markdown 15 | editor.command.addFunction = () => {} 16 | 17 | // 3. listener, eventbus, shortcut, contextmenu, override... 18 | } 19 | ``` 20 | 21 | ## Use Plugin 22 | 23 | ```javascript 24 | instance.use(myPlugin, options?: Option) 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/en/guide/shortcut-custom.md: -------------------------------------------------------------------------------- 1 | # Custom shortcut keys 2 | 3 | ## How to use? 4 | 5 | ```javascript 6 | import Editor from "@hufe921/canvas-editor" 7 | 8 | const instance = new Editor(container, data, options) 9 | instance.register.shortcutList([ 10 | { 11 | key: KeyMap; 12 | ctrl?: boolean; 13 | meta?: boolean; 14 | mod?: boolean; // windows:ctrl || mac:command 15 | shift?: boolean; 16 | alt?: boolean; 17 | isGlobal?: boolean; 18 | callback?: (command: Command) => any; 19 | disable?: boolean; 20 | } 21 | ]) 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/en/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | title: canvas-editor 5 | titleTemplate: rich text editor by canvas/svg 6 | 7 | hero: 8 | name: canvas-editor 9 | text: canvas/svg based rich text editor 10 | actions: 11 | - theme: brand 12 | text: Get Started 13 | link: /en/guide/start.html 14 | - theme: alt 15 | text: View on Github 16 | link: https://github.com/Hufe921/canvas-editor 17 | 18 | features: 19 | - icon: 💡 20 | title: WYSIWYG 21 | details: Similar to word pageable, what you see is what you get 22 | - icon: ⚡️ 23 | title: Lightweight Data Structure 24 | details: A piece of JSON can render complex styles 25 | - icon: 🛠️ 26 | title: Rich Features 27 | details: Supports familiar rich text operations, tables, watermarks, controls, formulas, etc 28 | - icon: 📦 29 | title: Easy to Use 30 | details: The core npm package is officially released, and the menu bar and toolbar can be maintained by themselves 31 | - icon: 🔩 32 | title: Flexible Development Mechanism 33 | details: Through the interface, you can obtain the life cycle, event callback, custom right-click menu, and shortcut keys 34 | - icon: 🔑 35 | title: Full TypeScript Types API 36 | details: Flexible apis and full TypeScript types 37 | --- 38 | 39 | 44 | -------------------------------------------------------------------------------- /docs/guide/api-common.md: -------------------------------------------------------------------------------- 1 | # 通用 API 2 | 3 | ## splitText 4 | 5 | 功能:拆分字符 6 | 7 | 用法: 8 | 9 | ```javascript 10 | import { splitText } from '@hufe921/canvas-editor' 11 | 12 | splitText(text: string): string[] 13 | ``` 14 | 15 | ## createDomFromElementList 16 | 17 | 功能:根据 elementList 创建 dom 树 18 | 19 | 用法: 20 | 21 | ```javascript 22 | import { createDomFromElementList } from '@hufe921/canvas-editor' 23 | 24 | createDomFromElementList(elementList: IElement[], options?: IEditorOption): HTMLDivElement 25 | ``` 26 | 27 | ## getElementListByHTML 28 | 29 | 功能:根据 HTML 创建 elementList 30 | 31 | 用法: 32 | 33 | ```javascript 34 | import { getElementListByHTML } from '@hufe921/canvas-editor' 35 | 36 | getElementListByHTML(htmlText: string, options: IGetElementListByHTMLOption): IElement[] 37 | ``` 38 | 39 | ## getTextFromElementList 40 | 41 | 功能:根据 elementList 创建文本 42 | 43 | 用法: 44 | 45 | ```javascript 46 | import { getTextFromElementList } from '@hufe921/canvas-editor' 47 | 48 | getTextFromElementList(elementList: IElement[]): string 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/guide/api-instance.md: -------------------------------------------------------------------------------- 1 | # 实例 API 2 | 3 | ## 使用方式 4 | 5 | ```javascript 6 | import Editor from "@hufe921/canvas-editor" 7 | 8 | const instance = new Editor(container, data, options) 9 | instance.apiName() 10 | ``` 11 | 12 | ## destroy 13 | 14 | 功能:销毁编辑器 15 | 16 | 用法: 17 | 18 | ```javascript 19 | instance.destroy() 20 | ``` 21 | 22 | ::: warning 23 | 仅销毁编辑器 dom 及相关事件,菜单栏、工具栏、外部变量等需自行处理。 24 | ::: 25 | -------------------------------------------------------------------------------- /docs/guide/contextmenu-custom.md: -------------------------------------------------------------------------------- 1 | # 自定义右键菜单 2 | 3 | ## 使用方式 4 | 5 | ```javascript 6 | import Editor from "@hufe921/canvas-editor" 7 | 8 | const instance = new Editor(container, data, options) 9 | instance.register.contextMenuList([ 10 | { 11 | key?: string; 12 | isDivider?: boolean; 13 | icon?: string; 14 | name?: string; // 使用%s代表选区文本。示例:搜索:%s 15 | shortCut?: string; 16 | disable?: boolean; 17 | when?: (payload: IContextMenuContext) => boolean; 18 | callback?: (command: Command, context: IContextMenuContext) => any; 19 | childMenus?: IRegisterContextMenu[]; 20 | } 21 | ]) 22 | ``` 23 | 24 | ## getContextMenuList 25 | 26 | 功能:获取注册的右键菜单列表 27 | 28 | 用法: 29 | 30 | ```javascript 31 | const contextMenuList = await instance.register.getContextMenuList() 32 | ``` 33 | 34 | 备注: 35 | 36 | ```javascript 37 | // 修改内部右键菜单示例 38 | contextmenuList.forEach(menu => { 39 | // 通过菜单key找到菜单项后进行属性修改 40 | if (menu.key === INTERNAL_CONTEXT_MENU_KEY.GLOBAL.PASTE) { 41 | menu.when = () => false 42 | } 43 | }) 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/guide/contextmenu-internal.md: -------------------------------------------------------------------------------- 1 | # 内部右键菜单 2 | 3 | ## 全局 4 | 5 | - 剪切 6 | - 复制 7 | - 粘贴 8 | - 全选 9 | - 打印 10 | 11 | ## 超链接 12 | 13 | - 删除链接 14 | - 取消链接 15 | - 编辑链接 16 | 17 | ## 图片 18 | 19 | - 更改图片 20 | - 另存为图片 21 | - 文字环绕 22 | - 嵌入型 23 | - 上下型环绕 24 | - 四周型环绕 25 | - 浮于文字上方 26 | - 衬于文字下方 27 | 28 | ## 表格 29 | 30 | - 表格边框 31 | - 所有框线 32 | - 无框线 33 | - 虚框线 34 | - 外侧框线 35 | - 内侧框线 36 | - 单元格边框 37 | - 上边框 38 | - 右边框 39 | - 下边框 40 | - 左边框 41 | - 正斜线 42 | - 反斜线 43 | - 垂直对齐 44 | - 顶端对齐 45 | - 垂直居中 46 | - 底端对齐 47 | - 插入行列 48 | - 上方插入 1 行 49 | - 下方插入 1 行 50 | - 左侧插入 1 列 51 | - 右侧插入 1 列 52 | - 删除行列 53 | - 删除 1 行 54 | - 删除 1 列 55 | - 删除整个表格 56 | - 合并单元格 57 | - 取消合并 58 | 59 | ## 控件 60 | 61 | - 删除控件 62 | -------------------------------------------------------------------------------- /docs/guide/override.md: -------------------------------------------------------------------------------- 1 | # 重写方法 2 | 3 | ## 使用方式 4 | 5 | ```javascript 6 | import Editor from "@hufe921/canvas-editor" 7 | 8 | const instance = new Editor(container, data, options) 9 | 10 | instance.override.overrideFunction = () => unknown | IOverrideResult 11 | ``` 12 | 13 | ```typescript 14 | interface IOverrideResult { 15 | preventDefault?: boolean // 阻止执行内部默认方法。默认阻止 16 | } 17 | ``` 18 | 19 | ## paste 20 | 21 | 功能:重写粘贴方法 22 | 23 | 用法: 24 | 25 | ```javascript 26 | instance.override.paste = (evt?: ClipboardEvent) => unknown | IOverrideResult 27 | ``` 28 | 29 | ## copy 30 | 31 | 功能:重写复制方法 32 | 33 | 用法: 34 | 35 | ```javascript 36 | instance.override.copy = () => unknown | IOverrideResult 37 | ``` 38 | 39 | ## drop 40 | 41 | 功能:重写拖放方法 42 | 43 | 用法: 44 | 45 | ```javascript 46 | instance.override.drop = (evt: DragEvent) => unknown | IOverrideResult 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/guide/plugin-custom.md: -------------------------------------------------------------------------------- 1 | # 自定义插件 2 | 3 | ::: tip 4 | 官方维护插件仓库:https://github.com/Hufe921/canvas-editor-plugin 5 | ::: 6 | 7 | ## 开发插件 8 | 9 | ```javascript 10 | export function myPlugin(editor: Editor, options?: Option) { 11 | // 1. 修改方法,详见:src/plugins/copy 12 | editor.command.updateFunction = () => {} 13 | 14 | // 2. 增加方法,详见:src/plugins/markdown 15 | editor.command.addFunction = () => {} 16 | 17 | // 3. 事件监听、快捷键、右键菜单、重写方法等组合处理 18 | } 19 | ``` 20 | 21 | ## 使用插件 22 | 23 | ```javascript 24 | instance.use(myPlugin, options?: Option) 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/guide/shortcut-custom.md: -------------------------------------------------------------------------------- 1 | # 自定义快捷键 2 | 3 | ## 使用方式 4 | 5 | ```javascript 6 | import Editor from "@hufe921/canvas-editor" 7 | 8 | const instance = new Editor(container, data, options) 9 | instance.register.shortcutList([ 10 | { 11 | key: KeyMap; 12 | ctrl?: boolean; 13 | meta?: boolean; 14 | mod?: boolean; // windows:ctrl || mac:command 15 | shift?: boolean; 16 | alt?: boolean; 17 | isGlobal?: boolean; 18 | callback?: (command: Command) => any; 19 | disable?: boolean; 20 | } 21 | ]) 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/guide/start.md: -------------------------------------------------------------------------------- 1 | # 入门 2 | 3 | > 所见即所得的富文本编辑器。 4 | 5 | 得益于光标及文字排版的完全自行实现。绘制底层也可由 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)。 6 | 7 | ::: warning 8 | 官方仅提供编辑器核心层 npm 包,菜单栏或其他外部工具可自行参考文档扩展,或直接参考[官方](https://github.com/Hufe921/canvas-editor)实现,详见[demo](https://hufe.club/canvas-editor/)。 9 | ::: 10 | 11 | ## 功能点 12 | 13 | - 富文本操作(撤销、重做、字体、字号、加粗、斜体、下划线、删除线、上下标、对齐方式、标题、列表.....) 14 | - 插入元素(表格、图片、链接、代码块、分页符、Math 公式、日期选择器、内容块......) 15 | - 打印(基于 canvas 转图片、pdf 绘制) 16 | - 控件(单选、文本、日期、单选框组、复选框组) 17 | - 右键菜单(内部、自定义) 18 | - 快捷键(内部、自定义) 19 | - 拖拽(文字、元素、控件) 20 | - 页眉、页脚、页码 21 | - 页边距 22 | - 分页 23 | - 水印 24 | - 批注 25 | - 目录 26 | - [插件](https://github.com/Hufe921/canvas-editor-plugin) 27 | 28 | ## 待开发 29 | 30 | - 计算性能 31 | - 控件规则 32 | - 表格分页 33 | - vue、react 等框架开箱即用版 34 | 35 | ## Step. 1: 下载 npm 包 36 | 37 | ```sh 38 | npm i @hufe921/canvas-editor --save 39 | ``` 40 | 41 | ## Step. 2: 准备一个容器 42 | 43 | ```html 44 |
45 | ``` 46 | 47 | ## Step. 3: 实例化编辑器 48 | 49 | - 仅包含正文内容 50 | 51 | ```javascript 52 | import Editor from '@hufe921/canvas-editor' 53 | 54 | new Editor( 55 | document.querySelector('.canvas-editor'), 56 | [ 57 | { 58 | value: 'Hello World' 59 | } 60 | ], 61 | {} 62 | ) 63 | ``` 64 | 65 | - 包含正文、页眉、页脚内容 66 | 67 | ```javascript 68 | import Editor from '@hufe921/canvas-editor' 69 | 70 | new Editor( 71 | document.querySelector('.canvas-editor'), 72 | { 73 | header: [ 74 | { 75 | value: 'Header', 76 | rowFlex: RowFlex.CENTER 77 | } 78 | ], 79 | main: [ 80 | { 81 | value: 'Hello World' 82 | } 83 | ], 84 | footer: [ 85 | { 86 | value: 'canvas-editor', 87 | size: 12 88 | } 89 | ] 90 | }, 91 | {} 92 | ) 93 | ``` 94 | 95 | ## Step. 4: 配置编辑器 96 | 97 | 详见下一节 98 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | title: canvas-editor 5 | titleTemplate: rich text editor by canvas/svg 6 | 7 | hero: 8 | name: canvas-editor 9 | text: 基于canvas/svg的富文本编辑器 10 | actions: 11 | - theme: brand 12 | text: 开始 13 | link: /guide/start.html 14 | - theme: alt 15 | text: 在 GitHub 上查看 16 | link: https://github.com/Hufe921/canvas-editor 17 | 18 | features: 19 | - icon: 💡 20 | title: 所见即所得 21 | details: 类word可分页,所见即所得 22 | - icon: ⚡️ 23 | title: 轻量的数据结构 24 | details: 一段JSON即可呈现复杂样式 25 | - icon: 🛠️ 26 | title: 丰富的功能 27 | details: 支持常见富文本操作、表格、水印、控件、公式等 28 | - icon: 📦 29 | title: 使用方便 30 | details: 官方发布核心npm包,菜单栏、工具栏可自行维护 31 | - icon: 🔩 32 | title: 灵活的开发机制 33 | details: 通过接口可获取生命周期、事件回调、自定义右键菜单、快捷键等 34 | - icon: 🔑 35 | title: 完全类型化的API 36 | details: 灵活的 API 和完整的 TypeScript 类型 37 | --- 38 | 39 | 44 | -------------------------------------------------------------------------------- /docs/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/docs/public/favicon.png -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/favicon.png -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process' 2 | import fs from 'fs' 3 | import path from 'path' 4 | 5 | const pkgPath = path.resolve('package.json') 6 | 7 | // 校验包合法性 8 | fs.accessSync(path.resolve('dist'), fs.constants.F_OK) 9 | fs.accessSync(path.resolve('dist/canvas-editor.es.js'), fs.constants.F_OK) 10 | fs.accessSync(path.resolve('dist/canvas-editor.umd.js'), fs.constants.F_OK) 11 | 12 | // 缓存项目package.json 13 | const sourcePkg = fs.readFileSync(pkgPath, 'utf-8') 14 | 15 | // 删除无用属性 16 | const targetPkg = JSON.parse(sourcePkg) 17 | Reflect.deleteProperty(targetPkg, 'dependencies') 18 | Reflect.deleteProperty(targetPkg.scripts, 'postinstall') 19 | fs.writeFileSync(pkgPath, JSON.stringify(targetPkg, null, 2)) 20 | 21 | // 发布包 22 | try { 23 | execSync('npm publish') 24 | } catch (error) { 25 | throw new Error(error) 26 | } finally { 27 | // 还原 28 | fs.writeFileSync(pkgPath, sourcePkg) 29 | } 30 | -------------------------------------------------------------------------------- /scripts/verifyCommit.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { readFileSync } from 'fs' 3 | import path from 'path' 4 | 5 | const msgPath = path.resolve('.git/COMMIT_EDITMSG') 6 | const msg = readFileSync(msgPath, 'utf-8').trim() 7 | 8 | const commitRE = 9 | /^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release|improve)(\(.+\))?: .{1,50}/ 10 | 11 | if (!commitRE.test(msg)) { 12 | console.error( 13 | `invalid commit message format.\n\n` + 14 | ` Proper commit message format is required for automated changelog generation. Examples:\n\n` + 15 | ` feat: add page header\n` + 16 | ` fix: IME position error #155\n` 17 | ) 18 | process.exit(1) 19 | } -------------------------------------------------------------------------------- /src/assets/images/alignment.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/arrow-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/arrow-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/block.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/bold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/catalog.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/center.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/checkbox.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/codeblock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/color.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/control.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/date.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/format.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/highlight.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/hyperlink.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/italic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/justify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/latex.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/line-dash-dot-dot.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/line-dash-dot.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/line-dash-large-gap.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/line-dash-small-gap.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/line-dot.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/line-double.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/line-single.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/line-wavy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/page-break.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/page-mode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/page-scale-add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/page-scale-minus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/painter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/paper-direction.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/paper-margin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/paper-size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/print.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/radio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/redo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/request-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/row-margin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/separator.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/signature-undo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/signature.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/size-add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/size-minus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/strikeout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/subscript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/superscript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/title.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/trash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/underline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/undo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/watermark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/word-tool.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.2.1.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.2.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.2.2.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.3.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.3.0.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.3.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.3.1.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.5.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.5.0.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.5.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.5.1.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.6.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.6.0.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.6.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.6.1.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.7.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.7.0.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.7.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.7.1.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.7.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.7.2.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.7.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.7.3.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.7.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.7.4.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.7.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.7.6.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.7.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.7.7.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.8.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.8.0.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.8.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.8.5.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.8.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.8.6.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.8.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.8.7.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.8.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.8.8.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.9.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.9.0.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.9.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.9.1.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.9.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.9.2.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.9.23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.9.23.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.9.28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.9.28.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.9.29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.9.29.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.9.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.9.3.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.9.30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.9.30.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.9.32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.9.32.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.9.35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.9.35.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.9.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.9.4.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.9.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.9.5.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.9.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.9.6.png -------------------------------------------------------------------------------- /src/assets/snapshots/main_v0.9.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hufe921/canvas-editor/ed64e9b4574573d8520956ce1838ae85170bc8cb/src/assets/snapshots/main_v0.9.8.png -------------------------------------------------------------------------------- /src/editor/assets/css/block/block.css: -------------------------------------------------------------------------------- 1 | .ce-block-item { 2 | position: absolute; 3 | z-index: 0; 4 | overflow: hidden; 5 | border-radius: 8px; 6 | background-color: #ffffff; 7 | border: 1px solid rgb(235 236 240); 8 | } -------------------------------------------------------------------------------- /src/editor/assets/css/control/select.css: -------------------------------------------------------------------------------- 1 | .ce-select-control-popup { 2 | max-width: 160px; 3 | min-width: 69px; 4 | max-height: 225px; 5 | position: absolute; 6 | z-index: 1; 7 | border: 1px solid #e4e7ed; 8 | border-radius: 4px; 9 | background-color: #fff; 10 | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1); 11 | box-sizing: border-box; 12 | margin: 5px 0; 13 | overflow-y: auto; 14 | } 15 | 16 | .ce-select-control-popup ul { 17 | list-style: none; 18 | padding: 3px 0; 19 | margin: 0; 20 | box-sizing: border-box; 21 | } 22 | 23 | .ce-select-control-popup ul li { 24 | font-size: 13px; 25 | padding: 0 20px; 26 | position: relative; 27 | white-space: nowrap; 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | color: #666; 31 | height: 36px; 32 | line-height: 36px; 33 | box-sizing: border-box; 34 | cursor: pointer; 35 | } 36 | 37 | .ce-select-control-popup ul li:hover { 38 | background-color: #EEF2FD; 39 | } 40 | 41 | .ce-select-control-popup ul li.active { 42 | color: var(--COLOR-HOVER, #5175f4); 43 | font-weight: 700; 44 | } -------------------------------------------------------------------------------- /src/editor/assets/css/hyperlink/hyperlink.css: -------------------------------------------------------------------------------- 1 | .ce-hyperlink-popup { 2 | background: #fff; 3 | box-shadow: 0 2px 12px 0 rgb(98 107 132 / 20%); 4 | border-radius: 2px; 5 | color: #3d4757; 6 | padding: 12px 16px; 7 | position: absolute; 8 | z-index: 1; 9 | text-align: center; 10 | display: none; 11 | } 12 | 13 | .ce-hyperlink-popup a { 14 | min-width: 100px; 15 | max-width: 300px; 16 | font-size: 12px; 17 | display: inline-block; 18 | white-space: nowrap; 19 | overflow: hidden; 20 | text-overflow: ellipsis; 21 | cursor: pointer; 22 | text-decoration: none; 23 | border-bottom-width: 1px; 24 | border-bottom-style: solid; 25 | color: #0000ff; 26 | } -------------------------------------------------------------------------------- /src/editor/assets/css/index.css: -------------------------------------------------------------------------------- 1 | @import './control/select.css'; 2 | @import './date/datePicker.css'; 3 | @import './block/block.css'; 4 | @import './table/table.css'; 5 | @import './resizer/resizer.css'; 6 | @import './previewer/previewer.css'; 7 | @import './contextmenu/contextmenu.css'; 8 | @import './hyperlink/hyperlink.css'; 9 | @import './zone/zone.css'; 10 | 11 | .ce-inputarea { 12 | width: 100px; 13 | height: 30px; 14 | min-width: 0; 15 | min-height: 0; 16 | margin: 0; 17 | padding: 0; 18 | left: 0; 19 | top: 0; 20 | letter-spacing: 0; 21 | font-size: 12px; 22 | position: absolute; 23 | z-index: -1; 24 | outline: none; 25 | resize: none; 26 | border: none; 27 | overflow: hidden; 28 | color: transparent; 29 | user-select: none; 30 | caret-color: transparent; 31 | background-color: transparent; 32 | } 33 | 34 | .ce-cursor { 35 | width: 1px; 36 | height: 20px; 37 | left: 0; 38 | right: 0; 39 | position: absolute; 40 | outline: none; 41 | background-color: #000000; 42 | pointer-events: none; 43 | } 44 | 45 | .ce-cursor.ce-cursor--animation { 46 | animation-duration: 1s; 47 | animation-iteration-count: infinite; 48 | animation-name: cursorAnimation; 49 | } 50 | 51 | @keyframes cursorAnimation { 52 | from { 53 | opacity: 1 54 | } 55 | 56 | 13% { 57 | opacity: 0 58 | } 59 | 60 | 50% { 61 | opacity: 0 62 | } 63 | 64 | 63% { 65 | opacity: 1 66 | } 67 | 68 | to { 69 | opacity: 1 70 | } 71 | } 72 | 73 | .ce-float-image { 74 | position: absolute; 75 | opacity: 0.5; 76 | pointer-events: none; 77 | } -------------------------------------------------------------------------------- /src/editor/assets/css/resizer/resizer.css: -------------------------------------------------------------------------------- 1 | .ce-resizer-selection { 2 | position: absolute; 3 | border: 1px solid; 4 | pointer-events: none; 5 | } 6 | 7 | .ce-resizer-selection .resizer-handle { 8 | position: absolute; 9 | z-index: 9; 10 | width: 10px; 11 | height: 10px; 12 | box-shadow: 0 1px 4px 0 rgb(0 0 0 / 30%); 13 | border-radius: 5px; 14 | border: 2px solid #ffffff; 15 | box-sizing: border-box; 16 | pointer-events: initial; 17 | } 18 | 19 | .ce-resizer-selection .handle-0 { 20 | cursor: nw-resize; 21 | } 22 | 23 | .ce-resizer-selection .handle-1 { 24 | cursor: n-resize; 25 | } 26 | 27 | .ce-resizer-selection .handle-2 { 28 | cursor: ne-resize; 29 | } 30 | 31 | .ce-resizer-selection .handle-3 { 32 | cursor: e-resize; 33 | } 34 | 35 | .ce-resizer-selection .handle-4 { 36 | cursor: se-resize; 37 | } 38 | 39 | .ce-resizer-selection .handle-5 { 40 | cursor: s-resize; 41 | } 42 | 43 | .ce-resizer-selection .handle-6 { 44 | cursor: sw-resize; 45 | } 46 | 47 | .ce-resizer-selection .handle-7 { 48 | cursor: w-resize; 49 | } 50 | 51 | .ce-resizer-size-view { 52 | display: flex; 53 | align-items: center; 54 | height: 20px; 55 | white-space: nowrap; 56 | position: absolute; 57 | z-index: 9; 58 | top: -30px; 59 | left: 0; 60 | opacity: .9; 61 | background-color: #000000; 62 | padding: 0 5px; 63 | border-radius: 4px; 64 | } 65 | 66 | .ce-resizer-size-view span { 67 | color: #ffffff; 68 | font-size: 12px; 69 | } 70 | 71 | .ce-resizer-image { 72 | position: absolute; 73 | opacity: 0.5; 74 | } -------------------------------------------------------------------------------- /src/editor/assets/css/zone/zone.css: -------------------------------------------------------------------------------- 1 | .ce-zone-indicator>div { 2 | padding: 3px 6px; 3 | color: #000000; 4 | font-size: 12px; 5 | background: rgb(218 231 252); 6 | position: absolute; 7 | transform-origin: 0 0; 8 | } 9 | 10 | .ce-zone-indicator-border__top, 11 | .ce-zone-indicator-border__bottom, 12 | .ce-zone-indicator-border__left, 13 | .ce-zone-indicator-border__right { 14 | display: block; 15 | position: absolute; 16 | z-index: 0; 17 | } 18 | 19 | .ce-zone-indicator-border__top { 20 | border-top: 2px dashed rgb(238, 238, 238); 21 | } 22 | 23 | .ce-zone-indicator-border__bottom { 24 | border-top: 2px dashed rgb(238, 238, 238); 25 | width: 100%; 26 | } 27 | 28 | .ce-zone-indicator-border__left { 29 | border-left: 2px dashed rgb(238, 238, 238); 30 | } 31 | 32 | .ce-zone-indicator-border__right { 33 | border-right: 2px dashed rgb(238, 238, 238); 34 | } 35 | 36 | .ce-zone-tip { 37 | display: none; 38 | align-items: center; 39 | height: 30px; 40 | white-space: nowrap; 41 | position: fixed; 42 | opacity: .9; 43 | background-color: #000000; 44 | padding: 0 5px; 45 | border-radius: 4px; 46 | z-index: 9; 47 | transition: all .3s; 48 | outline: none; 49 | user-select: none; 50 | pointer-events: none; 51 | transform: translate(10px, 10px); 52 | } 53 | 54 | .ce-zone-tip.show { 55 | display: flex; 56 | } 57 | 58 | .ce-zone-tip span { 59 | color: #ffffff; 60 | font-size: 12px; 61 | } -------------------------------------------------------------------------------- /src/editor/assets/images/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/delete-col.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/delete-row-col.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/delete-row.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/delete-table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/image-change.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/image-download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/insert-bottom-row.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/insert-left-col.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/insert-right-col.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/insert-row-col.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/insert-top-row.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/merge-cancel-cell.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/merge-cell.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/original-size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/print.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/rotate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/submenu-dropdown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/table-border-all.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/table-border-dash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/table-border-empty.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/table-border-external.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/table-border-internal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/table-border-td-back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/table-border-td-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/table-border-td-forward.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/table-border-td-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/table-border-td-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/table-border-td-top.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/table-border-td.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/vertical-align-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/vertical-align-middle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/vertical-align-top.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/vertical-align.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/zoom-in.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/assets/images/zoom-out.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/core/actuator/Actuator.ts: -------------------------------------------------------------------------------- 1 | import { EventBusMap } from '../../interface/EventBus' 2 | import { Draw } from '../draw/Draw' 3 | import { EventBus } from '../event/eventbus/EventBus' 4 | import { positionContextChange } from './handlers/positionContextChange' 5 | 6 | export class Actuator { 7 | private draw: Draw 8 | private eventBus: EventBus 9 | 10 | constructor(draw: Draw) { 11 | this.draw = draw 12 | this.eventBus = draw.getEventBus() 13 | this.execute() 14 | } 15 | 16 | private execute() { 17 | this.eventBus.on('positionContextChange', payload => { 18 | positionContextChange(this.draw, payload) 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/editor/core/actuator/handlers/positionContextChange.ts: -------------------------------------------------------------------------------- 1 | import { IPositionContextChangePayload } from '../../../interface/Listener' 2 | import { Draw } from '../../draw/Draw' 3 | 4 | export function positionContextChange( 5 | draw: Draw, 6 | payload: IPositionContextChangePayload 7 | ) { 8 | const { value, oldValue } = payload 9 | // 表格工具移除 10 | if (oldValue.isTable && !value.isTable) { 11 | draw.getTableTool().dispose() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/editor/core/contextmenu/menus/controlMenus.ts: -------------------------------------------------------------------------------- 1 | import { INTERNAL_CONTEXT_MENU_KEY } from '../../../dataset/constant/ContextMenu' 2 | import { EditorMode } from '../../../dataset/enum/Editor' 3 | import { IRegisterContextMenu } from '../../../interface/contextmenu/ContextMenu' 4 | import { Command } from '../../command/Command' 5 | const { 6 | CONTROL: { DELETE } 7 | } = INTERNAL_CONTEXT_MENU_KEY 8 | 9 | export const controlMenus: IRegisterContextMenu[] = [ 10 | { 11 | key: DELETE, 12 | i18nPath: 'contextmenu.control.delete', 13 | when: payload => { 14 | return ( 15 | !payload.isReadonly && 16 | !payload.editorHasSelection && 17 | !!payload.startElement?.controlId && 18 | payload.options.mode !== EditorMode.FORM 19 | ) 20 | }, 21 | callback: (command: Command) => { 22 | command.executeRemoveControl() 23 | } 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /src/editor/core/contextmenu/menus/globalMenus.ts: -------------------------------------------------------------------------------- 1 | import { INTERNAL_CONTEXT_MENU_KEY } from '../../../dataset/constant/ContextMenu' 2 | import { IRegisterContextMenu } from '../../../interface/contextmenu/ContextMenu' 3 | import { isApple } from '../../../utils/ua' 4 | import { Command } from '../../command/Command' 5 | const { 6 | GLOBAL: { CUT, COPY, PASTE, SELECT_ALL, PRINT } 7 | } = INTERNAL_CONTEXT_MENU_KEY 8 | 9 | export const globalMenus: IRegisterContextMenu[] = [ 10 | { 11 | key: CUT, 12 | i18nPath: 'contextmenu.global.cut', 13 | shortCut: `${isApple ? '⌘' : 'Ctrl'} + X`, 14 | when: payload => { 15 | return !payload.isReadonly 16 | }, 17 | callback: (command: Command) => { 18 | command.executeCut() 19 | } 20 | }, 21 | { 22 | key: COPY, 23 | i18nPath: 'contextmenu.global.copy', 24 | shortCut: `${isApple ? '⌘' : 'Ctrl'} + C`, 25 | when: payload => { 26 | return payload.editorHasSelection || payload.isCrossRowCol 27 | }, 28 | callback: (command: Command) => { 29 | command.executeCopy() 30 | } 31 | }, 32 | { 33 | key: PASTE, 34 | i18nPath: 'contextmenu.global.paste', 35 | shortCut: `${isApple ? '⌘' : 'Ctrl'} + V`, 36 | when: payload => { 37 | return !payload.isReadonly && payload.editorTextFocus 38 | }, 39 | callback: (command: Command) => { 40 | command.executePaste() 41 | } 42 | }, 43 | { 44 | key: SELECT_ALL, 45 | i18nPath: 'contextmenu.global.selectAll', 46 | shortCut: `${isApple ? '⌘' : 'Ctrl'} + A`, 47 | when: payload => { 48 | return payload.editorTextFocus 49 | }, 50 | callback: (command: Command) => { 51 | command.executeSelectAll() 52 | } 53 | }, 54 | { 55 | isDivider: true 56 | }, 57 | { 58 | key: PRINT, 59 | i18nPath: 'contextmenu.global.print', 60 | icon: 'print', 61 | when: () => true, 62 | callback: (command: Command) => { 63 | command.executePrint() 64 | } 65 | } 66 | ] 67 | -------------------------------------------------------------------------------- /src/editor/core/contextmenu/menus/hyperlinkMenus.ts: -------------------------------------------------------------------------------- 1 | import { INTERNAL_CONTEXT_MENU_KEY } from '../../../dataset/constant/ContextMenu' 2 | import { ElementType } from '../../../dataset/enum/Element' 3 | import { 4 | IContextMenuContext, 5 | IRegisterContextMenu 6 | } from '../../../interface/contextmenu/ContextMenu' 7 | import { Command } from '../../command/Command' 8 | const { 9 | HYPERLINK: { DELETE, CANCEL, EDIT } 10 | } = INTERNAL_CONTEXT_MENU_KEY 11 | 12 | export const hyperlinkMenus: IRegisterContextMenu[] = [ 13 | { 14 | key: DELETE, 15 | i18nPath: 'contextmenu.hyperlink.delete', 16 | when: payload => { 17 | return ( 18 | !payload.isReadonly && 19 | payload.startElement?.type === ElementType.HYPERLINK 20 | ) 21 | }, 22 | callback: (command: Command) => { 23 | command.executeDeleteHyperlink() 24 | } 25 | }, 26 | { 27 | key: CANCEL, 28 | i18nPath: 'contextmenu.hyperlink.cancel', 29 | when: payload => { 30 | return ( 31 | !payload.isReadonly && 32 | payload.startElement?.type === ElementType.HYPERLINK 33 | ) 34 | }, 35 | callback: (command: Command) => { 36 | command.executeCancelHyperlink() 37 | } 38 | }, 39 | { 40 | key: EDIT, 41 | i18nPath: 'contextmenu.hyperlink.edit', 42 | when: payload => { 43 | return ( 44 | !payload.isReadonly && 45 | payload.startElement?.type === ElementType.HYPERLINK 46 | ) 47 | }, 48 | callback: (command: Command, context: IContextMenuContext) => { 49 | const url = window.prompt('编辑链接', context.startElement?.url) 50 | if (url) { 51 | command.executeEditHyperlink(url) 52 | } 53 | } 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /src/editor/core/draw/control/number/NumberControl.ts: -------------------------------------------------------------------------------- 1 | import { TextControl } from '../text/TextControl' 2 | 3 | export class NumberControl extends TextControl {} 4 | -------------------------------------------------------------------------------- /src/editor/core/draw/control/richtext/Border.ts: -------------------------------------------------------------------------------- 1 | import { DeepRequired } from '../../../../interface/Common' 2 | import { IEditorOption } from '../../../../interface/Editor' 3 | import { IElementFillRect } from '../../../../interface/Element' 4 | import { Draw } from '../../Draw' 5 | 6 | export class ControlBorder { 7 | protected borderRect: IElementFillRect 8 | private options: DeepRequired 9 | 10 | constructor(draw: Draw) { 11 | this.borderRect = this.clearBorderInfo() 12 | this.options = draw.getOptions() 13 | } 14 | 15 | public clearBorderInfo() { 16 | this.borderRect = { 17 | x: 0, 18 | y: 0, 19 | width: 0, 20 | height: 0 21 | } 22 | return this.borderRect 23 | } 24 | 25 | public recordBorderInfo(x: number, y: number, width: number, height: number) { 26 | const isFirstRecord = !this.borderRect.width 27 | if (isFirstRecord) { 28 | this.borderRect.x = x 29 | this.borderRect.y = y 30 | this.borderRect.height = height 31 | } 32 | this.borderRect.width += width 33 | } 34 | 35 | public render(ctx: CanvasRenderingContext2D) { 36 | if (!this.borderRect.width) return 37 | const { 38 | scale, 39 | control: { borderWidth, borderColor } 40 | } = this.options 41 | const { x, y, width, height } = this.borderRect 42 | ctx.save() 43 | ctx.translate(0, 1 * scale) 44 | ctx.lineWidth = borderWidth * scale 45 | ctx.strokeStyle = borderColor 46 | ctx.beginPath() 47 | ctx.rect(x, y, width, height) 48 | ctx.stroke() 49 | ctx.restore() 50 | this.clearBorderInfo() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/editor/core/draw/frame/LineNumber.ts: -------------------------------------------------------------------------------- 1 | import { LineNumberType } from '../../../dataset/enum/LineNumber' 2 | import { DeepRequired } from '../../../interface/Common' 3 | import { IEditorOption } from '../../../interface/Editor' 4 | import { Draw } from '../Draw' 5 | 6 | export class LineNumber { 7 | private draw: Draw 8 | private options: DeepRequired 9 | 10 | constructor(draw: Draw) { 11 | this.draw = draw 12 | this.options = draw.getOptions() 13 | } 14 | 15 | public render(ctx: CanvasRenderingContext2D, pageNo: number) { 16 | const { 17 | scale, 18 | lineNumber: { color, size, font, right, type } 19 | } = this.options 20 | const textParticle = this.draw.getTextParticle() 21 | const margins = this.draw.getMargins() 22 | const positionList = this.draw.getPosition().getOriginalMainPositionList() 23 | const pageRowList = this.draw.getPageRowList() 24 | const rowList = pageRowList[pageNo] 25 | ctx.save() 26 | ctx.fillStyle = color 27 | ctx.font = `${size * scale}px ${font}` 28 | for (let i = 0; i < rowList.length; i++) { 29 | const row = rowList[i] 30 | const { 31 | coordinate: { leftBottom } 32 | } = positionList[row.startIndex] 33 | const seq = type === LineNumberType.PAGE ? i + 1 : row.rowIndex + 1 34 | const textMetrics = textParticle.measureText(ctx, { 35 | value: `${seq}` 36 | }) 37 | const x = margins[3] - (textMetrics.width + right) * scale 38 | const y = leftBottom[1] - textMetrics.actualBoundingBoxAscent * scale 39 | ctx.fillText(`${seq}`, x, y) 40 | } 41 | ctx.restore() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/editor/core/draw/frame/PageBorder.ts: -------------------------------------------------------------------------------- 1 | import { DeepRequired } from '../../../interface/Common' 2 | import { IEditorOption } from '../../../interface/Editor' 3 | import { Draw } from '../Draw' 4 | import { Footer } from './Footer' 5 | import { Header } from './Header' 6 | 7 | export class PageBorder { 8 | private draw: Draw 9 | private header: Header 10 | private footer: Footer 11 | private options: DeepRequired 12 | 13 | constructor(draw: Draw) { 14 | this.draw = draw 15 | this.header = draw.getHeader() 16 | this.footer = draw.getFooter() 17 | this.options = draw.getOptions() 18 | } 19 | 20 | public render(ctx: CanvasRenderingContext2D) { 21 | const { 22 | scale, 23 | pageBorder: { color, lineWidth, padding } 24 | } = this.options 25 | ctx.save() 26 | ctx.translate(0.5, 0.5) 27 | ctx.strokeStyle = color 28 | ctx.lineWidth = lineWidth * scale 29 | const margins = this.draw.getMargins() 30 | // x:左边距 - 左距离正文距离 31 | const x = margins[3] - padding[3] * scale 32 | // y:页眉上边距 + 页眉高度 - 上距离正文距离 33 | const y = margins[0] + this.header.getExtraHeight() - padding[0] * scale 34 | // width:页面宽度 + 左右距离正文距离 35 | const width = this.draw.getInnerWidth() + (padding[1] + padding[3]) * scale 36 | // height:页面高度 - 正文起始位置 - 页脚高度 - 下边距 - 下距离正文距离 37 | const height = 38 | this.draw.getHeight() - 39 | y - 40 | this.footer.getExtraHeight() - 41 | margins[2] + 42 | padding[2] * scale 43 | ctx.rect(x, y, width, height) 44 | ctx.stroke() 45 | ctx.restore() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/editor/core/draw/particle/LineBreakParticle.ts: -------------------------------------------------------------------------------- 1 | import { DeepRequired } from '../../../interface/Common' 2 | import { IEditorOption } from '../../../interface/Editor' 3 | import { IRowElement } from '../../../interface/Row' 4 | import { Draw } from '../Draw' 5 | 6 | export class LineBreakParticle { 7 | private options: DeepRequired 8 | public static readonly WIDTH = 12 9 | public static readonly HEIGHT = 9 10 | public static readonly GAP = 3 // 距离左边间隙 11 | 12 | constructor(draw: Draw) { 13 | this.options = draw.getOptions() 14 | } 15 | 16 | public render( 17 | ctx: CanvasRenderingContext2D, 18 | element: IRowElement, 19 | x: number, 20 | y: number 21 | ) { 22 | const { 23 | scale, 24 | lineBreak: { color, lineWidth } 25 | } = this.options 26 | ctx.save() 27 | ctx.beginPath() 28 | // 换行符尺寸设置为9像素 29 | const top = y - (LineBreakParticle.HEIGHT * scale) / 2 30 | const left = x + element.metrics.width 31 | // 移动位置并设置缩放 32 | ctx.translate(left, top) 33 | ctx.scale(scale, scale) 34 | // 样式设置 35 | ctx.strokeStyle = color 36 | ctx.lineWidth = lineWidth 37 | ctx.lineCap = 'round' 38 | ctx.lineJoin = 'round' 39 | ctx.beginPath() 40 | // 回车折线 41 | ctx.moveTo(8, 0) 42 | ctx.lineTo(12, 0) 43 | ctx.lineTo(12, 6) 44 | ctx.lineTo(3, 6) 45 | // 箭头向上 46 | ctx.moveTo(3, 6) 47 | ctx.lineTo(6, 3) 48 | // 箭头向下 49 | ctx.moveTo(3, 6) 50 | ctx.lineTo(6, 9) 51 | ctx.stroke() 52 | ctx.closePath() 53 | ctx.restore() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/editor/core/draw/particle/PageBreakParticle.ts: -------------------------------------------------------------------------------- 1 | import { DeepRequired } from '../../../interface/Common' 2 | import { IEditorOption } from '../../../interface/Editor' 3 | import { IRowElement } from '../../../interface/Row' 4 | import { I18n } from '../../i18n/I18n' 5 | import { Draw } from '../Draw' 6 | 7 | export class PageBreakParticle { 8 | private draw: Draw 9 | private options: DeepRequired 10 | private i18n: I18n 11 | 12 | constructor(draw: Draw) { 13 | this.draw = draw 14 | this.options = draw.getOptions() 15 | this.i18n = draw.getI18n() 16 | } 17 | 18 | public render( 19 | ctx: CanvasRenderingContext2D, 20 | element: IRowElement, 21 | x: number, 22 | y: number 23 | ) { 24 | const { 25 | pageBreak: { font, fontSize, lineDash } 26 | } = this.options 27 | const displayName = this.i18n.t('pageBreak.displayName') 28 | const { scale, defaultRowMargin } = this.options 29 | const size = fontSize * scale 30 | const elementWidth = element.width! * scale 31 | const offsetY = 32 | this.draw.getDefaultBasicRowMarginHeight() * defaultRowMargin 33 | ctx.save() 34 | ctx.font = `${size}px ${font}` 35 | const textMeasure = ctx.measureText(displayName) 36 | const halfX = (elementWidth - textMeasure.width) / 2 37 | // 线段 38 | ctx.setLineDash(lineDash) 39 | ctx.translate(0, 0.5 + offsetY) 40 | ctx.beginPath() 41 | ctx.moveTo(x, y) 42 | ctx.lineTo(x + halfX, y) 43 | ctx.moveTo(x + halfX + textMeasure.width, y) 44 | ctx.lineTo(x + elementWidth, y) 45 | ctx.stroke() 46 | // 文字 47 | ctx.fillText( 48 | displayName, 49 | x + halfX, 50 | y + textMeasure.actualBoundingBoxAscent - size / 2 51 | ) 52 | ctx.restore() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/editor/core/draw/particle/SeparatorParticle.ts: -------------------------------------------------------------------------------- 1 | import { DeepRequired } from '../../../interface/Common' 2 | import { IEditorOption } from '../../../interface/Editor' 3 | import { IRowElement } from '../../../interface/Row' 4 | import { Draw } from '../Draw' 5 | 6 | export class SeparatorParticle { 7 | private options: DeepRequired 8 | 9 | constructor(draw: Draw) { 10 | this.options = draw.getOptions() 11 | } 12 | 13 | public render( 14 | ctx: CanvasRenderingContext2D, 15 | element: IRowElement, 16 | x: number, 17 | y: number 18 | ) { 19 | ctx.save() 20 | const { 21 | scale, 22 | separator: { lineWidth, strokeStyle } 23 | } = this.options 24 | ctx.lineWidth = lineWidth * scale 25 | ctx.strokeStyle = element.color || strokeStyle 26 | if (element.dashArray?.length) { 27 | ctx.setLineDash(element.dashArray) 28 | } 29 | const offsetY = Math.round(y) // 四舍五入避免绘制模糊 30 | ctx.translate(0, ctx.lineWidth / 2) 31 | ctx.beginPath() 32 | ctx.moveTo(x, offsetY) 33 | ctx.lineTo(x + element.width! * scale, offsetY) 34 | ctx.stroke() 35 | ctx.restore() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/editor/core/draw/particle/SubscriptParticle.ts: -------------------------------------------------------------------------------- 1 | import { IRowElement } from '../../../interface/Row' 2 | 3 | export class SubscriptParticle { 4 | // 向下偏移字高的一半 5 | public getOffsetY(element: IRowElement): number { 6 | return element.metrics.height / 2 7 | } 8 | 9 | public render( 10 | ctx: CanvasRenderingContext2D, 11 | element: IRowElement, 12 | x: number, 13 | y: number 14 | ) { 15 | ctx.save() 16 | ctx.font = element.style 17 | if (element.color) { 18 | ctx.fillStyle = element.color 19 | } 20 | ctx.fillText(element.value, x, y + this.getOffsetY(element)) 21 | ctx.restore() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/editor/core/draw/particle/SuperscriptParticle.ts: -------------------------------------------------------------------------------- 1 | import { IRowElement } from '../../../interface/Row' 2 | 3 | export class SuperscriptParticle { 4 | // 向上偏移字高的一半 5 | public getOffsetY(element: IRowElement): number { 6 | return -element.metrics.height / 2 7 | } 8 | 9 | public render( 10 | ctx: CanvasRenderingContext2D, 11 | element: IRowElement, 12 | x: number, 13 | y: number 14 | ) { 15 | ctx.save() 16 | ctx.font = element.style 17 | if (element.color) { 18 | ctx.fillStyle = element.color 19 | } 20 | ctx.fillText(element.value, x, y + this.getOffsetY(element)) 21 | ctx.restore() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/editor/core/draw/particle/block/modules/IFrameBlock.ts: -------------------------------------------------------------------------------- 1 | import { IRowElement } from '../../../../../interface/Row' 2 | 3 | export class IFrameBlock { 4 | public static readonly sandbox = ['allow-scripts', 'allow-same-origin'] 5 | private element: IRowElement 6 | 7 | constructor(element: IRowElement) { 8 | this.element = element 9 | } 10 | 11 | private _defineIframeProperties(iframeWindow: Window) { 12 | Object.defineProperties(iframeWindow, { 13 | // 禁止获取parent避免安全漏洞 14 | parent: { 15 | get: () => null 16 | }, 17 | // 用于区分上下文 18 | __POWERED_BY_CANVAS_EDITOR__: { 19 | get: () => true 20 | } 21 | }) 22 | } 23 | 24 | public render(blockItemContainer: HTMLDivElement) { 25 | const block = this.element.block! 26 | const iframe = document.createElement('iframe') 27 | iframe.setAttribute('data-id', this.element.id!) 28 | iframe.sandbox.add(...IFrameBlock.sandbox) 29 | iframe.style.border = 'none' 30 | iframe.style.width = '100%' 31 | iframe.style.height = '100%' 32 | if (block.iframeBlock?.src) { 33 | iframe.src = block.iframeBlock.src 34 | } else if (block.iframeBlock?.srcdoc) { 35 | iframe.srcdoc = block.iframeBlock.srcdoc 36 | } 37 | blockItemContainer.append(iframe) 38 | // 重新定义iframe上属性 39 | this._defineIframeProperties(iframe.contentWindow!) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/editor/core/draw/particle/block/modules/VideoBlock.ts: -------------------------------------------------------------------------------- 1 | import { IRowElement } from '../../../../../interface/Row' 2 | 3 | export class VideoBlock { 4 | private element: IRowElement 5 | 6 | constructor(element: IRowElement) { 7 | this.element = element 8 | } 9 | 10 | public render(blockItemContainer: HTMLDivElement) { 11 | const block = this.element.block! 12 | const video = document.createElement('video') 13 | video.style.width = '100%' 14 | video.style.height = '100%' 15 | video.style.objectFit = 'contain' 16 | video.src = block.videoBlock?.src || '' 17 | video.controls = true 18 | blockItemContainer.append(video) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/editor/core/draw/particle/latex/LaTexParticle.ts: -------------------------------------------------------------------------------- 1 | import { IElement } from '../../../../interface/Element' 2 | import { ImageParticle } from '../ImageParticle' 3 | import { LaTexSVG, LaTexUtils } from './utils/LaTexUtils' 4 | 5 | export class LaTexParticle extends ImageParticle { 6 | public static convertLaTextToSVG(laTex: string): LaTexSVG { 7 | return new LaTexUtils(laTex).svg({ 8 | SCALE_X: 10, 9 | SCALE_Y: 10, 10 | MARGIN_X: 0, 11 | MARGIN_Y: 0 12 | }) 13 | } 14 | 15 | public render( 16 | ctx: CanvasRenderingContext2D, 17 | element: IElement, 18 | x: number, 19 | y: number 20 | ) { 21 | const { scale } = this.options 22 | const width = element.width! * scale 23 | const height = element.height! * scale 24 | if (this.imageCache.has(element.value)) { 25 | const img = this.imageCache.get(element.value)! 26 | ctx.drawImage(img, x, y, width, height) 27 | } else { 28 | const laTexLoadPromise = new Promise((resolve, reject) => { 29 | const img = new Image() 30 | img.src = element.laTexSVG! 31 | img.onload = () => { 32 | ctx.drawImage(img, x, y, width, height) 33 | this.imageCache.set(element.value, img) 34 | resolve(element) 35 | } 36 | img.onerror = error => { 37 | reject(error) 38 | } 39 | }) 40 | this.addImageObserver(laTexLoadPromise) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/editor/core/draw/richtext/AbstractRichText.ts: -------------------------------------------------------------------------------- 1 | import { TextDecorationStyle } from '../../../dataset/enum/Text' 2 | import { IElementFillRect } from '../../../interface/Element' 3 | 4 | export abstract class AbstractRichText { 5 | protected fillRect: IElementFillRect 6 | protected fillColor?: string 7 | protected fillDecorationStyle?: TextDecorationStyle 8 | 9 | constructor() { 10 | this.fillRect = this.clearFillInfo() 11 | } 12 | 13 | public clearFillInfo() { 14 | this.fillColor = undefined 15 | this.fillDecorationStyle = undefined 16 | this.fillRect = { 17 | x: 0, 18 | y: 0, 19 | width: 0, 20 | height: 0 21 | } 22 | return this.fillRect 23 | } 24 | 25 | public recordFillInfo( 26 | ctx: CanvasRenderingContext2D, 27 | x: number, 28 | y: number, 29 | width: number, 30 | height?: number, 31 | color?: string, 32 | decorationStyle?: TextDecorationStyle 33 | ) { 34 | const isFirstRecord = !this.fillRect.width 35 | // 颜色不同时立即绘制 36 | if ( 37 | !isFirstRecord && 38 | (this.fillColor !== color || this.fillDecorationStyle !== decorationStyle) 39 | ) { 40 | this.render(ctx) 41 | this.clearFillInfo() 42 | // 重新记录 43 | this.recordFillInfo(ctx, x, y, width, height, color, decorationStyle) 44 | return 45 | } 46 | if (isFirstRecord) { 47 | this.fillRect.x = x 48 | this.fillRect.y = y 49 | } 50 | if (height && this.fillRect.height < height) { 51 | this.fillRect.height = height 52 | } 53 | this.fillRect.width += width 54 | this.fillColor = color 55 | this.fillDecorationStyle = decorationStyle 56 | } 57 | 58 | public abstract render(ctx: CanvasRenderingContext2D): void 59 | } 60 | -------------------------------------------------------------------------------- /src/editor/core/draw/richtext/Highlight.ts: -------------------------------------------------------------------------------- 1 | import { AbstractRichText } from './AbstractRichText' 2 | import { IEditorOption } from '../../../interface/Editor' 3 | import { Draw } from '../Draw' 4 | 5 | export class Highlight extends AbstractRichText { 6 | private options: Required 7 | 8 | constructor(draw: Draw) { 9 | super() 10 | this.options = draw.getOptions() 11 | } 12 | 13 | public render(ctx: CanvasRenderingContext2D) { 14 | if (!this.fillRect.width) return 15 | const { highlightAlpha } = this.options 16 | const { x, y, width, height } = this.fillRect 17 | ctx.save() 18 | ctx.globalAlpha = highlightAlpha 19 | ctx.fillStyle = this.fillColor! 20 | ctx.fillRect(x, y, width, height) 21 | ctx.restore() 22 | this.clearFillInfo() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/editor/core/draw/richtext/Strikeout.ts: -------------------------------------------------------------------------------- 1 | import { AbstractRichText } from './AbstractRichText' 2 | import { IEditorOption } from '../../../interface/Editor' 3 | import { Draw } from '../Draw' 4 | 5 | export class Strikeout extends AbstractRichText { 6 | private options: Required 7 | 8 | constructor(draw: Draw) { 9 | super() 10 | this.options = draw.getOptions() 11 | } 12 | 13 | public render(ctx: CanvasRenderingContext2D) { 14 | if (!this.fillRect.width) return 15 | const { scale, strikeoutColor } = this.options 16 | const { x, y, width } = this.fillRect 17 | ctx.save() 18 | ctx.lineWidth = scale 19 | ctx.strokeStyle = strikeoutColor 20 | const adjustY = y + 0.5 // 从1处渲染,避免线宽度等于3 21 | ctx.beginPath() 22 | ctx.moveTo(x, adjustY) 23 | ctx.lineTo(x + width, adjustY) 24 | ctx.stroke() 25 | ctx.restore() 26 | this.clearFillInfo() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/editor/core/event/eventbus/EventBus.ts: -------------------------------------------------------------------------------- 1 | export class EventBus { 2 | private eventHub: Map> 3 | 4 | constructor() { 5 | this.eventHub = new Map() 6 | } 7 | 8 | public on( 9 | eventName: K, 10 | callback: EventMap[K] 11 | ) { 12 | if (!eventName || typeof callback !== 'function') return 13 | const eventSet = this.eventHub.get(eventName) || new Set() 14 | eventSet.add(callback) 15 | this.eventHub.set(eventName, eventSet) 16 | } 17 | 18 | public emit( 19 | eventName: K, 20 | payload?: EventMap[K] extends (payload: infer P) => void ? P : never 21 | ) { 22 | if (!eventName) return 23 | const callBackSet = this.eventHub.get(eventName) 24 | if (!callBackSet) return 25 | if (callBackSet.size === 1) { 26 | const callBack = [...callBackSet] 27 | return callBack[0](payload) 28 | } 29 | callBackSet.forEach(callBack => callBack(payload)) 30 | } 31 | 32 | public off( 33 | eventName: K, 34 | callback: EventMap[K] 35 | ) { 36 | if (!eventName || typeof callback !== 'function') return 37 | const callBackSet = this.eventHub.get(eventName) 38 | if (!callBackSet) return 39 | callBackSet.delete(callback) 40 | } 41 | 42 | public isSubscribe(eventName: K): boolean { 43 | const eventSet = this.eventHub.get(eventName) 44 | return !!eventSet && eventSet.size > 0 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/editor/core/event/handlers/composition.ts: -------------------------------------------------------------------------------- 1 | import { CanvasEvent } from '../CanvasEvent' 2 | import { input, removeComposingInput } from './input' 3 | 4 | function compositionstart(host: CanvasEvent) { 5 | host.isComposing = true 6 | } 7 | 8 | function compositionend(host: CanvasEvent, evt: CompositionEvent) { 9 | host.isComposing = false 10 | // 处理输入框关闭 11 | const draw = host.getDraw() 12 | // 不存在值:删除合成输入 13 | if (!evt.data) { 14 | removeComposingInput(host) 15 | const rangeManager = draw.getRange() 16 | const { endIndex: curIndex } = rangeManager.getRange() 17 | draw.render({ 18 | curIndex, 19 | isSubmitHistory: false 20 | }) 21 | } else { 22 | // 存在值:无法触发input事件需手动检测并触发渲染 23 | setTimeout(() => { 24 | if (host.compositionInfo) { 25 | input(evt.data, host) 26 | } 27 | }, 1) // 如果为0,火狐浏览器会在input事件之前执行导致重复输入 28 | } 29 | // 移除代理输入框数据 30 | const cursor = draw.getCursor() 31 | cursor.clearAgentDomValue() 32 | } 33 | 34 | export default { 35 | compositionstart, 36 | compositionend 37 | } 38 | -------------------------------------------------------------------------------- /src/editor/core/event/handlers/cut.ts: -------------------------------------------------------------------------------- 1 | import { writeElementList } from '../../../utils/clipboard' 2 | import { CanvasEvent } from '../CanvasEvent' 3 | 4 | export function cut(host: CanvasEvent) { 5 | const draw = host.getDraw() 6 | const rangeManager = draw.getRange() 7 | const { startIndex, endIndex } = rangeManager.getRange() 8 | if (!~startIndex && !~startIndex) return 9 | if (draw.isReadonly() || !rangeManager.getIsCanInput()) return 10 | 11 | const elementList = draw.getElementList() 12 | let start = startIndex 13 | let end = endIndex 14 | // 无选区则剪切一行 15 | if (startIndex === endIndex) { 16 | const position = draw.getPosition() 17 | const positionList = position.getPositionList() 18 | const startPosition = positionList[startIndex] 19 | const curRowNo = startPosition.rowNo 20 | const curPageNo = startPosition.pageNo 21 | const cutElementIndexList: number[] = [] 22 | for (let p = 0; p < positionList.length; p++) { 23 | const position = positionList[p] 24 | if (position.pageNo > curPageNo) break 25 | if (position.pageNo === curPageNo && position.rowNo === curRowNo) { 26 | cutElementIndexList.push(p) 27 | } 28 | } 29 | const firstElementIndex = cutElementIndexList[0] - 1 30 | start = firstElementIndex < 0 ? 0 : firstElementIndex 31 | end = cutElementIndexList[cutElementIndexList.length - 1] 32 | } 33 | const options = draw.getOptions() 34 | // 写入粘贴板 35 | writeElementList(elementList.slice(start + 1, end + 1), options) 36 | const control = draw.getControl() 37 | let curIndex: number 38 | if (control.getActiveControl() && control.getIsRangeWithinControl()) { 39 | curIndex = control.cut() 40 | control.emitControlContentChange() 41 | } else { 42 | draw.spliceElementList(elementList, start + 1, end - start) 43 | curIndex = start 44 | } 45 | rangeManager.setRange(curIndex, curIndex) 46 | draw.render({ curIndex }) 47 | } 48 | -------------------------------------------------------------------------------- /src/editor/core/event/handlers/drop.ts: -------------------------------------------------------------------------------- 1 | import { IOverrideResult } from '../../override/Override' 2 | import { CanvasEvent } from '../CanvasEvent' 3 | import { pasteImage } from './paste' 4 | 5 | export function drop(evt: DragEvent, host: CanvasEvent) { 6 | const draw = host.getDraw() 7 | // 自定义拖放事件 8 | const { drop } = draw.getOverride() 9 | if (drop) { 10 | const overrideResult = drop(evt) 11 | // 默认阻止默认事件 12 | if ((overrideResult)?.preventDefault !== false) return 13 | } 14 | evt.preventDefault() 15 | const data = evt.dataTransfer?.getData('text') 16 | if (data) { 17 | host.input(data) 18 | } else { 19 | const files = evt.dataTransfer?.files 20 | if (!files) return 21 | for (let i = 0; i < files.length; i++) { 22 | const file = files[i] 23 | if (file.type.startsWith('image')) { 24 | pasteImage(host, file) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/editor/core/event/handlers/keydown/tab.ts: -------------------------------------------------------------------------------- 1 | import { EDITOR_ELEMENT_STYLE_ATTR } from '../../../../dataset/constant/Element' 2 | import { ElementType } from '../../../../dataset/enum/Element' 3 | import { MoveDirection } from '../../../../dataset/enum/Observer' 4 | import { IElement } from '../../../../interface/Element' 5 | import { pickObject } from '../../../../utils' 6 | import { formatElementContext } from '../../../../utils/element' 7 | import { CanvasEvent } from '../../CanvasEvent' 8 | 9 | export function tab(evt: KeyboardEvent, host: CanvasEvent) { 10 | const draw = host.getDraw() 11 | const isReadonly = draw.isReadonly() 12 | if (isReadonly) return 13 | evt.preventDefault() 14 | // 在控件上下文时,tab键控制控件之间移动 15 | const control = draw.getControl() 16 | const activeControl = control.getActiveControl() 17 | if (activeControl && control.getIsRangeWithinControl()) { 18 | control.initNextControl({ 19 | direction: evt.shiftKey ? MoveDirection.UP : MoveDirection.DOWN 20 | }) 21 | } else { 22 | const rangeManager = draw.getRange() 23 | const elementList = draw.getElementList() 24 | const { startIndex, endIndex } = rangeManager.getRange() 25 | // 插入tab符 26 | const anchorStyle = rangeManager.getRangeAnchorStyle(elementList, endIndex) 27 | // 仅复制样式 28 | const copyStyle = anchorStyle 29 | ? pickObject(anchorStyle, EDITOR_ELEMENT_STYLE_ATTR) 30 | : null 31 | const tabElement: IElement = { 32 | ...copyStyle, 33 | type: ElementType.TAB, 34 | value: '' 35 | } 36 | formatElementContext(elementList, [tabElement], startIndex, { 37 | editorOptions: draw.getOptions() 38 | }) 39 | draw.insertElementList([tabElement]) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/editor/core/event/handlers/mouseleave.ts: -------------------------------------------------------------------------------- 1 | import { CanvasEvent } from '../CanvasEvent' 2 | 3 | export function mouseleave(evt: MouseEvent, host: CanvasEvent) { 4 | const draw = host.getDraw() 5 | // 鼠标移出页面时选区禁用 6 | if (!draw.getOptions().pageOuterSelectionDisable) return 7 | // 是否还在canvas内部 8 | const pageContainer = draw.getPageContainer() 9 | const { x, y, width, height } = pageContainer.getBoundingClientRect() 10 | if (evt.x >= x && evt.x <= x + width && evt.y >= y && evt.y <= y + height) { 11 | return 12 | } 13 | host.setIsAllowSelection(false) 14 | } 15 | -------------------------------------------------------------------------------- /src/editor/core/history/HistoryManager.ts: -------------------------------------------------------------------------------- 1 | import { Draw } from '../draw/Draw' 2 | 3 | export class HistoryManager { 4 | private undoStack: Array = [] 5 | private redoStack: Array = [] 6 | private maxRecordCount: number 7 | 8 | constructor(draw: Draw) { 9 | // 忽略第一次历史记录 10 | this.maxRecordCount = draw.getOptions().historyMaxRecordCount + 1 11 | } 12 | 13 | public undo() { 14 | if (this.undoStack.length > 1) { 15 | const pop = this.undoStack.pop()! 16 | this.redoStack.push(pop) 17 | if (this.undoStack.length) { 18 | this.undoStack[this.undoStack.length - 1]() 19 | } 20 | } 21 | } 22 | 23 | public redo() { 24 | if (this.redoStack.length) { 25 | const pop = this.redoStack.pop()! 26 | this.undoStack.push(pop) 27 | pop() 28 | } 29 | } 30 | 31 | public execute(fn: Function) { 32 | this.undoStack.push(fn) 33 | if (this.redoStack.length) { 34 | this.redoStack = [] 35 | } 36 | while (this.undoStack.length > this.maxRecordCount) { 37 | this.undoStack.shift() 38 | } 39 | } 40 | 41 | public isCanUndo(): boolean { 42 | return this.undoStack.length > 1 43 | } 44 | 45 | public isCanRedo(): boolean { 46 | return !!this.redoStack.length 47 | } 48 | 49 | public isStackEmpty(): boolean { 50 | return !this.undoStack.length && !this.redoStack.length 51 | } 52 | 53 | public recovery() { 54 | this.undoStack = [] 55 | this.redoStack = [] 56 | } 57 | 58 | public popUndo() { 59 | return this.undoStack.pop() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/editor/core/i18n/I18n.ts: -------------------------------------------------------------------------------- 1 | import { ILang } from '../../interface/i18n/I18n' 2 | import zhCN from './lang/zh-CN.json' 3 | import en from './lang/en.json' 4 | import { mergeObject } from '../../utils' 5 | import { DeepPartial } from '../../interface/Common' 6 | 7 | export class I18n { 8 | private langMap: Map = new Map([ 9 | ['zhCN', zhCN], 10 | ['en', en] 11 | ]) 12 | 13 | private currentLocale = 'zhCN' 14 | 15 | public registerLangMap(locale: string, lang: DeepPartial) { 16 | const sourceLang = this.langMap.get(locale) 17 | this.langMap.set(locale, mergeObject(sourceLang || zhCN, lang)) 18 | } 19 | 20 | public getLocale(): string { 21 | return this.currentLocale 22 | } 23 | 24 | public setLocale(locale: string) { 25 | this.currentLocale = locale 26 | } 27 | 28 | public getLang(): ILang { 29 | return this.langMap.get(this.currentLocale) || zhCN 30 | } 31 | 32 | public t(path: string): string { 33 | const keyList = path.split('.') 34 | let value = '' 35 | let item = this.getLang() 36 | for (let k = 0; k < keyList.length; k++) { 37 | const key = keyList[k] 38 | const currentValue = Reflect.get(item, key) 39 | if (currentValue) { 40 | value = item = currentValue 41 | } else { 42 | return '' 43 | } 44 | } 45 | return value 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/editor/core/listener/Listener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IContentChange, 3 | IControlChange, 4 | IControlContentChange, 5 | IIntersectionPageNoChange, 6 | IPageModeChange, 7 | IPageScaleChange, 8 | IPageSizeChange, 9 | IRangeStyleChange, 10 | ISaved, 11 | IVisiblePageNoListChange, 12 | IZoneChange 13 | } from '../../interface/Listener' 14 | 15 | export class Listener { 16 | public rangeStyleChange: IRangeStyleChange | null 17 | public visiblePageNoListChange: IVisiblePageNoListChange | null 18 | public intersectionPageNoChange: IIntersectionPageNoChange | null 19 | public pageSizeChange: IPageSizeChange | null 20 | public pageScaleChange: IPageScaleChange | null 21 | public saved: ISaved | null 22 | public contentChange: IContentChange | null 23 | public controlChange: IControlChange | null 24 | public controlContentChange: IControlContentChange | null 25 | public pageModeChange: IPageModeChange | null 26 | public zoneChange: IZoneChange | null 27 | 28 | constructor() { 29 | this.rangeStyleChange = null 30 | this.visiblePageNoListChange = null 31 | this.intersectionPageNoChange = null 32 | this.pageSizeChange = null 33 | this.pageScaleChange = null 34 | this.saved = null 35 | this.contentChange = null 36 | this.controlChange = null 37 | this.controlContentChange = null 38 | this.pageModeChange = null 39 | this.zoneChange = null 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/editor/core/observer/ImageObserver.ts: -------------------------------------------------------------------------------- 1 | export class ImageObserver { 2 | private promiseList: Promise[] 3 | 4 | constructor() { 5 | this.promiseList = [] 6 | } 7 | 8 | public add(payload: Promise) { 9 | this.promiseList.push(payload) 10 | } 11 | 12 | public clearAll() { 13 | this.promiseList = [] 14 | } 15 | 16 | public allSettled() { 17 | return Promise.allSettled(this.promiseList) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/editor/core/observer/MouseObserver.ts: -------------------------------------------------------------------------------- 1 | import { EventBusMap } from '../../interface/EventBus' 2 | import { Draw } from '../draw/Draw' 3 | import { EventBus } from '../event/eventbus/EventBus' 4 | 5 | export class MouseObserver { 6 | private draw: Draw 7 | private eventBus: EventBus 8 | private pageContainer: HTMLDivElement 9 | constructor(draw: Draw) { 10 | this.draw = draw 11 | this.eventBus = this.draw.getEventBus() 12 | this.pageContainer = this.draw.getPageContainer() 13 | this.pageContainer.addEventListener('mousemove', this._mousemove.bind(this)) 14 | this.pageContainer.addEventListener( 15 | 'mouseenter', 16 | this._mouseenter.bind(this) 17 | ) 18 | this.pageContainer.addEventListener( 19 | 'mouseleave', 20 | this._mouseleave.bind(this) 21 | ) 22 | this.pageContainer.addEventListener('mousedown', this._mousedown.bind(this)) 23 | this.pageContainer.addEventListener('mouseup', this._mouseup.bind(this)) 24 | this.pageContainer.addEventListener('click', this._click.bind(this)) 25 | } 26 | 27 | private _mousemove(evt: MouseEvent) { 28 | if (!this.eventBus.isSubscribe('mousemove')) return 29 | this.eventBus.emit('mousemove', evt) 30 | } 31 | 32 | private _mouseenter(evt: MouseEvent) { 33 | if (!this.eventBus.isSubscribe('mouseenter')) return 34 | this.eventBus.emit('mouseenter', evt) 35 | } 36 | 37 | private _mouseleave(evt: MouseEvent) { 38 | if (!this.eventBus.isSubscribe('mouseleave')) return 39 | this.eventBus.emit('mouseleave', evt) 40 | } 41 | 42 | private _mousedown(evt: MouseEvent) { 43 | if (!this.eventBus.isSubscribe('mousedown')) return 44 | this.eventBus.emit('mousedown', evt) 45 | } 46 | 47 | private _mouseup(evt: MouseEvent) { 48 | if (!this.eventBus.isSubscribe('mouseup')) return 49 | this.eventBus.emit('mouseup', evt) 50 | } 51 | 52 | private _click(evt: MouseEvent) { 53 | if (!this.eventBus.isSubscribe('click')) return 54 | this.eventBus.emit('click', evt) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/editor/core/override/Override.ts: -------------------------------------------------------------------------------- 1 | export interface IOverrideResult { 2 | preventDefault?: boolean 3 | } 4 | 5 | export class Override { 6 | public paste: 7 | | ((evt?: ClipboardEvent) => unknown | IOverrideResult) 8 | | undefined 9 | public copy: (() => unknown | IOverrideResult) | undefined 10 | public drop: ((evt: DragEvent) => unknown | IOverrideResult) | undefined 11 | } 12 | -------------------------------------------------------------------------------- /src/editor/core/plugin/Plugin.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../..' 2 | import { PluginFunction } from '../../interface/Plugin' 3 | 4 | export class Plugin { 5 | private editor: Editor 6 | 7 | constructor(editor: Editor) { 8 | this.editor = editor 9 | } 10 | 11 | public use( 12 | pluginFunction: PluginFunction, 13 | options?: Options 14 | ) { 15 | pluginFunction(this.editor, options) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/editor/core/register/Register.ts: -------------------------------------------------------------------------------- 1 | import { IRegisterContextMenu } from '../../interface/contextmenu/ContextMenu' 2 | import { IRegisterShortcut } from '../../interface/shortcut/Shortcut' 3 | import { ContextMenu } from '../contextmenu/ContextMenu' 4 | import { Shortcut } from '../shortcut/Shortcut' 5 | import { I18n } from '../i18n/I18n' 6 | import { ILang } from '../../interface/i18n/I18n' 7 | import { DeepPartial } from '../../interface/Common' 8 | 9 | interface IRegisterPayload { 10 | contextMenu: ContextMenu 11 | shortcut: Shortcut 12 | i18n: I18n 13 | } 14 | 15 | export class Register { 16 | public contextMenuList: (payload: IRegisterContextMenu[]) => void 17 | public getContextMenuList: () => IRegisterContextMenu[] 18 | public shortcutList: (payload: IRegisterShortcut[]) => void 19 | public langMap: (locale: string, lang: DeepPartial) => void 20 | 21 | constructor(payload: IRegisterPayload) { 22 | const { contextMenu, shortcut, i18n } = payload 23 | this.contextMenuList = contextMenu.registerContextMenuList.bind(contextMenu) 24 | this.getContextMenuList = contextMenu.getContextMenuList.bind(contextMenu) 25 | this.shortcutList = shortcut.registerShortcutList.bind(shortcut) 26 | this.langMap = i18n.registerLangMap.bind(i18n) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/editor/core/shortcut/keys/listKeys.ts: -------------------------------------------------------------------------------- 1 | import { Command, ListStyle, ListType } from '../../..' 2 | import { KeyMap } from '../../../dataset/enum/KeyMap' 3 | import { IRegisterShortcut } from '../../../interface/shortcut/Shortcut' 4 | 5 | export const listKeys: IRegisterShortcut[] = [ 6 | { 7 | key: KeyMap.I, 8 | shift: true, 9 | mod: true, 10 | callback: (command: Command) => { 11 | command.executeList(ListType.UL, ListStyle.DISC) 12 | } 13 | }, 14 | { 15 | key: KeyMap.U, 16 | shift: true, 17 | mod: true, 18 | callback: (command: Command) => { 19 | command.executeList(ListType.OL) 20 | } 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /src/editor/core/shortcut/keys/titleKeys.ts: -------------------------------------------------------------------------------- 1 | import { Command, TitleLevel } from '../../..' 2 | import { KeyMap } from '../../../dataset/enum/KeyMap' 3 | import { IRegisterShortcut } from '../../../interface/shortcut/Shortcut' 4 | 5 | export const titleKeys: IRegisterShortcut[] = [ 6 | { 7 | key: KeyMap.ZERO, 8 | alt: true, 9 | ctrl: true, 10 | callback: (command: Command) => { 11 | command.executeTitle(null) 12 | } 13 | }, 14 | { 15 | key: KeyMap.ONE, 16 | alt: true, 17 | ctrl: true, 18 | callback: (command: Command) => { 19 | command.executeTitle(TitleLevel.FIRST) 20 | } 21 | }, 22 | { 23 | key: KeyMap.TWO, 24 | alt: true, 25 | ctrl: true, 26 | callback: (command: Command) => { 27 | command.executeTitle(TitleLevel.SECOND) 28 | } 29 | }, 30 | { 31 | key: KeyMap.THREE, 32 | alt: true, 33 | ctrl: true, 34 | callback: (command: Command) => { 35 | command.executeTitle(TitleLevel.THIRD) 36 | } 37 | }, 38 | { 39 | key: KeyMap.FOUR, 40 | alt: true, 41 | ctrl: true, 42 | callback: (command: Command) => { 43 | command.executeTitle(TitleLevel.FOURTH) 44 | } 45 | }, 46 | { 47 | key: KeyMap.FIVE, 48 | alt: true, 49 | ctrl: true, 50 | callback: (command: Command) => { 51 | command.executeTitle(TitleLevel.FIFTH) 52 | } 53 | }, 54 | { 55 | key: KeyMap.SIX, 56 | alt: true, 57 | ctrl: true, 58 | callback: (command: Command) => { 59 | command.executeTitle(TitleLevel.SIXTH) 60 | } 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /src/editor/core/worker/works/group.ts: -------------------------------------------------------------------------------- 1 | import { IElement } from '../../../interface/Element' 2 | 3 | enum ElementType { 4 | TABLE = 'table' 5 | } 6 | 7 | function getGroupIds(elementList: IElement[]): string[] { 8 | const groupIds: string[] = [] 9 | for (const element of elementList) { 10 | if (element.type === ElementType.TABLE) { 11 | const trList = element.trList! 12 | for (let r = 0; r < trList.length; r++) { 13 | const tr = trList[r] 14 | for (let d = 0; d < tr.tdList.length; d++) { 15 | const td = tr.tdList[d] 16 | groupIds.push(...getGroupIds(td.value)) 17 | } 18 | } 19 | } 20 | if (!element.groupIds) continue 21 | for (const groupId of element.groupIds) { 22 | if (!groupIds.includes(groupId)) { 23 | groupIds.push(groupId) 24 | } 25 | } 26 | } 27 | return groupIds 28 | } 29 | 30 | onmessage = evt => { 31 | const elementList = evt.data 32 | const groupIds = getGroupIds(elementList) 33 | postMessage(groupIds) 34 | } 35 | -------------------------------------------------------------------------------- /src/editor/core/worker/works/value.ts: -------------------------------------------------------------------------------- 1 | import { IGetValueOption } from '../../../interface/Draw' 2 | import { IEditorData } from '../../../interface/Editor' 3 | import { zipElementList } from '../../../utils/element' 4 | 5 | interface IGetValueWorkerOption { 6 | data: Required 7 | options: IGetValueOption 8 | } 9 | 10 | onmessage = evt => { 11 | const payload = evt.data 12 | const { options, data } = payload 13 | const { extraPickAttrs = [] } = options || {} 14 | 15 | const editorData: IEditorData = { 16 | header: zipElementList(data.header, { 17 | extraPickAttrs, 18 | isClone: false 19 | }), 20 | main: zipElementList(data.main, { 21 | extraPickAttrs, 22 | isClassifyArea: true, 23 | isClone: false 24 | }), 25 | footer: zipElementList(data.footer, { 26 | extraPickAttrs, 27 | isClone: false 28 | }) 29 | } 30 | 31 | postMessage(editorData) 32 | } 33 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Background.ts: -------------------------------------------------------------------------------- 1 | import { IBackgroundOption } from '../../interface/Background' 2 | import { BackgroundRepeat, BackgroundSize } from '../enum/Background' 3 | 4 | export const defaultBackground: Readonly> = { 5 | color: '#FFFFFF', 6 | image: '', 7 | size: BackgroundSize.COVER, 8 | repeat: BackgroundRepeat.NO_REPEAT, 9 | applyPageNumbers: [] 10 | } 11 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Badge.ts: -------------------------------------------------------------------------------- 1 | import { IBadgeOption } from '../../interface/Badge' 2 | 3 | export const defaultBadgeOption: Readonly> = { 4 | top: 0, 5 | left: 5 6 | } 7 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Checkbox.ts: -------------------------------------------------------------------------------- 1 | import { ICheckboxOption } from '../../interface/Checkbox' 2 | import { VerticalAlign } from '../enum/VerticalAlign' 3 | 4 | export const defaultCheckboxOption: Readonly> = { 5 | width: 14, 6 | height: 14, 7 | gap: 5, 8 | lineWidth: 1, 9 | fillStyle: '#5175f4', 10 | strokeStyle: '#ffffff', 11 | verticalAlign: VerticalAlign.BOTTOM 12 | } 13 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Common.ts: -------------------------------------------------------------------------------- 1 | import { MaxHeightRatio } from '../enum/Common' 2 | 3 | export const ZERO = '\u200B' 4 | export const WRAP = '\n' 5 | export const HORIZON_TAB = '\t' 6 | export const NBSP = '\u0020' 7 | export const NON_BREAKING_SPACE = ' ' 8 | export const PUNCTUATION_LIST = [ 9 | '·', 10 | '、', 11 | ':', 12 | ':', 13 | ',', 14 | ',', 15 | '.', 16 | '。', 17 | ';', 18 | ';', 19 | '?', 20 | '?', 21 | '!', 22 | '!' 23 | ] 24 | 25 | export const maxHeightRadioMapping: Record = { 26 | [MaxHeightRatio.HALF]: 1 / 2, 27 | [MaxHeightRatio.ONE_THIRD]: 1 / 3, 28 | [MaxHeightRatio.QUARTER]: 1 / 4 29 | } 30 | 31 | export const LETTER_CLASS = { 32 | ENGLISH: 'A-Za-z', 33 | SPANISH: 'A-Za-zÁÉÍÓÚáéíóúÑñÜü', 34 | FRENCH: 'A-Za-zÀÂÇàâçÉéÈèÊêËëÎîÏïÔôÙùÛûŸÿ', 35 | GERMAN: 'A-Za-zÄäÖöÜüß', 36 | RUSSIAN: 'А-Яа-яЁё', 37 | PORTUGUESE: 'A-Za-zÁÉÍÓÚáéíóúÃÕãõÇç', 38 | ITALIAN: 'A-Za-zÀàÈèÉéÌìÍíÎîÓóÒòÙù', 39 | DUTCH: 'A-Za-zÀàÁáÂâÄäÈèÉéÊêËëÌìÍíÎîÏïÓóÒòÔôÖöÙùÛûÜü', 40 | SWEDISH: 'A-Za-zÅåÄäÖö', 41 | GREEK: 'ΑαΒβΓγΔδΕεΖζΗηΘθΙιΚκΛλΜμΝνΞξΟοΠπΡρΣσςΤτΥυΦφΧχΨψΩω' 42 | } 43 | 44 | export const METRICS_BASIS_TEXT = '日' 45 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Control.ts: -------------------------------------------------------------------------------- 1 | import { IControlOption } from '../../interface/Control' 2 | 3 | export const defaultControlOption: Readonly> = { 4 | placeholderColor: '#9c9b9b', 5 | bracketColor: '#000000', 6 | prefix: '{', 7 | postfix: '}', 8 | borderWidth: 1, 9 | borderColor: '#000000', 10 | activeBackgroundColor: '', 11 | disabledBackgroundColor: '', 12 | existValueBackgroundColor: '', 13 | noValueBackgroundColor: '' 14 | } 15 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Cursor.ts: -------------------------------------------------------------------------------- 1 | import { ICursorOption } from '../../interface/Cursor' 2 | 3 | export const CURSOR_AGENT_OFFSET_HEIGHT = 12 4 | 5 | export const defaultCursorOption: Readonly> = { 6 | width: 1, 7 | color: '#000000', 8 | dragWidth: 2, 9 | dragColor: '#0000FF', 10 | dragFloatImageDisabled: false 11 | } 12 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Editor.ts: -------------------------------------------------------------------------------- 1 | import { DeepRequired } from '../../interface/Common' 2 | import { IModeRule } from '../../interface/Editor' 3 | 4 | export const EDITOR_COMPONENT = 'editor-component' 5 | export const EDITOR_PREFIX = 'ce' 6 | export const EDITOR_CLIPBOARD = `${EDITOR_PREFIX}-clipboard` 7 | 8 | export const defaultModeRuleOption: Readonly> = { 9 | print: { 10 | imagePreviewerDisabled: false 11 | }, 12 | readonly: { 13 | imagePreviewerDisabled: false 14 | }, 15 | form: { 16 | controlDeletableDisabled: false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Footer.ts: -------------------------------------------------------------------------------- 1 | import { IFooter } from '../../interface/Footer' 2 | import { MaxHeightRatio } from '../enum/Common' 3 | 4 | export const defaultFooterOption: Readonly> = { 5 | bottom: 30, 6 | maxHeightRadio: MaxHeightRatio.HALF, 7 | disabled: false, 8 | editable: true 9 | } 10 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Group.ts: -------------------------------------------------------------------------------- 1 | import { IGroup } from '../../interface/Group' 2 | 3 | export const defaultGroupOption: Readonly> = { 4 | opacity: 0.1, 5 | backgroundColor: '#E99D00', 6 | activeOpacity: 0.5, 7 | activeBackgroundColor: '#E99D00', 8 | disabled: false, 9 | deletable: true 10 | } 11 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Header.ts: -------------------------------------------------------------------------------- 1 | import { IHeader } from '../../interface/Header' 2 | import { MaxHeightRatio } from '../enum/Common' 3 | 4 | export const defaultHeaderOption: Readonly> = { 5 | top: 30, 6 | maxHeightRadio: MaxHeightRatio.HALF, 7 | disabled: false, 8 | editable: true 9 | } 10 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/LineBreak.ts: -------------------------------------------------------------------------------- 1 | import { ILineBreakOption } from '../../interface/LineBreak' 2 | 3 | export const defaultLineBreak: Readonly> = { 4 | disabled: true, 5 | color: '#CCCCCC', 6 | lineWidth: 1.5 7 | } 8 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/LineNumber.ts: -------------------------------------------------------------------------------- 1 | import { ILineNumberOption } from '../../interface/LineNumber' 2 | import { LineNumberType } from '../enum/LineNumber' 3 | 4 | export const defaultLineNumberOption: Readonly> = { 5 | size: 12, 6 | font: 'Microsoft YaHei', 7 | color: '#000000', 8 | disabled: true, 9 | right: 20, 10 | type: LineNumberType.CONTINUITY 11 | } 12 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/List.ts: -------------------------------------------------------------------------------- 1 | import { ListStyle, ListType, UlStyle } from '../enum/List' 2 | 3 | export const ulStyleMapping: Record = { 4 | [UlStyle.DISC]: '•', 5 | [UlStyle.CIRCLE]: '◦', 6 | [UlStyle.SQUARE]: '▫︎', 7 | [UlStyle.CHECKBOX]: '☑️' 8 | } 9 | 10 | export const listTypeElementMapping: Record = { 11 | [ListType.OL]: 'ol', 12 | [ListType.UL]: 'ul' 13 | } 14 | 15 | export const listStyleCSSMapping: Record = { 16 | [ListStyle.DISC]: 'disc', 17 | [ListStyle.CIRCLE]: 'circle', 18 | [ListStyle.SQUARE]: 'square', 19 | [ListStyle.DECIMAL]: 'decimal', 20 | [ListStyle.CHECKBOX]: 'checkbox' 21 | } 22 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/PageBorder.ts: -------------------------------------------------------------------------------- 1 | import { IPageBorderOption } from '../../interface/PageBorder' 2 | 3 | export const defaultPageBorderOption: Readonly> = { 4 | color: '#000000', 5 | lineWidth: 1, 6 | padding: [0, 5, 0, 5], 7 | disabled: true 8 | } 9 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/PageBreak.ts: -------------------------------------------------------------------------------- 1 | import { IPageBreak } from '../../interface/PageBreak' 2 | 3 | export const defaultPageBreakOption: Readonly> = { 4 | font: 'Microsoft YaHei', 5 | fontSize: 12, 6 | lineDash: [3, 1] 7 | } 8 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/PageNumber.ts: -------------------------------------------------------------------------------- 1 | import { IPageNumber } from '../../interface/PageNumber' 2 | import { NumberType } from '../enum/Common' 3 | import { RowFlex } from '../enum/Row' 4 | 5 | export const FORMAT_PLACEHOLDER = { 6 | PAGE_NO: '{pageNo}', 7 | PAGE_COUNT: '{pageCount}' 8 | } 9 | 10 | export const defaultPageNumberOption: Readonly> = { 11 | bottom: 60, 12 | size: 12, 13 | font: 'Microsoft YaHei', 14 | color: '#000000', 15 | rowFlex: RowFlex.CENTER, 16 | format: FORMAT_PLACEHOLDER.PAGE_NO, 17 | numberType: NumberType.ARABIC, 18 | disabled: false, 19 | startPageNo: 1, 20 | fromPageNo: 0, 21 | maxPageNo: null 22 | } 23 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Placeholder.ts: -------------------------------------------------------------------------------- 1 | import { IPlaceholder } from '../../interface/Placeholder' 2 | 3 | export const defaultPlaceholderOption: Readonly> = { 4 | data: '', 5 | color: '#DCDFE6', 6 | opacity: 1, 7 | size: 16, 8 | font: 'Microsoft YaHei' 9 | } 10 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Radio.ts: -------------------------------------------------------------------------------- 1 | import { IRadioOption } from '../../interface/Radio' 2 | import { VerticalAlign } from '../enum/VerticalAlign' 3 | 4 | export const defaultRadioOption: Readonly> = { 5 | width: 14, 6 | height: 14, 7 | gap: 5, 8 | lineWidth: 1, 9 | fillStyle: '#5175f4', 10 | strokeStyle: '#000000', 11 | verticalAlign: VerticalAlign.BOTTOM 12 | } 13 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Separator.ts: -------------------------------------------------------------------------------- 1 | import { ISeparatorOption } from '../../interface/Separator' 2 | 3 | export const defaultSeparatorOption: Readonly> = { 4 | lineWidth: 1, 5 | strokeStyle: '#000000' 6 | } 7 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Table.ts: -------------------------------------------------------------------------------- 1 | import { ITableOption } from '../../interface/table/Table' 2 | 3 | export const defaultTableOption: Readonly> = { 4 | tdPadding: [0, 5, 5, 5], 5 | defaultTrMinHeight: 42, 6 | defaultColMinWidth: 40, 7 | defaultBorderColor: '#000000' 8 | } 9 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Title.ts: -------------------------------------------------------------------------------- 1 | import { ITitleOption, ITitleSizeOption } from '../../interface/Title' 2 | import { TitleLevel } from '../enum/Title' 3 | 4 | export const defaultTitleOption: Readonly> = { 5 | defaultFirstSize: 26, 6 | defaultSecondSize: 24, 7 | defaultThirdSize: 22, 8 | defaultFourthSize: 20, 9 | defaultFifthSize: 18, 10 | defaultSixthSize: 16 11 | } 12 | 13 | export const titleSizeMapping: Record = { 14 | [TitleLevel.FIRST]: 'defaultFirstSize', 15 | [TitleLevel.SECOND]: 'defaultSecondSize', 16 | [TitleLevel.THIRD]: 'defaultThirdSize', 17 | [TitleLevel.FOURTH]: 'defaultFourthSize', 18 | [TitleLevel.FIFTH]: 'defaultFifthSize', 19 | [TitleLevel.SIXTH]: 'defaultSixthSize' 20 | } 21 | 22 | export const titleOrderNumberMapping: Record = { 23 | [TitleLevel.FIRST]: 1, 24 | [TitleLevel.SECOND]: 2, 25 | [TitleLevel.THIRD]: 3, 26 | [TitleLevel.FOURTH]: 4, 27 | [TitleLevel.FIFTH]: 5, 28 | [TitleLevel.SIXTH]: 6 29 | } 30 | 31 | export const titleNodeNameMapping: Record = { 32 | H1: TitleLevel.FIRST, 33 | H2: TitleLevel.SECOND, 34 | H3: TitleLevel.THIRD, 35 | H4: TitleLevel.FOURTH, 36 | H5: TitleLevel.FIFTH, 37 | H6: TitleLevel.SIXTH 38 | } 39 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Watermark.ts: -------------------------------------------------------------------------------- 1 | import { IWatermark } from '../../interface/Watermark' 2 | import { NumberType } from '../enum/Common' 3 | 4 | export const defaultWatermarkOption: Readonly> = { 5 | data: '', 6 | color: '#AEB5C0', 7 | opacity: 0.3, 8 | size: 200, 9 | font: 'Microsoft YaHei', 10 | repeat: false, 11 | gap: [10, 10], 12 | numberType: NumberType.ARABIC 13 | } 14 | -------------------------------------------------------------------------------- /src/editor/dataset/constant/Zone.ts: -------------------------------------------------------------------------------- 1 | import { IZoneOption } from '../../interface/Zone' 2 | 3 | export const defaultZoneOption: Readonly> = { 4 | tipDisabled: true 5 | } 6 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/Area.ts: -------------------------------------------------------------------------------- 1 | export enum AreaMode { 2 | EDIT = 'edit', // 编辑模式(文档可编辑、辅助元素均存在) 3 | READONLY = 'readonly', // 只读模式(文档不可编辑) 4 | FORM = 'form' // 表单模式(仅控件内可编辑) 5 | } 6 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/Background.ts: -------------------------------------------------------------------------------- 1 | export enum BackgroundSize { 2 | CONTAIN = 'contain', 3 | COVER = 'cover' 4 | } 5 | 6 | export enum BackgroundRepeat { 7 | REPEAT = 'repeat', 8 | NO_REPEAT = 'no-repeat', 9 | REPEAT_X = 'repeat-x', 10 | REPEAT_Y = 'repeat-y' 11 | } 12 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/Block.ts: -------------------------------------------------------------------------------- 1 | export enum BlockType { 2 | IFRAME = 'iframe', 3 | VIDEO = 'video' 4 | } 5 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/Common.ts: -------------------------------------------------------------------------------- 1 | export enum MaxHeightRatio { 2 | HALF = 'half', 3 | ONE_THIRD = 'one-third', 4 | QUARTER = 'quarter' 5 | } 6 | 7 | export enum NumberType { 8 | ARABIC = 'arabic', 9 | CHINESE = 'chinese' 10 | } 11 | 12 | export enum ImageDisplay { 13 | INLINE = 'inline', 14 | BLOCK = 'block', 15 | SURROUND = 'surround', 16 | FLOAT_TOP = 'float-top', 17 | FLOAT_BOTTOM = 'float-bottom' 18 | } 19 | 20 | export enum LocationPosition { 21 | BEFORE = 'before', 22 | AFTER = 'after', 23 | OUTER_BEFORE = 'outer-before', 24 | OUTER_AFTER = 'outer-after' 25 | } 26 | 27 | export enum FlexDirection { 28 | ROW = 'row', 29 | COLUMN = 'column' 30 | } 31 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/Control.ts: -------------------------------------------------------------------------------- 1 | export enum ControlType { 2 | TEXT = 'text', 3 | SELECT = 'select', 4 | CHECKBOX = 'checkbox', 5 | RADIO = 'radio', 6 | DATE = 'date', 7 | NUMBER = 'number' 8 | } 9 | 10 | export enum ControlComponent { 11 | PREFIX = 'prefix', 12 | POSTFIX = 'postfix', 13 | PRE_TEXT = 'preText', 14 | POST_TEXT = 'postText', 15 | PLACEHOLDER = 'placeholder', 16 | VALUE = 'value', 17 | CHECKBOX = 'checkbox', 18 | RADIO = 'radio' 19 | } 20 | 21 | // 控件内容缩进方式 22 | export enum ControlIndentation { 23 | ROW_START = 'rowStart', // 从行起始位置缩进 24 | VALUE_START = 'valueStart' // 从值起始位置缩进 25 | } 26 | 27 | // 控件状态 28 | export enum ControlState { 29 | ACTIVE = 'active', 30 | INACTIVE = 'inactive' 31 | } 32 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/Editor.ts: -------------------------------------------------------------------------------- 1 | export enum EditorComponent { 2 | COMPONENT = 'component', 3 | MENU = 'menu', 4 | MAIN = 'main', 5 | FOOTER = 'footer', 6 | CONTEXTMENU = 'contextmenu', 7 | POPUP = 'popup', 8 | CATALOG = 'catalog', 9 | COMMENT = 'comment' 10 | } 11 | 12 | export enum EditorContext { 13 | PAGE = 'page', 14 | TABLE = 'table' 15 | } 16 | 17 | export enum EditorMode { 18 | EDIT = 'edit', // 编辑模式(文档可编辑、辅助元素均存在) 19 | CLEAN = 'clean', // 清洁模式(隐藏辅助元素) 20 | READONLY = 'readonly', // 只读模式(文档不可编辑) 21 | FORM = 'form', // 表单模式(仅控件内可编辑) 22 | PRINT = 'print', // 打印模式(文档不可编辑、隐藏辅助元素、选区、未书写控件及边框) 23 | DESIGN = 'design' // 设计模式(不可删除、只读等配置不控制) 24 | } 25 | 26 | export enum EditorZone { 27 | HEADER = 'header', 28 | MAIN = 'main', 29 | FOOTER = 'footer' 30 | } 31 | 32 | export enum PageMode { 33 | PAGING = 'paging', 34 | CONTINUITY = 'continuity' 35 | } 36 | 37 | export enum PaperDirection { 38 | VERTICAL = 'vertical', 39 | HORIZONTAL = 'horizontal' 40 | } 41 | 42 | export enum WordBreak { 43 | BREAK_ALL = 'break-all', 44 | BREAK_WORD = 'break-word' 45 | } 46 | 47 | export enum RenderMode { 48 | SPEED = 'speed', 49 | COMPATIBILITY = 'compatibility' 50 | } 51 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/Element.ts: -------------------------------------------------------------------------------- 1 | export enum ElementType { 2 | TEXT = 'text', 3 | IMAGE = 'image', 4 | TABLE = 'table', 5 | HYPERLINK = 'hyperlink', 6 | SUPERSCRIPT = 'superscript', 7 | SUBSCRIPT = 'subscript', 8 | SEPARATOR = 'separator', 9 | PAGE_BREAK = 'pageBreak', 10 | CONTROL = 'control', 11 | AREA = 'area', 12 | CHECKBOX = 'checkbox', 13 | RADIO = 'radio', 14 | LATEX = 'latex', 15 | TAB = 'tab', 16 | DATE = 'date', 17 | BLOCK = 'block', 18 | TITLE = 'title', 19 | LIST = 'list' 20 | } 21 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/ElementStyle.ts: -------------------------------------------------------------------------------- 1 | export enum ElementStyleKey { 2 | font = 'font', 3 | size = 'size', 4 | width = 'width', 5 | height = 'height', 6 | bold = 'bold', 7 | color = 'color', 8 | highlight = 'highlight', 9 | italic = 'italic', 10 | underline = 'underline', 11 | strikeout = 'strikeout' 12 | } 13 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/Event.ts: -------------------------------------------------------------------------------- 1 | export enum MouseEventButton { 2 | LEFT = 0, 3 | CENTER = 1, 4 | RIGHT = 2 5 | } 6 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/KeyMap.ts: -------------------------------------------------------------------------------- 1 | export enum KeyMap { 2 | Delete = 'Delete', 3 | Backspace = 'Backspace', 4 | Enter = 'Enter', 5 | Left = 'ArrowLeft', 6 | Right = 'ArrowRight', 7 | Up = 'ArrowUp', 8 | Down = 'ArrowDown', 9 | ESC = 'Escape', 10 | TAB = 'Tab', 11 | META = 'Meta', 12 | LEFT_BRACKET = '[', 13 | RIGHT_BRACKET = ']', 14 | COMMA = ',', 15 | PERIOD = '.', 16 | LEFT_ANGLE_BRACKET = '<', 17 | RIGHT_ANGLE_BRACKET = '>', 18 | EQUAL = '=', 19 | MINUS = '-', 20 | PLUS = '+', 21 | A = 'a', 22 | B = 'b', 23 | C = 'c', 24 | D = 'd', 25 | E = 'e', 26 | F = 'f', 27 | G = 'g', 28 | H = 'h', 29 | I = 'i', 30 | J = 'j', 31 | K = 'k', 32 | L = 'l', 33 | M = 'm', 34 | N = 'n', 35 | O = 'o', 36 | P = 'p', 37 | Q = 'q', 38 | R = 'r', 39 | S = 's', 40 | T = 't', 41 | U = 'u', 42 | V = 'v', 43 | W = 'w', 44 | X = 'x', 45 | Y = 'y', 46 | Z = 'z', 47 | A_UPPERCASE = 'A', 48 | B_UPPERCASE = 'B', 49 | C_UPPERCASE = 'C', 50 | D_UPPERCASE = 'D', 51 | E_UPPERCASE = 'E', 52 | F_UPPERCASE = 'F', 53 | G_UPPERCASE = 'G', 54 | H_UPPERCASE = 'H', 55 | I_UPPERCASE = 'I', 56 | J_UPPERCASE = 'J', 57 | K_UPPERCASE = 'K', 58 | L_UPPERCASE = 'L', 59 | M_UPPERCASE = 'M', 60 | N_UPPERCASE = 'N', 61 | O_UPPERCASE = 'O', 62 | P_UPPERCASE = 'P', 63 | Q_UPPERCASE = 'Q', 64 | R_UPPERCASE = 'R', 65 | S_UPPERCASE = 'S', 66 | T_UPPERCASE = 'T', 67 | U_UPPERCASE = 'U', 68 | V_UPPERCASE = 'V', 69 | W_UPPERCASE = 'W', 70 | X_UPPERCASE = 'X', 71 | Y_UPPERCASE = 'Y', 72 | Z_UPPERCASE = 'Z', 73 | ZERO = '0', 74 | ONE = '1', 75 | TWO = '2', 76 | THREE = '3', 77 | FOUR = '4', 78 | FIVE = '5', 79 | SIX = '6', 80 | SEVEN = '7', 81 | EIGHT = '8', 82 | NINE = '9' 83 | } 84 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/LineNumber.ts: -------------------------------------------------------------------------------- 1 | export enum LineNumberType { 2 | PAGE = 'page', 3 | CONTINUITY = 'continuity' 4 | } 5 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/List.ts: -------------------------------------------------------------------------------- 1 | export enum ListType { 2 | UL = 'ul', 3 | OL = 'ol' 4 | } 5 | 6 | export enum UlStyle { 7 | DISC = 'disc', // 实心圆点 8 | CIRCLE = 'circle', // 空心圆点 9 | SQUARE = 'square', // 实心方块 10 | CHECKBOX = 'checkbox' // 复选框 11 | } 12 | 13 | export enum OlStyle { 14 | DECIMAL = 'decimal' // 阿拉伯数字 15 | } 16 | 17 | export enum ListStyle { 18 | DISC = UlStyle.DISC, 19 | CIRCLE = UlStyle.CIRCLE, 20 | SQUARE = UlStyle.SQUARE, 21 | DECIMAL = OlStyle.DECIMAL, 22 | CHECKBOX = UlStyle.CHECKBOX 23 | } 24 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/Observer.ts: -------------------------------------------------------------------------------- 1 | export enum MoveDirection { 2 | UP = 'top', 3 | DOWN = 'down', 4 | LEFT = 'left', 5 | RIGHT = 'right' 6 | } 7 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/Row.ts: -------------------------------------------------------------------------------- 1 | export enum RowFlex { 2 | LEFT = 'left', 3 | CENTER = 'center', 4 | RIGHT = 'right', 5 | ALIGNMENT = 'alignment', 6 | JUSTIFY = 'justify' 7 | } 8 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/Text.ts: -------------------------------------------------------------------------------- 1 | export enum TextDecorationStyle { 2 | SOLID = 'solid', 3 | DOUBLE = 'double', 4 | DASHED = 'dashed', 5 | DOTTED = 'dotted', 6 | WAVY = 'wavy' 7 | } 8 | 9 | export enum DashType { 10 | SOLID = 'solid', 11 | DASHED = 'dashed', 12 | DOTTED = 'dotted' 13 | } 14 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/Title.ts: -------------------------------------------------------------------------------- 1 | export enum TitleLevel { 2 | FIRST = 'first', 3 | SECOND = 'second', 4 | THIRD = 'third', 5 | FOURTH = 'fourth', 6 | FIFTH = 'fifth', 7 | SIXTH = 'sixth' 8 | } 9 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/VerticalAlign.ts: -------------------------------------------------------------------------------- 1 | export enum VerticalAlign { 2 | TOP = 'top', 3 | MIDDLE = 'middle', 4 | BOTTOM = 'bottom' 5 | } 6 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/table/Table.ts: -------------------------------------------------------------------------------- 1 | export enum TableBorder { 2 | ALL = 'all', 3 | EMPTY = 'empty', 4 | EXTERNAL = 'external', 5 | INTERNAL = 'internal', 6 | DASH = 'dash' 7 | } 8 | 9 | export enum TdBorder { 10 | TOP = 'top', 11 | RIGHT = 'right', 12 | BOTTOM = 'bottom', 13 | LEFT = 'left' 14 | } 15 | 16 | export enum TdSlash { 17 | FORWARD = 'forward', // 正斜线 / 18 | BACK = 'back' // 反斜线 \ 19 | } 20 | -------------------------------------------------------------------------------- /src/editor/dataset/enum/table/TableTool.ts: -------------------------------------------------------------------------------- 1 | export enum TableOrder { 2 | ROW = 'row', 3 | COL = 'col' 4 | } 5 | -------------------------------------------------------------------------------- /src/editor/interface/Area.ts: -------------------------------------------------------------------------------- 1 | import { AreaMode } from '../dataset/enum/Area' 2 | import { LocationPosition } from '../dataset/enum/Common' 3 | import { IElement, IElementPosition } from './Element' 4 | import { IPlaceholder } from './Placeholder' 5 | 6 | export interface IAreaBasic { 7 | extension?: unknown 8 | placeholder?: IPlaceholder 9 | } 10 | 11 | export interface IAreaStyle { 12 | top?: number 13 | borderColor?: string 14 | backgroundColor?: string 15 | } 16 | 17 | export interface IAreaRule { 18 | mode?: AreaMode 19 | hide?: boolean 20 | deletable?: boolean 21 | } 22 | 23 | export type IArea = IAreaBasic & IAreaStyle & IAreaRule 24 | 25 | export interface IInsertAreaOption { 26 | id?: string 27 | area: IArea 28 | value: IElement[] 29 | position?: LocationPosition 30 | } 31 | 32 | export interface ISetAreaPropertiesOption { 33 | id?: string 34 | properties: IArea 35 | } 36 | 37 | export interface IGetAreaValueOption { 38 | id?: string 39 | } 40 | 41 | export interface IGetAreaValueResult { 42 | id?: string 43 | area: IArea 44 | startPageNo: number 45 | endPageNo: number 46 | value: IElement[] 47 | } 48 | 49 | export interface IAreaInfo { 50 | id: string 51 | area: IArea 52 | elementList: IElement[] 53 | positionList: IElementPosition[] 54 | } 55 | -------------------------------------------------------------------------------- /src/editor/interface/Background.ts: -------------------------------------------------------------------------------- 1 | import { BackgroundRepeat, BackgroundSize } from '../dataset/enum/Background' 2 | 3 | export interface IBackgroundOption { 4 | color?: string 5 | image?: string 6 | size?: BackgroundSize 7 | repeat?: BackgroundRepeat 8 | applyPageNumbers?: number[] 9 | } 10 | -------------------------------------------------------------------------------- /src/editor/interface/Badge.ts: -------------------------------------------------------------------------------- 1 | export interface IBadge { 2 | top?: number 3 | left?: number 4 | width: number 5 | height: number 6 | value: string 7 | } 8 | 9 | export interface IBadgeOption { 10 | top?: number 11 | left?: number 12 | } 13 | 14 | export interface IAreaBadge { 15 | areaId: string 16 | badge: IBadge 17 | } 18 | -------------------------------------------------------------------------------- /src/editor/interface/Block.ts: -------------------------------------------------------------------------------- 1 | import { BlockType } from '../dataset/enum/Block' 2 | 3 | export interface IIFrameBlock { 4 | src?: string 5 | srcdoc?: string 6 | } 7 | 8 | export interface IVideoBlock { 9 | src: string 10 | } 11 | 12 | export interface IBlock { 13 | type: BlockType 14 | iframeBlock?: IIFrameBlock 15 | videoBlock?: IVideoBlock 16 | } 17 | -------------------------------------------------------------------------------- /src/editor/interface/Catalog.ts: -------------------------------------------------------------------------------- 1 | import { TitleLevel } from '../dataset/enum/Title' 2 | 3 | export interface ICatalogItem { 4 | id: string 5 | name: string 6 | level: TitleLevel 7 | pageNo: number 8 | subCatalog: ICatalogItem[] 9 | } 10 | 11 | export type ICatalog = ICatalogItem[] 12 | -------------------------------------------------------------------------------- /src/editor/interface/Checkbox.ts: -------------------------------------------------------------------------------- 1 | import { VerticalAlign } from '../dataset/enum/VerticalAlign' 2 | 3 | export interface ICheckbox { 4 | value: boolean | null 5 | code?: string 6 | disabled?: boolean 7 | } 8 | 9 | export interface ICheckboxOption { 10 | width?: number 11 | height?: number 12 | gap?: number 13 | lineWidth?: number 14 | fillStyle?: string 15 | strokeStyle?: string 16 | verticalAlign?: VerticalAlign 17 | } 18 | -------------------------------------------------------------------------------- /src/editor/interface/Command.ts: -------------------------------------------------------------------------------- 1 | export interface IRichtextOption { 2 | isIgnoreDisabledRule: boolean 3 | } 4 | -------------------------------------------------------------------------------- /src/editor/interface/Common.ts: -------------------------------------------------------------------------------- 1 | export type Primitive = 2 | | string 3 | | number 4 | | boolean 5 | | bigint 6 | | symbol 7 | | undefined 8 | | null 9 | 10 | export type Builtin = Primitive | Function | Date | Error | RegExp 11 | 12 | export type DeepRequired = T extends Error 13 | ? Required 14 | : T extends Builtin 15 | ? T 16 | : T extends Map 17 | ? Map, DeepRequired> 18 | : T extends ReadonlyMap 19 | ? ReadonlyMap, DeepRequired> 20 | : T extends WeakMap 21 | ? WeakMap, DeepRequired> 22 | : T extends Set 23 | ? Set> 24 | : T extends ReadonlySet 25 | ? ReadonlySet> 26 | : T extends WeakSet 27 | ? WeakSet> 28 | : T extends Promise 29 | ? Promise> 30 | : T extends {} 31 | ? { [K in keyof T]-?: DeepRequired } 32 | : Required 33 | 34 | export type DeepPartial = { 35 | [P in keyof T]?: DeepPartial 36 | } 37 | 38 | export type IPadding = [ 39 | top: number, 40 | right: number, 41 | bottom: number, 42 | left: number 43 | ] 44 | -------------------------------------------------------------------------------- /src/editor/interface/Cursor.ts: -------------------------------------------------------------------------------- 1 | export interface ICursorOption { 2 | width?: number 3 | color?: string 4 | dragWidth?: number 5 | dragColor?: string 6 | dragFloatImageDisabled?: boolean 7 | } 8 | -------------------------------------------------------------------------------- /src/editor/interface/Event.ts: -------------------------------------------------------------------------------- 1 | import { IElement } from './Element' 2 | import { RangeRect } from './Range' 3 | 4 | export interface IPasteOption { 5 | isPlainText: boolean 6 | } 7 | 8 | export interface ITableInfoByEvent { 9 | element: IElement 10 | trIndex: number 11 | tdIndex: number 12 | } 13 | 14 | export interface IPositionContextByEventResult { 15 | pageNo: number 16 | element: IElement | null 17 | rangeRect: RangeRect | null 18 | tableInfo: ITableInfoByEvent | null 19 | } 20 | 21 | export interface IPositionContextByEventOption { 22 | isMustDirectHit?: boolean 23 | } 24 | 25 | export interface ICopyOption { 26 | isPlainText: boolean 27 | } 28 | -------------------------------------------------------------------------------- /src/editor/interface/EventBus.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IContentChange, 3 | IControlChange, 4 | IControlContentChange, 5 | IImageSizeChange, 6 | IIntersectionPageNoChange, 7 | IMouseEventChange, 8 | IPageModeChange, 9 | IPageScaleChange, 10 | IPageSizeChange, 11 | IPositionContextChange, 12 | IRangeStyleChange, 13 | ISaved, 14 | IVisiblePageNoListChange, 15 | IZoneChange 16 | } from './Listener' 17 | 18 | export interface EventBusMap { 19 | rangeStyleChange: IRangeStyleChange 20 | visiblePageNoListChange: IVisiblePageNoListChange 21 | intersectionPageNoChange: IIntersectionPageNoChange 22 | pageSizeChange: IPageSizeChange 23 | pageScaleChange: IPageScaleChange 24 | saved: ISaved 25 | contentChange: IContentChange 26 | controlChange: IControlChange 27 | controlContentChange: IControlContentChange 28 | pageModeChange: IPageModeChange 29 | zoneChange: IZoneChange 30 | mousemove: IMouseEventChange 31 | mouseleave: IMouseEventChange 32 | mouseenter: IMouseEventChange 33 | mousedown: IMouseEventChange 34 | mouseup: IMouseEventChange 35 | click: IMouseEventChange 36 | positionContextChange: IPositionContextChange 37 | imageSizeChange: IImageSizeChange 38 | } 39 | -------------------------------------------------------------------------------- /src/editor/interface/Footer.ts: -------------------------------------------------------------------------------- 1 | import { MaxHeightRatio } from '../dataset/enum/Common' 2 | 3 | export interface IFooter { 4 | bottom?: number 5 | maxHeightRadio?: MaxHeightRatio 6 | disabled?: boolean 7 | editable?: boolean 8 | } 9 | -------------------------------------------------------------------------------- /src/editor/interface/Group.ts: -------------------------------------------------------------------------------- 1 | export interface IGroup { 2 | opacity?: number 3 | backgroundColor?: string 4 | activeOpacity?: number 5 | activeBackgroundColor?: string 6 | disabled?: boolean 7 | deletable?: boolean 8 | } 9 | -------------------------------------------------------------------------------- /src/editor/interface/Header.ts: -------------------------------------------------------------------------------- 1 | import { MaxHeightRatio } from '../dataset/enum/Common' 2 | 3 | export interface IHeader { 4 | top?: number 5 | maxHeightRadio?: MaxHeightRatio 6 | disabled?: boolean 7 | editable?: boolean 8 | } 9 | -------------------------------------------------------------------------------- /src/editor/interface/LineBreak.ts: -------------------------------------------------------------------------------- 1 | export interface ILineBreakOption { 2 | disabled?: boolean 3 | color?: string 4 | lineWidth?: number 5 | } 6 | -------------------------------------------------------------------------------- /src/editor/interface/LineNumber.ts: -------------------------------------------------------------------------------- 1 | import { LineNumberType } from '../dataset/enum/LineNumber' 2 | 3 | export interface ILineNumberOption { 4 | size?: number 5 | font?: string 6 | color?: string 7 | disabled?: boolean 8 | right?: number 9 | type?: LineNumberType 10 | } 11 | -------------------------------------------------------------------------------- /src/editor/interface/Margin.ts: -------------------------------------------------------------------------------- 1 | export type IMargin = [top: number, right: number, bottom: number, left: number] 2 | -------------------------------------------------------------------------------- /src/editor/interface/PageBorder.ts: -------------------------------------------------------------------------------- 1 | import { IPadding } from './Common' 2 | 3 | export interface IPageBorderOption { 4 | color?: string 5 | lineWidth?: number 6 | padding?: IPadding 7 | disabled?: boolean 8 | } 9 | -------------------------------------------------------------------------------- /src/editor/interface/PageBreak.ts: -------------------------------------------------------------------------------- 1 | export interface IPageBreak { 2 | font?: string 3 | fontSize?: number 4 | lineDash?: number[] 5 | } 6 | -------------------------------------------------------------------------------- /src/editor/interface/PageNumber.ts: -------------------------------------------------------------------------------- 1 | import { NumberType } from '../dataset/enum/Common' 2 | import { RowFlex } from '../dataset/enum/Row' 3 | 4 | export interface IPageNumber { 5 | bottom?: number 6 | size?: number 7 | font?: string 8 | color?: string 9 | rowFlex?: RowFlex 10 | format?: string 11 | numberType?: NumberType 12 | disabled?: boolean 13 | startPageNo?: number 14 | fromPageNo?: number 15 | maxPageNo?: number | null 16 | } 17 | -------------------------------------------------------------------------------- /src/editor/interface/Placeholder.ts: -------------------------------------------------------------------------------- 1 | export interface IPlaceholder { 2 | data: string 3 | color?: string 4 | opacity?: number 5 | size?: number 6 | font?: string 7 | } 8 | -------------------------------------------------------------------------------- /src/editor/interface/Plugin.ts: -------------------------------------------------------------------------------- 1 | import Editor from '..' 2 | 3 | export type PluginFunction = (editor: Editor, options?: Options) => any 4 | 5 | export type UsePlugin = ( 6 | pluginFunction: PluginFunction, 7 | options?: Options 8 | ) => void 9 | -------------------------------------------------------------------------------- /src/editor/interface/Previewer.ts: -------------------------------------------------------------------------------- 1 | import { IElement } from './Element' 2 | 3 | export interface IPreviewerCreateResult { 4 | resizerSelection: HTMLDivElement 5 | resizerHandleList: HTMLDivElement[] 6 | resizerImageContainer: HTMLDivElement 7 | resizerImage: HTMLImageElement 8 | resizerSize: HTMLSpanElement 9 | } 10 | 11 | export interface IPreviewerDrawOption { 12 | mime?: 'png' | 'jpg' | 'jpeg' | 'svg' 13 | srcKey?: keyof Pick 14 | dragDisable?: boolean 15 | } 16 | -------------------------------------------------------------------------------- /src/editor/interface/Radio.ts: -------------------------------------------------------------------------------- 1 | import { VerticalAlign } from '../dataset/enum/VerticalAlign' 2 | 3 | export interface IRadio { 4 | value: boolean | null 5 | code?: string 6 | disabled?: boolean 7 | } 8 | 9 | export interface IRadioOption { 10 | width?: number 11 | height?: number 12 | gap?: number 13 | lineWidth?: number 14 | fillStyle?: string 15 | strokeStyle?: string 16 | verticalAlign?: VerticalAlign 17 | } 18 | -------------------------------------------------------------------------------- /src/editor/interface/Range.ts: -------------------------------------------------------------------------------- 1 | import { EditorZone } from '../dataset/enum/Editor' 2 | import { IElement, IElementFillRect, IElementStyle } from './Element' 3 | 4 | export interface IRange { 5 | startIndex: number 6 | endIndex: number 7 | isCrossRowCol?: boolean 8 | tableId?: string 9 | startTdIndex?: number 10 | endTdIndex?: number 11 | startTrIndex?: number 12 | endTrIndex?: number 13 | zone?: EditorZone 14 | } 15 | 16 | export type RangeRowArray = Map 17 | 18 | export type RangeRowMap = Map> 19 | 20 | export type RangeRect = IElementFillRect 21 | 22 | export type RangeContext = { 23 | isCollapsed: boolean 24 | startElement: IElement 25 | endElement: IElement 26 | startPageNo: number 27 | endPageNo: number 28 | startRowNo: number 29 | endRowNo: number 30 | rangeRects: RangeRect[] 31 | zone: EditorZone 32 | isTable: boolean 33 | trIndex: number | null 34 | tdIndex: number | null 35 | tableElement: IElement | null 36 | selectionText: string | null 37 | selectionElementList: IElement[] 38 | titleId: string | null 39 | titleStartPageNo: number | null 40 | } 41 | 42 | export interface IRangeParagraphInfo { 43 | elementList: IElement[] 44 | startIndex: number 45 | } 46 | 47 | export type IRangeElementStyle = Pick< 48 | IElementStyle, 49 | | 'bold' 50 | | 'color' 51 | | 'highlight' 52 | | 'font' 53 | | 'size' 54 | | 'italic' 55 | | 'underline' 56 | | 'strikeout' 57 | > 58 | -------------------------------------------------------------------------------- /src/editor/interface/Row.ts: -------------------------------------------------------------------------------- 1 | import { RowFlex } from '../dataset/enum/Row' 2 | import { IElement, IElementMetrics } from './Element' 3 | 4 | export type IRowElement = IElement & { 5 | metrics: IElementMetrics 6 | style: string 7 | left?: number 8 | } 9 | 10 | export interface IRow { 11 | width: number 12 | height: number 13 | ascent: number 14 | rowFlex?: RowFlex 15 | startIndex: number 16 | isPageBreak?: boolean 17 | isList?: boolean 18 | listIndex?: number 19 | offsetX?: number 20 | offsetY?: number 21 | elementList: IRowElement[] 22 | isWidthNotEnough?: boolean 23 | rowIndex: number 24 | isSurround?: boolean 25 | } 26 | -------------------------------------------------------------------------------- /src/editor/interface/Search.ts: -------------------------------------------------------------------------------- 1 | import { EditorContext } from '../dataset/enum/Editor' 2 | import { IElementPosition } from './Element' 3 | import { IRange } from './Range' 4 | 5 | export interface ISearchResultBasic { 6 | type: EditorContext 7 | index: number 8 | groupId: string 9 | } 10 | 11 | export interface ISearchResultRestArgs { 12 | tableId?: string 13 | tableIndex?: number 14 | trIndex?: number 15 | tdIndex?: number 16 | tdId?: string 17 | startIndex?: number 18 | } 19 | 20 | export type ISearchResult = ISearchResultBasic & ISearchResultRestArgs 21 | 22 | export interface ISearchResultContext { 23 | range: IRange 24 | startPosition: IElementPosition 25 | endPosition: IElementPosition 26 | } 27 | 28 | export interface IReplaceOption { 29 | index?: number 30 | } 31 | -------------------------------------------------------------------------------- /src/editor/interface/Separator.ts: -------------------------------------------------------------------------------- 1 | export interface ISeparatorOption { 2 | strokeStyle?: string 3 | lineWidth?: number 4 | } 5 | -------------------------------------------------------------------------------- /src/editor/interface/Text.ts: -------------------------------------------------------------------------------- 1 | import { TextDecorationStyle } from '../dataset/enum/Text' 2 | 3 | export interface ITextMetrics { 4 | width: number 5 | actualBoundingBoxAscent: number 6 | actualBoundingBoxDescent: number 7 | actualBoundingBoxLeft: number 8 | actualBoundingBoxRight: number 9 | fontBoundingBoxAscent: number 10 | fontBoundingBoxDescent: number 11 | } 12 | 13 | export interface ITextDecoration { 14 | style?: TextDecorationStyle 15 | } 16 | -------------------------------------------------------------------------------- /src/editor/interface/Title.ts: -------------------------------------------------------------------------------- 1 | import { EditorZone } from '../dataset/enum/Editor' 2 | import { IElement } from './Element' 3 | 4 | export interface ITitleSizeOption { 5 | defaultFirstSize?: number 6 | defaultSecondSize?: number 7 | defaultThirdSize?: number 8 | defaultFourthSize?: number 9 | defaultFifthSize?: number 10 | defaultSixthSize?: number 11 | } 12 | 13 | export type ITitleOption = ITitleSizeOption & {} 14 | 15 | export interface ITitleRule { 16 | deletable?: boolean 17 | disabled?: boolean 18 | } 19 | 20 | export type ITitle = ITitleRule & { 21 | conceptId?: string 22 | } 23 | 24 | export interface IGetTitleValueOption { 25 | conceptId: string 26 | } 27 | 28 | export type IGetTitleValueResult = (ITitle & { 29 | value: string | null 30 | elementList: IElement[] 31 | zone: EditorZone 32 | })[] 33 | -------------------------------------------------------------------------------- /src/editor/interface/Watermark.ts: -------------------------------------------------------------------------------- 1 | import { NumberType } from '../dataset/enum/Common' 2 | 3 | export interface IWatermark { 4 | data: string 5 | color?: string 6 | opacity?: number 7 | size?: number 8 | font?: string 9 | repeat?: boolean 10 | numberType?: NumberType 11 | gap?: [horizontal: number, vertical: number] 12 | } 13 | -------------------------------------------------------------------------------- /src/editor/interface/Zone.ts: -------------------------------------------------------------------------------- 1 | export interface IZoneOption { 2 | tipDisabled?: boolean 3 | } 4 | -------------------------------------------------------------------------------- /src/editor/interface/contextmenu/ContextMenu.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../core/command/Command' 2 | import { EditorZone } from '../../dataset/enum/Editor' 3 | import { DeepRequired } from '../Common' 4 | import { IEditorOption } from '../Editor' 5 | import { IElement } from '../Element' 6 | 7 | export interface IContextMenuContext { 8 | startElement: IElement | null 9 | endElement: IElement | null 10 | isReadonly: boolean 11 | editorHasSelection: boolean 12 | editorTextFocus: boolean 13 | isInTable: boolean 14 | isCrossRowCol: boolean 15 | zone: EditorZone 16 | trIndex: number | null 17 | tdIndex: number | null 18 | tableElement: IElement | null 19 | options: DeepRequired 20 | } 21 | 22 | export interface IRegisterContextMenu { 23 | key?: string 24 | i18nPath?: string 25 | isDivider?: boolean 26 | icon?: string 27 | name?: string 28 | shortCut?: string 29 | disable?: boolean 30 | when?: (payload: IContextMenuContext) => boolean 31 | callback?: (command: Command, context: IContextMenuContext) => any 32 | childMenus?: IRegisterContextMenu[] 33 | } 34 | 35 | export interface IContextmenuLang { 36 | global: { 37 | cut: string 38 | copy: string 39 | paste: string 40 | selectAll: string 41 | print: string 42 | } 43 | control: { 44 | delete: string 45 | } 46 | hyperlink: { 47 | delete: string 48 | cancel: string 49 | edit: string 50 | } 51 | image: { 52 | change: string 53 | saveAs: string 54 | textWrap: string 55 | textWrapType: { 56 | embed: string 57 | upDown: string 58 | } 59 | } 60 | table: { 61 | insertRowCol: string 62 | insertTopRow: string 63 | insertBottomRow: string 64 | insertLeftCol: string 65 | insertRightCol: string 66 | deleteRowCol: string 67 | deleteRow: string 68 | deleteCol: string 69 | deleteTable: string 70 | mergeCell: string 71 | mergeCancelCell: string 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/editor/interface/i18n/I18n.ts: -------------------------------------------------------------------------------- 1 | import { IDatePickerLang } from '../../core/draw/particle/date/DatePicker' 2 | import { IContextmenuLang } from '../contextmenu/ContextMenu' 3 | 4 | export interface ILang { 5 | contextmenu: IContextmenuLang 6 | datePicker: IDatePickerLang 7 | } 8 | -------------------------------------------------------------------------------- /src/editor/interface/shortcut/Shortcut.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../core/command/Command' 2 | import { KeyMap } from '../../dataset/enum/KeyMap' 3 | 4 | export interface IRegisterShortcut { 5 | key: KeyMap 6 | ctrl?: boolean 7 | meta?: boolean 8 | mod?: boolean // windows:ctrl || mac:command 9 | shift?: boolean 10 | alt?: boolean // windows:alt || mac:option 11 | isGlobal?: boolean 12 | callback?: (command: Command) => any 13 | disable?: boolean 14 | } 15 | -------------------------------------------------------------------------------- /src/editor/interface/table/Colgroup.ts: -------------------------------------------------------------------------------- 1 | export interface IColgroup { 2 | id?: string 3 | width: number 4 | } 5 | -------------------------------------------------------------------------------- /src/editor/interface/table/Table.ts: -------------------------------------------------------------------------------- 1 | import { IPadding } from '../Common' 2 | 3 | export interface ITableOption { 4 | tdPadding?: IPadding 5 | defaultTrMinHeight?: number 6 | defaultColMinWidth?: number 7 | defaultBorderColor?: string 8 | } 9 | -------------------------------------------------------------------------------- /src/editor/interface/table/Td.ts: -------------------------------------------------------------------------------- 1 | import { VerticalAlign } from '../../dataset/enum/VerticalAlign' 2 | import { TdBorder, TdSlash } from '../../dataset/enum/table/Table' 3 | import { IElement, IElementPosition } from '../Element' 4 | import { IRow } from '../Row' 5 | 6 | export interface ITd { 7 | conceptId?: string 8 | id?: string 9 | extension?: unknown 10 | externalId?: string 11 | x?: number 12 | y?: number 13 | width?: number 14 | height?: number 15 | colspan: number 16 | rowspan: number 17 | value: IElement[] 18 | trIndex?: number 19 | tdIndex?: number 20 | isLastRowTd?: boolean 21 | isLastColTd?: boolean 22 | isLastTd?: boolean 23 | rowIndex?: number 24 | colIndex?: number 25 | rowList?: IRow[] 26 | positionList?: IElementPosition[] 27 | verticalAlign?: VerticalAlign 28 | backgroundColor?: string 29 | borderTypes?: TdBorder[] 30 | slashTypes?: TdSlash[] 31 | mainHeight?: number // 内容 + 内边距高度 32 | realHeight?: number // 真实高度(包含跨列) 33 | realMinHeight?: number // 真实最小高度(包含跨列) 34 | disabled?: boolean // 内容不可编辑 35 | deletable?: boolean // 内容不可删除 36 | } 37 | -------------------------------------------------------------------------------- /src/editor/interface/table/Tr.ts: -------------------------------------------------------------------------------- 1 | import { ITd } from './Td' 2 | 3 | export interface ITr { 4 | id?: string 5 | extension?: unknown 6 | externalId?: string 7 | height: number 8 | tdList: ITd[] 9 | minHeight?: number 10 | pagingRepeat?: boolean // 在各页顶端以标题行的形式重复出现 11 | } 12 | -------------------------------------------------------------------------------- /src/editor/types/index.d.ts: -------------------------------------------------------------------------------- 1 | // 部分浏览器canvas上下文支持设置以下属性 2 | interface CanvasRenderingContext2D { 3 | letterSpacing: string 4 | wordSpacing: string 5 | } 6 | -------------------------------------------------------------------------------- /src/editor/utils/hotkey.ts: -------------------------------------------------------------------------------- 1 | import { isApple } from './ua' 2 | 3 | export function isMod(evt: KeyboardEvent | MouseEvent) { 4 | return isApple ? evt.metaKey : evt.ctrlKey 5 | } 6 | -------------------------------------------------------------------------------- /src/editor/utils/ua.ts: -------------------------------------------------------------------------------- 1 | export const isApple = 2 | typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent) 3 | 4 | export const isIOS = 5 | typeof navigator !== 'undefined' && /iPad|iPhone/.test(navigator.userAgent) 6 | 7 | export const isMobile = 8 | /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( 9 | navigator.userAgent 10 | ) 11 | -------------------------------------------------------------------------------- /src/plugins/copy/index.ts: -------------------------------------------------------------------------------- 1 | // 复制内容时带入版权信息,代码仅为参考 2 | import Editor from '../../editor' 3 | 4 | export interface ICopyWithCopyrightOption { 5 | copyrightText: string 6 | } 7 | 8 | export function copyWithCopyrightPlugin( 9 | editor: Editor, 10 | options?: ICopyWithCopyrightOption 11 | ) { 12 | const copy = editor.command.executeCopy 13 | 14 | editor.command.executeCopy = () => { 15 | const { copyrightText } = options || {} 16 | if (copyrightText) { 17 | const rangeText = editor.command.getRangeText() 18 | if (!rangeText) return 19 | const text = `${rangeText}${copyrightText}` 20 | const plainText = new Blob([text], { type: 'text/plain' }) 21 | // @ts-ignore 22 | const item = new ClipboardItem({ 23 | [plainText.type]: plainText 24 | }) 25 | window.navigator.clipboard.write([item]) 26 | } else { 27 | copy() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function debounce( 2 | func: (...arg: T) => unknown, 3 | delay: number 4 | ) { 5 | let timer: number 6 | return function (this: unknown, ...args: T) { 7 | if (timer) { 8 | window.clearTimeout(timer) 9 | } 10 | timer = window.setTimeout(() => { 11 | func.apply(this, args) 12 | }, delay) 13 | } 14 | } 15 | 16 | export function scrollIntoView(container: HTMLElement, selected: HTMLElement) { 17 | if (!selected) { 18 | container.scrollTop = 0 19 | return 20 | } 21 | const offsetParents: HTMLElement[] = [] 22 | let pointer = selected.offsetParent 23 | while (pointer && container !== pointer && container.contains(pointer)) { 24 | offsetParents.push(pointer) 25 | pointer = pointer.offsetParent 26 | } 27 | const top = 28 | selected.offsetTop + 29 | offsetParents.reduce((prev, curr) => prev + curr.offsetTop, 0) 30 | const bottom = top + selected.offsetHeight 31 | const viewRectTop = container.scrollTop 32 | const viewRectBottom = viewRectTop + container.clientHeight 33 | if (top < viewRectTop) { 34 | container.scrollTop = top 35 | } else if (bottom > viewRectBottom) { 36 | container.scrollTop = bottom - container.clientHeight 37 | } 38 | } 39 | 40 | export function nextTick(fn: Function) { 41 | const callback = window.requestIdleCallback || window.setTimeout 42 | callback(() => { 43 | fn() 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "declaration": true, 12 | "noEmit": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "outDir": "dist", 17 | "rootDir": "", 18 | }, 19 | "include": ["./src/"], 20 | "exclude": [ 21 | "node_modules", 22 | "dist" 23 | ] 24 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import typescript from '@rollup/plugin-typescript' 3 | import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js' 4 | import * as path from 'path' 5 | 6 | export default defineConfig(({ mode }) => { 7 | const name = 'canvas-editor' 8 | if (mode === 'lib') { 9 | return { 10 | plugins: [ 11 | cssInjectedByJsPlugin({ 12 | styleId: `${name}-style`, 13 | topExecutionPriority: true 14 | }), 15 | { 16 | ...typescript({ 17 | tsconfig: './tsconfig.json', 18 | include: ['./src/editor/**'] 19 | }), 20 | apply: 'build', 21 | declaration: true, 22 | declarationDir: 'types/', 23 | rootDir: '/' 24 | } 25 | ], 26 | build: { 27 | lib: { 28 | name, 29 | fileName: name, 30 | entry: path.resolve(__dirname, 'src/editor/index.ts') 31 | }, 32 | rollupOptions: { 33 | output: { 34 | sourcemap: true 35 | } 36 | } 37 | } 38 | } 39 | } 40 | return { 41 | base: `/${name}/`, 42 | server: { 43 | host: '0.0.0.0' 44 | } 45 | } 46 | }) 47 | --------------------------------------------------------------------------------