├── test
├── setup
│ ├── stylesMock.js
│ └── index.ts
├── utils
│ └── create-editor.ts
└── module
│ ├── local.test.ts
│ ├── plugin.test.ts
│ ├── elem-to-html.test.ts
│ ├── render-elem.test.ts
│ ├── parse-elem-html.test.ts
│ └── menu
│ ├── insert-formula.test.ts
│ └── edit-formula.test.ts
├── .yarnrc
├── .eslintignore
├── .babelrc
├── postcss.config.js
├── _img
└── demo.png
├── .npmignore
├── commitlint.config.js
├── src
├── register-custom-elem
│ ├── README.md
│ ├── index.ts
│ └── native-shim.ts
├── index.ts
├── module
│ ├── custom-types.ts
│ ├── menu
│ │ ├── index.ts
│ │ ├── InsertFormula.ts
│ │ └── EditFormula.ts
│ ├── local.ts
│ ├── elem-to-html.ts
│ ├── index.ts
│ ├── parse-elem-html.ts
│ ├── plugin.ts
│ └── render-elem.ts
├── utils
│ ├── util.ts
│ └── dom.ts
└── constants
│ └── icon-svg.ts
├── .editorconfig
├── .release-it.js
├── .vscode
└── settings.json
├── .github
└── workflows
│ ├── npm-publish.yml
│ └── build.yml
├── .prettierrc.js
├── jest.config.js
├── tsconfig.json
├── .eslintrc.js
├── DEV.md
├── LICENSE
├── README.md
├── example
├── index.ts
└── index.html
├── .cz-config.js
├── README-en.md
├── .gitignore
└── package.json
/test/setup/stylesMock.js:
--------------------------------------------------------------------------------
1 | module.exports = {}
2 |
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | registry "https://registry.npm.taobao.org"
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | dist/
3 | lib/
4 | *.html
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"],
3 | "plugins": []
4 | }
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require('autoprefixer')],
3 | }
4 |
--------------------------------------------------------------------------------
/_img/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangeditor-team/wangEditor-plugin-formula/HEAD/_img/demo.png
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .github/
2 | .vscode/
3 | build/
4 | node_modules/
5 | test/
6 | yarn.lock
7 | package-lock.json
8 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['cz'],
3 | rules: {
4 | 'type-empty': [2, 'never'],
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/register-custom-elem/README.md:
--------------------------------------------------------------------------------
1 | # register custom elem
2 |
3 | 全局注册一个自定义元素 `` 用于渲染公式。
4 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description src entry
3 | * @author wangfupeng
4 | */
5 |
6 | // 全局注册自定义组件,用于渲染公式
7 | import './register-custom-elem'
8 |
9 | import module from './module/index'
10 |
11 | export default module
12 |
--------------------------------------------------------------------------------
/test/setup/index.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom'
2 | import nodeCrypto from 'crypto'
3 |
4 | // @ts-ignore
5 | global.crypto = {
6 | getRandomValues: function (buffer: any) {
7 | return nodeCrypto.randomFillSync(buffer)
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/src/module/custom-types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description formula element
3 | * @author wangfupeng
4 | */
5 |
6 | type EmptyText = {
7 | text: ''
8 | }
9 |
10 | export type FormulaElement = {
11 | type: 'formula'
12 | value: string
13 | children: EmptyText[]
14 | }
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | indent_style = space
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [*.txt]
16 | insert_final_newline = false
17 | trim_trailing_whitespace = false
18 |
--------------------------------------------------------------------------------
/test/utils/create-editor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description create editor
3 | * @author wangfupeng
4 | */
5 |
6 | import { createEditor } from '@wangeditor/editor'
7 |
8 | export default function (options: any = {}) {
9 | const container = document.createElement('div')
10 | document.body.appendChild(container)
11 |
12 | return createEditor({
13 | selector: container,
14 | ...options,
15 | })
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/util.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description 工具函数
3 | * @author wangfupeng
4 | */
5 |
6 | import { nanoid } from 'nanoid'
7 |
8 | /**
9 | * 获取随机数字符串
10 | * @param prefix 前缀
11 | * @returns 随机数字符串
12 | */
13 | export function genRandomStr(prefix: string = 'r'): string {
14 | return `${prefix}-${nanoid()}`
15 | }
16 |
17 | // export function replaceSymbols(str: string) {
18 | // return str.replace(//g, '>')
19 | // }
20 |
--------------------------------------------------------------------------------
/.release-it.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | git: {
3 | tagName: "v${version}",
4 | commitMessage: "release: v${version}",
5 | requireCleanWorkingDir: false,
6 | requireBranch: "main",
7 | },
8 | hooks: {
9 | "before:init": ["git pull origin main", "npm run test"],
10 | },
11 | npm: {
12 | publish: false,
13 | },
14 | prompt: {
15 | ghRelease: false,
16 | glRelease: false,
17 | publish: false,
18 | },
19 | }
20 |
--------------------------------------------------------------------------------
/test/module/local.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description formula local test
3 | * @author wangfupeng
4 | */
5 |
6 | import '../../src/module/local'
7 | import { i18nChangeLanguage, t } from '@wangeditor/editor'
8 |
9 | describe('local', () => {
10 | it('zh-CN', () => {
11 | expect(t('formula.formula')).toBe('公式')
12 | })
13 | it('en', () => {
14 | i18nChangeLanguage('en')
15 | expect(t('formula.formula')).toBe('Formula')
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "cSpell.words": [
4 | "commitlint",
5 | "Elems",
6 | "hoverbar",
7 | "katex",
8 | "nocheck",
9 | "prettierrc",
10 | "snabbdom",
11 | "vnode",
12 | "wangeditor",
13 | "wangfupeng"
14 | ],
15 | "typescript.tsdk": "node_modules/typescript/lib",
16 | "editor.codeActionsOnSave": {
17 | "source.fixAll.eslint": true
18 | },
19 | "eslint.validate": [
20 | "javascript"
21 | ],
22 | "editor.detectIndentation": false,
23 | "editor.tabSize": 2
24 | }
--------------------------------------------------------------------------------
/src/module/menu/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description formula menu entry
3 | * @author wangfupeng
4 | */
5 |
6 | import InsertFormulaMenu from './InsertFormula'
7 | import EditFormulaMenu from './EditFormula'
8 |
9 | export const insertFormulaMenuConf = {
10 | key: 'insertFormula', // menu key ,唯一。注册之后,可配置到工具栏
11 | factory() {
12 | return new InsertFormulaMenu()
13 | },
14 | }
15 |
16 | export const editFormulaMenuConf = {
17 | key: 'editFormula', // menu key ,唯一。注册之后,可配置到工具栏
18 | factory() {
19 | return new EditFormulaMenu()
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/src/module/local.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description 多语言
3 | * @author wangfupeng
4 | */
5 |
6 | import { i18nAddResources } from '@wangeditor/editor'
7 |
8 | i18nAddResources('en', {
9 | formula: {
10 | formula: 'Formula',
11 | placeholder: 'Use LateX syntax',
12 | insert: 'Insert formula',
13 | edit: 'Edit formula',
14 | ok: 'OK',
15 | },
16 | })
17 |
18 | i18nAddResources('zh-CN', {
19 | formula: {
20 | formula: '公式',
21 | placeholder: '使用 LateX 语法',
22 | insert: '插入公式',
23 | edit: '编辑公式',
24 | ok: '确定',
25 | },
26 | })
27 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | name: publish npm Package
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*.*.*"
7 |
8 | jobs:
9 | publish-npm:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: actions/setup-node@v1
14 | with:
15 | node-version: 12
16 | registry-url: https://registry.npmjs.org/
17 | - run: npm i
18 | - run: npm run test
19 | - run: npm run build
20 | - run: npm publish --access=public
21 | env:
22 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
23 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // 箭头函数只有一个参数的时候可以忽略括号
3 | arrowParens: 'avoid',
4 | // 括号内部不要出现空格
5 | bracketSpacing: true,
6 | // 行结束符使用 Unix 格式
7 | endOfLine: 'lf',
8 | // true: Put > on the last line instead of at a new line
9 | jsxBracketSameLine: false,
10 | // 行宽
11 | printWidth: 100,
12 | // 换行方式
13 | proseWrap: 'preserve',
14 | // 分号
15 | semi: false,
16 | // 使用单引号
17 | singleQuote: true,
18 | // 缩进
19 | tabWidth: 2,
20 | // 使用 tab 缩进
21 | useTabs: false,
22 | // 后置逗号,多行对象、数组在最后一行增加逗号
23 | trailingComma: 'es5',
24 | parser: 'typescript',
25 | }
26 |
--------------------------------------------------------------------------------
/src/module/elem-to-html.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description elem to html
3 | * @author wangfupeng
4 | */
5 |
6 | import { SlateElement } from '@wangeditor/editor'
7 | import { FormulaElement } from './custom-types'
8 |
9 | // 生成 html 的函数
10 | function formulaToHtml(elem: SlateElement, childrenHtml: string): string {
11 | const { value = '' } = elem as FormulaElement
12 | return ``
13 | }
14 |
15 | // 配置
16 | const conf = {
17 | type: 'formula', // 节点 type ,重要!!!
18 | elemToHtml: formulaToHtml,
19 | }
20 |
21 | export default conf
22 |
--------------------------------------------------------------------------------
/test/module/plugin.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description formula plugin test
3 | * @author wangfupeng
4 | */
5 |
6 | import createEditor from '../utils/create-editor'
7 | import withFormula from '../../src/module/plugin'
8 | import { FormulaElement } from '../../src/module/custom-types'
9 |
10 | describe('formula plugin', () => {
11 | const editor = withFormula(createEditor())
12 | const formulaElem: FormulaElement = { type: 'formula', value: '123', children: [{ text: '' }] }
13 |
14 | it('isInline', () => {
15 | expect(editor.isInline(formulaElem)).toBe(true)
16 | })
17 |
18 | it('isVoid', () => {
19 | expect(editor.isVoid(formulaElem)).toBe(true)
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: test and build
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | - 'master'
8 | - 'dev'
9 | - 'feature-*'
10 | - 'fix-*'
11 | - 'hotfix-*'
12 | - 'refactor-*'
13 | paths:
14 | - '.github/workflows/*'
15 | - 'src/**'
16 | - 'example/**'
17 | - 'test/**'
18 | - 'build/**'
19 |
20 | jobs:
21 | publish-npm:
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v2
25 | - uses: actions/setup-node@v1
26 | with:
27 | node-version: 12
28 | registry-url: https://registry.npmjs.org/
29 | - run: npm i
30 | - run: npm run test
31 | - run: npm run build
32 |
--------------------------------------------------------------------------------
/src/module/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description formula module entry
3 | * @author wangfupeng
4 | */
5 |
6 | import './local' // 多语言
7 |
8 | import { IModuleConf } from '@wangeditor/editor'
9 | import withFormula from './plugin'
10 | import renderElemConf from './render-elem'
11 | import elemToHtmlConf from './elem-to-html'
12 | import parseHtmlConf from './parse-elem-html'
13 | import { insertFormulaMenuConf, editFormulaMenuConf } from './menu/index'
14 |
15 | const module: Partial = {
16 | editorPlugin: withFormula,
17 | renderElems: [renderElemConf],
18 | elemsToHtml: [elemToHtmlConf],
19 | parseElemsHtml: [parseHtmlConf],
20 | menus: [insertFormulaMenuConf, editFormulaMenuConf],
21 | }
22 |
23 | export default module
24 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | roots: ['/test'],
3 | testEnvironment: 'jsdom',
4 | testMatch: ['**/(*.)+(spec|test).+(ts|js|tsx)'],
5 | transform: {
6 | '^.+\\.tsx?$': 'ts-jest',
7 | '^.+\\.js$': 'ts-jest',
8 | },
9 | globals: {
10 | 'ts-jest': {
11 | tsconfig: '/tsconfig.json',
12 | },
13 | },
14 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
15 | moduleNameMapper: {
16 | '^.+\\.(css|less)$': '/test/setup/stylesMock.js',
17 |
18 | // import `dom7` 时默认是 esm 格式,换成 umd 格式
19 | dom7: '/node_modules/dom7/dom7.js',
20 | },
21 | transformIgnorePatterns: ['node_modules'],
22 | setupFilesAfterEnv: ['/test/setup/index.ts'],
23 | }
24 |
--------------------------------------------------------------------------------
/test/module/elem-to-html.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description formula elem-to-html test
3 | * @author wangfupeng
4 | */
5 |
6 | import elemToHtmlConf from '../../src/module/elem-to-html'
7 | import { FormulaElement } from '../../src/module/custom-types'
8 |
9 | describe('formula elem-to-html', () => {
10 | const formulaElem: FormulaElement = { type: 'formula', value: '123', children: [{ text: '' }] }
11 |
12 | it('type', () => {
13 | expect(elemToHtmlConf.type).toBe('formula')
14 | })
15 |
16 | it('elem to html', () => {
17 | const html = elemToHtmlConf.elemToHtml(formulaElem, '')
18 | expect(html).toBe(
19 | ``
20 | )
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/src/module/parse-elem-html.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description parse elem html
3 | * @author wangfupeng
4 | */
5 |
6 | import { DOMElement } from '../utils/dom'
7 | import { IDomEditor, SlateDescendant, SlateElement } from '@wangeditor/editor'
8 | import { FormulaElement } from './custom-types'
9 |
10 | function parseHtml(
11 | elem: DOMElement,
12 | children: SlateDescendant[],
13 | editor: IDomEditor
14 | ): SlateElement {
15 | const value = elem.getAttribute('data-value') || ''
16 | return {
17 | type: 'formula',
18 | value,
19 | children: [{ text: '' }], // void node 必须有一个空白 text
20 | } as FormulaElement
21 | }
22 |
23 | const parseHtmlConf = {
24 | selector: 'span[data-w-e-type="formula"]',
25 | parseElemHtml: parseHtml,
26 | }
27 |
28 | export default parseHtmlConf
29 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "ES2015",
5 | "lib": ["es6", "dom", "esnext"],
6 | "declaration": true,
7 | "sourceMap": true,
8 | "outDir": "dist",
9 | "strict": true,
10 | "noImplicitAny": true,
11 | "resolveJsonModule": true,
12 | "esModuleInterop": true,
13 | "moduleResolution": "node",
14 | "forceConsistentCasingInFileNames": true,
15 | "jsx": "react",
16 | "jsxFactory": "jsx",
17 | "downlevelIteration": true
18 | },
19 | "include": [
20 | "src/**/*",
21 | "example/**/*",
22 | "test/**/*"
23 | ],
24 | "exclude": [
25 | "node_modules",
26 | "build"
27 | ]
28 | }
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es6: true,
5 | mocha: true,
6 | jest: true,
7 | node: true,
8 | },
9 | extends: [
10 | 'eslint:recommended',
11 | 'plugin:@typescript-eslint/eslint-recommended',
12 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
13 | ],
14 | globals: {
15 | Atomics: 'readonly',
16 | SharedArrayBuffer: 'readonly',
17 | },
18 | parser: '@typescript-eslint/parser',
19 | parserOptions: {
20 | ecmaVersion: 2018,
21 | sourceType: 'module',
22 | },
23 | plugins: ['@typescript-eslint', 'prettier'],
24 | rules: {
25 | 'no-unused-vars': 0,
26 | },
27 | }
28 |
--------------------------------------------------------------------------------
/src/module/plugin.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description formula plugin
3 | * @author wangfupeng
4 | */
5 |
6 | import { DomEditor, IDomEditor } from '@wangeditor/editor'
7 |
8 | function withFormula(editor: T) {
9 | const { isInline, isVoid } = editor
10 | const newEditor = editor
11 |
12 | // 重写 isInline
13 | newEditor.isInline = elem => {
14 | const type = DomEditor.getNodeType(elem)
15 | if (type === 'formula') {
16 | return true
17 | }
18 |
19 | return isInline(elem)
20 | }
21 |
22 | // 重写 isVoid
23 | newEditor.isVoid = elem => {
24 | const type = DomEditor.getNodeType(elem)
25 | if (type === 'formula') {
26 | return true
27 | }
28 |
29 | return isVoid(elem)
30 | }
31 |
32 | return newEditor
33 | }
34 |
35 | export default withFormula
36 |
--------------------------------------------------------------------------------
/DEV.md:
--------------------------------------------------------------------------------
1 | # Dev doc
2 |
3 | ## 主要目录
4 |
5 | - `src` 源代码
6 | - `test` 单元测试
7 | - `example` 本地测试 demo ,不用于 build
8 | - `build` 打包配置
9 |
10 | ## dev 本地运行
11 |
12 | `yarn dev` 启动本地服务,**使用 example 目录**。
13 |
14 | `yarn test` 单元测试,使用 test 目录。
15 |
16 | ## build 构建
17 |
18 | `yarn build` 构建代码,**使用 src 目录**。
19 |
20 | ## release 发布
21 |
22 | 第一,升级 package.json 版本
23 |
24 | 第二,提交 git tag 可触发 github actions 并发布 npm
25 |
26 | ```sh
27 | git tag -a v1.0.1 -m "v1.0.1" # 和 package.json 版本同步即可
28 | git push origin --tags
29 | ```
30 |
31 | ## 注意事项
32 |
33 | package.json
34 | - 定义 `"main": "dist/index.js"`
35 | - 定义 `"module": "dist/index.js"`
36 | - 定义 `"types": "dist/src/index.d.ts"`
37 | - `@wangeditor/editor` 不要安装在 `dependencies` ,否则用户安装时也会安装它们
38 |
39 | webpack 配置
40 | - 定义 `library`
41 | - 定义 `externals` ,构建时忽略 `@wangeditor/editor` `katex` ,否则体积会很大
42 |
--------------------------------------------------------------------------------
/src/constants/icon-svg.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description icon svg
3 | * @author wangfupeng
4 | */
5 |
6 | /**
7 | * 【注意】svg 字符串的长度 ,否则会导致代码体积过大
8 | * 尽量选择 https://www.iconfont.cn/collections/detail?spm=a313x.7781069.0.da5a778a4&cid=20293
9 | * 找不到再从 iconfont.com 搜索
10 | */
11 |
12 | // 公式
13 | export const SIGMA_SVG = ``
14 |
15 | // 编辑
16 | export const PENCIL_SVG =
17 | ''
18 |
--------------------------------------------------------------------------------
/test/module/render-elem.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description formula render-elem test
3 | * @author wangfupeng
4 | */
5 |
6 | import createEditor from '../utils/create-editor'
7 | import renderElemConf from '../../src/module/render-elem'
8 | import { FormulaElement } from '../../src/module/custom-types'
9 |
10 | describe('formula render-elem', () => {
11 | const editor = createEditor()
12 | const formulaElem: FormulaElement = { type: 'formula', value: '123', children: [{ text: '' }] }
13 |
14 | it('type', () => {
15 | expect(renderElemConf.type).toBe('formula')
16 | })
17 |
18 | it('render elem', () => {
19 | const containerVnode = renderElemConf.renderElem(formulaElem, null, editor) as any
20 | expect(containerVnode.sel).toBe('div')
21 | expect(containerVnode.data.props.contentEditable).toBe(false)
22 |
23 | const formulaVnode = containerVnode.children[0]
24 | expect(formulaVnode.sel).toBe('w-e-formula-card')
25 | expect(formulaVnode.data.dataset.value).toBe('123')
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/test/module/parse-elem-html.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description parse elem html test
3 | * @author wangfupeng
4 | */
5 |
6 | import createEditor from '../utils/create-editor'
7 | import parseHtmlConf from '../../src/module/parse-elem-html'
8 | import { FormulaElement } from '../../src/module/custom-types'
9 |
10 | describe('parse elem html', () => {
11 | const editor = createEditor()
12 |
13 | it('selector', () => {
14 | expect(parseHtmlConf.selector).toBe('span[data-w-e-type="formula"]')
15 | })
16 |
17 | it('parse html', () => {
18 | const value = '123'
19 | // elem-to-html 产出的 html 格式:
20 | const elem = document.createElement('span')
21 | elem.setAttribute('data-w-e-type', 'formula')
22 | elem.setAttribute('data-value', value)
23 |
24 | const formula = parseHtmlConf.parseElemHtml(elem, [], editor) as FormulaElement
25 | expect(formula.type).toBe('formula')
26 | expect(formula.value).toBe(value)
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/src/utils/dom.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description DOM 操作
3 | * @author wangfupeng
4 | */
5 |
6 | import $, { append, html, val, on, focus, is, parents, find } from 'dom7'
7 |
8 | if (append) $.fn.append = append
9 | if (html) $.fn.html = html
10 | if (val) $.fn.val = val
11 | if (on) $.fn.on = on
12 | if (focus) $.fn.focus = focus
13 | if (is) $.fn.is = is
14 | if (parents) $.fn.parents = parents
15 | if (find) $.fn.find = find
16 |
17 | export { Dom7Array } from 'dom7'
18 | export default $
19 |
20 | // COMPAT: This is required to prevent TypeScript aliases from doing some very
21 | // weird things for Slate's types with the same name as globals. (2019/11/27)
22 | // https://github.com/microsoft/TypeScript/issues/35002
23 | import DOMNode = globalThis.Node
24 | import DOMComment = globalThis.Comment
25 | import DOMElement = globalThis.Element
26 | import DOMText = globalThis.Text
27 | import DOMRange = globalThis.Range
28 | import DOMSelection = globalThis.Selection
29 | import DOMStaticRange = globalThis.StaticRange
30 | export { DOMNode, DOMComment, DOMElement, DOMText, DOMRange, DOMSelection, DOMStaticRange }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 wangEditor 团队
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/module/render-elem.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description render elem
3 | * @author wangfupeng
4 | */
5 |
6 | import { h, VNode } from 'snabbdom'
7 | import { DomEditor, IDomEditor, SlateElement } from '@wangeditor/editor'
8 | import { FormulaElement } from './custom-types'
9 |
10 | function renderFormula(elem: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode {
11 | // 当前节点是否选中
12 | const selected = DomEditor.isNodeSelected(editor, elem)
13 |
14 | // 构建 formula vnode
15 | const { value = '' } = elem as FormulaElement
16 | const formulaVnode = h(
17 | 'w-e-formula-card',
18 | {
19 | dataset: { value },
20 | },
21 | null
22 | )
23 |
24 | // 构建容器 vnode
25 | const containerVnode = h(
26 | 'div',
27 | {
28 | props: {
29 | contentEditable: false, // 不可编辑
30 | },
31 | style: {
32 | display: 'inline-block', // inline
33 | marginLeft: '3px',
34 | marginRight: '3px',
35 | border: selected // 选中/不选中,样式不一样
36 | ? '2px solid var(--w-e-textarea-selected-border-color)' // wangEditor 提供了 css var https://www.wangeditor.com/v5/theme.html
37 | : '2px solid transparent',
38 | borderRadius: '3px',
39 | padding: '3px 3px',
40 | },
41 | },
42 | [formulaVnode]
43 | )
44 |
45 | return containerVnode
46 | }
47 |
48 | const conf = {
49 | type: 'formula', // 节点 type ,重要!!!
50 | renderElem: renderFormula,
51 | }
52 |
53 | export default conf
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # wangEditor 公式
2 |
3 | [English Documentation](./README-en.md)
4 |
5 | ## 介绍
6 |
7 | [wangEditor](https://www.wangeditor.com/) 公式插件,使用 [LateX](https://baike.baidu.com/item/LaTeX/1212106) 语法。
8 |
9 | 
10 |
11 | ## 安装
12 |
13 | ```shell
14 | yarn add katex
15 | yarn add @wangeditor/plugin-formula
16 | ```
17 |
18 | ## 使用
19 |
20 | ### 注册到编辑器
21 |
22 | ```js
23 | import { Boot, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
24 | import formulaModule from '@wangeditor/plugin-formula'
25 |
26 | // 注册。要在创建编辑器之前注册,且只能注册一次,不可重复注册。
27 | Boot.registerModule(formulaModule)
28 | ```
29 |
30 | ### 配置
31 |
32 | ```js
33 | // 编辑器配置
34 | const editorConfig: Partial = {
35 | // 选中公式时的悬浮菜单
36 | hoverbarKeys: {
37 | formula: {
38 | menuKeys: ['editFormula'], // “编辑公式”菜单
39 | },
40 | },
41 |
42 | // 其他...
43 | }
44 |
45 | // 工具栏配置
46 | const toolbarConfig: Partial = {
47 | insertKeys: {
48 | index: 0,
49 | keys: [
50 | 'insertFormula', // “插入公式”菜单
51 | // 'editFormula' // “编辑公式”菜单
52 | ],
53 | },
54 |
55 | // 其他...
56 | }
57 | ```
58 |
59 | 然后创建编辑器和工具栏,会用到 `editorConfig` 和 `toolbarConfig` 。具体查看 wangEditor 文档。
60 |
61 | ### 显示 HTML
62 |
63 | 公式获取的 HTML 格式如下
64 |
65 | ```html
66 |
67 | ```
68 |
69 | 其中 `data-value` 就是 LateX 格式的值,可使用第三方工具把 `` 渲染成公式卡片,如 [KateX](https://katex.org/)。
70 |
71 | ## 其他
72 |
73 | 支持 i18n 多语言
74 |
--------------------------------------------------------------------------------
/test/module/menu/insert-formula.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description insert formula menu test
3 | * @author wangfupeng
4 | */
5 |
6 | import { SlateEditor } from '@wangeditor/editor'
7 | import createEditor from '../../utils/create-editor'
8 | import { FormulaElement } from '../../../src/module/custom-types'
9 | import InsertFormulaMenu from '../../../src/module/menu/InsertFormula'
10 | import withFormula from '../../../src/module/plugin'
11 |
12 | describe('insert formula menu', () => {
13 | const editor = withFormula(createEditor())
14 | const startLocation = SlateEditor.start(editor, [])
15 | const menu = new InsertFormulaMenu()
16 |
17 | function genFormulaElem() {
18 | const formulaElem: FormulaElement = { type: 'formula', value: '123', children: [{ text: '' }] }
19 | return formulaElem
20 | }
21 |
22 | it('getValue', () => {
23 | expect(menu.getValue(editor)).toBe('')
24 | })
25 |
26 | it('isActive', () => {
27 | expect(menu.isActive(editor)).toBe(false)
28 | })
29 |
30 | it('isDisabled', () => {
31 | // 选中空编辑器
32 | editor.select(startLocation)
33 | expect(menu.isDisabled(editor)).toBeFalsy()
34 |
35 | // 选中 formula 节点
36 | editor.insertNode(genFormulaElem())
37 | editor.select({ path: [0, 1, 0], offset: 0 })
38 | expect(menu.isDisabled(editor)).toBeTruthy()
39 | })
40 |
41 | it('getModalPositionNode', () => {
42 | expect(menu.getModalPositionNode(editor)).toBeNull()
43 | })
44 |
45 | it('getModalContentElem', () => {
46 | const elem = menu.getModalContentElem(editor)
47 | expect(elem.tagName).toBe('DIV')
48 | })
49 | })
50 |
--------------------------------------------------------------------------------
/example/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description examples entry
3 | * @author wangfupeng
4 | */
5 |
6 | import { createEditor, createToolbar, Boot, i18nChangeLanguage } from '@wangeditor/editor'
7 | import module from '../src/index'
8 |
9 | Boot.registerModule(module)
10 |
11 | // i18nChangeLanguage('en')
12 |
13 | // 创建编辑器
14 | const editor = createEditor({
15 | selector: '#editor-container',
16 | config: {
17 | hoverbarKeys: {
18 | formula: {
19 | menuKeys: ['editFormula'], // “编辑”菜单
20 | },
21 | },
22 | onChange(editor) {
23 | const html = editor.getHtml()
24 | // @ts-ignore
25 | document.getElementById('text-html').value = html
26 | const contentStr = JSON.stringify(editor.children, null, 2)
27 | // @ts-ignore
28 | document.getElementById('text-json').value = contentStr
29 | },
30 | },
31 | // content: [
32 | // {
33 | // // @ts-ignore
34 | // type: 'paragraph',
35 | // children: [
36 | // { text: 'hello world' },
37 | // // @ts-ignore
38 | // { type: 'formula', value: 'c = \\pm\\sqrt{a^1 + b^2}', children: [{ text: '' }] },
39 | // ],
40 | // },
41 | // ],
42 | html: `hello world100
`,
43 | })
44 | const toolbar = createToolbar({
45 | editor,
46 | selector: '#toolbar-container',
47 | config: {
48 | insertKeys: {
49 | index: 0,
50 | keys: ['insertFormula'], // “插入”菜单
51 | },
52 | },
53 | })
54 |
55 | // @ts-ignore 为了便于调试,暴露到 window
56 | window.editor = editor
57 | // @ts-ignore
58 | window.toolbar = toolbar
59 |
--------------------------------------------------------------------------------
/.cz-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | types: [
3 | {
4 | value: 'WIP',
5 | name: '💡 WIP: Work in progress',
6 | },
7 | {
8 | value: 'feat',
9 | name: '🚀 feat: A new feature',
10 | },
11 | {
12 | value: 'fix',
13 | name: '🔧 fix: A bug fix',
14 | },
15 | {
16 | value: 'refactor',
17 | name: '🔨 refactor: A code change that neither fixes a bug nor adds a feature',
18 | },
19 | {
20 | value: 'release',
21 | name: '🛳 release: Bump to a new Semantic version',
22 | },
23 | {
24 | value: 'docs',
25 | name: '📚 docs: Documentation only changes',
26 | },
27 | {
28 | value: 'test',
29 | name: '🔍 test: Add missing tests or correcting existing tests',
30 | },
31 | {
32 | value: 'perf',
33 | name: '⚡️ perf: Changes that improve performance',
34 | },
35 | {
36 | value: 'chore',
37 | name:
38 | "🚬 chore: Changes that don't modify src or test files. Such as updating build tasks, package manager",
39 | },
40 | {
41 | value: 'workflow',
42 | name:
43 | '📦 workflow: Changes that only affect the workflow. Such as updating build systems or CI etc.',
44 | },
45 | {
46 | value: 'style',
47 | name:
48 | '💅 style: Code Style, Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)',
49 | },
50 | {
51 | value: 'revert',
52 | name: '⏱ revert: Revert to a commit',
53 | },
54 | ],
55 | // Specify the scopes for your particular project
56 | scopes: [],
57 | allowCustomScopes: true,
58 | allowBreakingChanges: ['feat', 'fix'],
59 | }
60 |
--------------------------------------------------------------------------------
/README-en.md:
--------------------------------------------------------------------------------
1 | # wangEditor formula module
2 |
3 | [中文文档](./README.md)
4 |
5 | ## Introduction
6 |
7 | [wangEditor](https://www.wangeditor.com/en/) Formula plugin, use `LateX` syntax.
8 |
9 | 
10 |
11 | ## Installation
12 |
13 | ```shell
14 | yarn add katex
15 | yarn add @wangeditor/plugin-formula
16 | ```
17 |
18 | ## Usage
19 |
20 | ### Register to editor
21 |
22 |
23 | ```js
24 | import { Boot, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
25 | import formulaModule from '@wangeditor/plugin-formula'
26 |
27 | // Register
28 | // You should register this before create editor, and register only once (not repeatedly).
29 | Boot.registerModule(formulaModule)
30 | ```
31 |
32 | ### Menu config
33 |
34 | ```js
35 | const editorConfig: Partial = {
36 | // Hoverbar keys when selected a formula node.
37 | hoverbarKeys: {
38 | formula: {
39 | menuKeys: ['editFormula'], // “编辑公式”菜单
40 | },
41 | },
42 |
43 | // others...
44 | }
45 |
46 | const toolbarConfig: Partial = {
47 | insertKeys: {
48 | index: 0,
49 | keys: [
50 | 'insertFormula', // Insert formula menu
51 | // 'editFormula' // Edit formula menu
52 | ],
53 | },
54 |
55 | // others...
56 | }
57 | ```
58 |
59 | Then create editor and toolbar, you will use `editorConfig` and `toolbarConfig`
60 |
61 | ### Render HTML
62 |
63 | You will get a formula's HTML format like this
64 |
65 | ```html
66 |
67 | ```
68 |
69 | Dateset `data-value` is the `LateX` syntax value, you can use a third-party lib to render formula card, like [KateX](https://katex.org/).
70 |
71 | ## Others
72 |
73 | Support i18n.
74 |
--------------------------------------------------------------------------------
/src/register-custom-elem/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description 注册自定义 elem
3 | * @author wangfupeng
4 | */
5 |
6 | // @ts-ignore
7 | import katexStyleContent from '!!raw-loader!katex/dist/katex.css'
8 | import katex from 'katex'
9 | import './native-shim'
10 |
11 | class WangEditorFormulaCard extends HTMLElement {
12 | private span: HTMLElement
13 |
14 | // 监听的 attr
15 | static get observedAttributes() {
16 | return ['data-value']
17 | }
18 |
19 | constructor() {
20 | super()
21 | const shadow = this.attachShadow({ mode: 'open' })
22 | const document = shadow.ownerDocument
23 |
24 | const style = document.createElement('style')
25 | style.innerHTML = katexStyleContent // 加载 css 文本
26 | shadow.appendChild(style)
27 |
28 | const span = document.createElement('span')
29 | span.style.display = 'inline-block'
30 | shadow.appendChild(span)
31 | this.span = span
32 | }
33 |
34 | // connectedCallback() {
35 | // // 当 custom element首次被插入文档DOM时,被调用
36 | // console.log('connected')
37 | // }
38 | // disconnectedCallback() {
39 | // // 当 custom element从文档DOM中删除时,被调用
40 | // console.log('disconnected')
41 | // }
42 | // adoptedCallback() {
43 | // // 当 custom element被移动到新的文档时,被调用
44 | // console.log('adopted')
45 | // }
46 | attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
47 | if (name === 'data-value') {
48 | if (oldValue == newValue) return
49 | this.render(newValue || '')
50 | }
51 | }
52 |
53 | private render(value: string) {
54 | katex.render(value, this.span, {
55 | throwOnError: false,
56 | })
57 | }
58 | }
59 |
60 | if (!window.customElements.get('w-e-formula-card')) {
61 | window.customElements.define('w-e-formula-card', WangEditorFormulaCard)
62 | }
63 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | wangEditor formula demo
9 |
10 |
11 |
16 |
17 |
18 |
19 | wangEditor formula demo
20 | LateX 语法示例
21 |
22 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | ISSUE.md
107 |
--------------------------------------------------------------------------------
/test/module/menu/edit-formula.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description edit formula menu test
3 | * @author wangfupeng
4 | */
5 |
6 | import { SlateEditor, DomEditor, SlateNode } from '@wangeditor/editor'
7 | import createEditor from '../../utils/create-editor'
8 | import { FormulaElement } from '../../../src/module/custom-types'
9 | import EditorFormulaMenu from '../../../src/module/menu/EditFormula'
10 | import withFormula from '../../../src/module/plugin'
11 |
12 | describe('edit formula menu', () => {
13 | let editor = withFormula(createEditor())
14 | const startLocation = SlateEditor.start(editor, [])
15 | const menu = new EditorFormulaMenu()
16 |
17 | function genFormulaElem() {
18 | const formulaElem: FormulaElement = { type: 'formula', value: '123', children: [{ text: '' }] }
19 | return formulaElem
20 | }
21 |
22 | beforeEach(() => {
23 | editor = withFormula(createEditor())
24 | })
25 |
26 | it('getValue', () => {
27 | // 选中空编辑器
28 | editor.select(startLocation)
29 | expect(menu.getValue(editor)).toBe('')
30 |
31 | // 选中 formula 节点
32 | editor.insertNode(genFormulaElem())
33 | editor.select({ path: [0, 1, 0], offset: 0 })
34 | expect(menu.getValue(editor)).toBe('123')
35 | })
36 |
37 | it('isActive', () => {
38 | expect(menu.isActive(editor)).toBe(false)
39 | })
40 |
41 | it('isDisabled', () => {
42 | // 选中空编辑器
43 | editor.select(startLocation)
44 | expect(menu.isDisabled(editor)).toBeTruthy()
45 |
46 | // 选中 formula 节点
47 | editor.insertNode(genFormulaElem())
48 | editor.select({ path: [0, 1, 0], offset: 0 })
49 | expect(menu.isDisabled(editor)).toBeFalsy()
50 | })
51 |
52 | it('getModalPositionNode', () => {
53 | editor.select(startLocation)
54 | editor.insertNode(genFormulaElem())
55 | editor.select({ path: [0, 1, 0], offset: 0 })
56 |
57 | const positionNode = menu.getModalPositionNode(editor)
58 | expect(positionNode).not.toBeNull()
59 | expect(DomEditor.getNodeType(positionNode as SlateNode)).toBe('formula')
60 | })
61 |
62 | it('getModalContentElem', () => {
63 | editor.select(startLocation)
64 | editor.insertNode(genFormulaElem())
65 | editor.select({ path: [0, 1, 0], offset: 0 })
66 |
67 | const modalElem = menu.getModalContentElem(editor)
68 | expect(modalElem.tagName).toBe('DIV')
69 | })
70 | })
71 |
--------------------------------------------------------------------------------
/src/register-custom-elem/native-shim.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | // 参考 https://github.com/webcomponents/custom-elements/blob/master/src/native-shim.js
4 |
5 | /**
6 | * @license
7 | * Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
8 | * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
9 | * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
10 | * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
11 | * Code distributed by Google as part of the polymer project is also
12 | * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
13 | */
14 |
15 | /**
16 | * This shim allows elements written in, or compiled to, ES5 to work on native
17 | * implementations of Custom Elements v1. It sets new.target to the value of
18 | * this.constructor so that the native HTMLElement constructor can access the
19 | * current under-construction element's definition.
20 | */
21 | ;(function () {
22 | if (
23 | // No Reflect, no classes, no need for shim because native custom elements
24 | // require ES2015 classes or Reflect.
25 | window.Reflect === undefined ||
26 | window.customElements === undefined ||
27 | // The webcomponentsjs custom elements polyfill doesn't require
28 | // ES2015-compatible construction (`super()` or `Reflect.construct`).
29 | window.customElements.polyfillWrapFlushCallback
30 | ) {
31 | return
32 | }
33 | const BuiltInHTMLElement = HTMLElement
34 | /**
35 | * With jscompiler's RECOMMENDED_FLAGS the function name will be optimized away.
36 | * However, if we declare the function as a property on an object literal, and
37 | * use quotes for the property name, then closure will leave that much intact,
38 | * which is enough for the JS VM to correctly set Function.prototype.name.
39 | */
40 | const wrapperForTheName = {
41 | HTMLElement: /** @this {!Object} */ function HTMLElement() {
42 | return Reflect.construct(BuiltInHTMLElement, [], /** @type {!Function} */ this.constructor)
43 | },
44 | }
45 | window.HTMLElement = wrapperForTheName['HTMLElement']
46 | HTMLElement.prototype = BuiltInHTMLElement.prototype
47 | HTMLElement.prototype.constructor = HTMLElement
48 | Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement)
49 | })()
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@wangeditor/plugin-formula",
3 | "version": "1.0.11",
4 | "description": "wangEditor formula 公式",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/wangeditor-team/wangEditor-plugin-formula.git"
8 | },
9 | "keywords": [
10 | "公式",
11 | "formula",
12 | "wangeditor"
13 | ],
14 | "author": "github.com/wangfupeng1988",
15 | "license": "MIT",
16 | "bugs": {
17 | "url": "https://github.com/wangeditor-team/wangEditor-plugin-formula/issues"
18 | },
19 | "homepage": "https://github.com/wangeditor-team/wangEditor-plugin-formula#readme",
20 | "main": "dist/index.js",
21 | "module": "dist/index.js",
22 | "types": "dist/src/index.d.ts",
23 | "scripts": {
24 | "test": "cross-env NODE_OPTIONS=--unhandled-rejections=warn jest --detectOpenHandles --passWithNoTests",
25 | "test-c": "cross-env NODE_OPTIONS=--unhandled-rejections=warn jest --coverage",
26 | "dev": "cross-env NODE_ENV=development webpack serve --config build/webpack.dev.js",
27 | "build": "cross-env NODE_ENV=production webpack --config build/webpack.prod.js",
28 | "build:analyzer": "cross-env NODE_ENV=production_analyzer webpack --config build/webpack.prod.js",
29 | "release": "release-it",
30 | "format": "yarn prettier --write",
31 | "lint": "eslint \"{src,test,cypress,build,example}/**/*.{js,ts}\"",
32 | "lint-fix": "eslint --fix \"{src,test,cypress,build,example}/**/*.{js,ts}\"",
33 | "prettier": "prettier --write --config .prettierrc.js \"{src,test,cypress,build,example}/**/*.{js,ts}\""
34 | },
35 | "lint-staged": {
36 | "*.{ts,tsx,js}": [
37 | "yarn prettier",
38 | "yarn lint",
39 | "yarn test"
40 | ]
41 | },
42 | "husky": {
43 | "hooks": {
44 | "pre-commit": "lint-staged",
45 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
46 | }
47 | },
48 | "config": {
49 | "commitizen": {
50 | "path": "node_modules/cz-customizable"
51 | }
52 | },
53 | "devDependencies": {
54 | "@babel/core": "^7.13.14",
55 | "@babel/preset-env": "^7.13.12",
56 | "@testing-library/jest-dom": "^5.16.2",
57 | "@types/jest": "^27.4.0",
58 | "@types/katex": "^0.11.1",
59 | "@typescript-eslint/eslint-plugin": "^5.12.0",
60 | "@typescript-eslint/parser": "^5.12.0",
61 | "@wangeditor/editor": "^5.0.0",
62 | "autoprefixer": "^10.2.5",
63 | "babel-jest": "^27.3.1",
64 | "babel-loader": "^8.2.2",
65 | "clean-webpack-plugin": "^3.0.0",
66 | "commitlint": "^16.2.1",
67 | "commitlint-config-cz": "^0.13.3",
68 | "cross-env": "^7.0.3",
69 | "crypto": "^1.0.1",
70 | "css-loader": "^5.2.0",
71 | "cz-customizable": "^6.3.0",
72 | "eslint": "^8.9.0",
73 | "eslint-config-prettier": "^8.3.0",
74 | "eslint-plugin-prettier": "^4.0.0",
75 | "html-webpack-plugin": "^5.3.1",
76 | "husky": "^7.0.4",
77 | "jest": "^27.5.1",
78 | "jest-environment-jsdom": "^27.5.1",
79 | "katex": "^0.15.2",
80 | "less": "^4.1.1",
81 | "less-loader": "^8.0.0",
82 | "lint-staged": "^12.3.4",
83 | "postcss-loader": "^5.2.0",
84 | "prettier": "^2.5.1",
85 | "raw-loader": "^4.0.2",
86 | "release-it": "^14.11.6",
87 | "snabbdom": "^3.3.1",
88 | "style-loader": "^2.0.0",
89 | "ts-jest": "^27.0.7",
90 | "ts-loader": "^8.1.0",
91 | "typescript": "^4.2.3",
92 | "url-loader": "^4.1.1",
93 | "webpack": "^5.30.0",
94 | "webpack-bundle-analyzer": "^4.4.0",
95 | "webpack-cli": "^4.6.0",
96 | "webpack-dev-server": "^3.11.2",
97 | "webpack-merge": "^5.7.3"
98 | },
99 | "peerDependencies": {
100 | "@wangeditor/editor": ">=5.0.0",
101 | "katex": "^0.15.2",
102 | "snabbdom": "^3.3.1"
103 | },
104 | "dependencies": {
105 | "dom7": "^4.0.4",
106 | "nanoid": "^3.2.0"
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/module/menu/InsertFormula.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description insert formula menu
3 | * @author wangfupeng
4 | */
5 |
6 | import { IModalMenu } from '@wangeditor/editor'
7 | import {
8 | DomEditor,
9 | IDomEditor,
10 | SlateNode,
11 | SlateRange,
12 | t,
13 | genModalTextareaElems,
14 | genModalButtonElems,
15 | } from '@wangeditor/editor'
16 | import { SIGMA_SVG } from '../../constants/icon-svg'
17 | import $, { Dom7Array, DOMElement } from '../../utils/dom'
18 | import { genRandomStr } from '../../utils/util'
19 | import { FormulaElement } from '../custom-types'
20 |
21 | /**
22 | * 生成唯一的 DOM ID
23 | */
24 | function genDomID(): string {
25 | return genRandomStr('w-e-insert-formula')
26 | }
27 |
28 | class InsertFormulaMenu implements IModalMenu {
29 | readonly title = t('formula.insert')
30 | readonly iconSvg = SIGMA_SVG
31 | readonly tag = 'button'
32 | readonly showModal = true // 点击 button 时显示 modal
33 | readonly modalWidth = 300
34 | private $content: Dom7Array | null = null
35 | private readonly textareaId = genDomID()
36 | private readonly buttonId = genDomID()
37 |
38 | getValue(editor: IDomEditor): string | boolean {
39 | // 插入菜单,不需要 value
40 | return ''
41 | }
42 |
43 | isActive(editor: IDomEditor): boolean {
44 | // 任何时候,都不用激活 menu
45 | return false
46 | }
47 |
48 | exec(editor: IDomEditor, value: string | boolean) {
49 | // 点击菜单时,弹出 modal 之前,不需要执行其他代码
50 | // 此处空着即可
51 | }
52 |
53 | isDisabled(editor: IDomEditor): boolean {
54 | const { selection } = editor
55 | if (selection == null) return true
56 | if (SlateRange.isExpanded(selection)) return true // 选区非折叠,禁用
57 |
58 | const selectedElems = DomEditor.getSelectedElems(editor)
59 |
60 | const hasVoidElem = selectedElems.some(elem => editor.isVoid(elem))
61 | if (hasVoidElem) return true // 选中了 void 元素,禁用
62 |
63 | const hasPreElem = selectedElems.some(elem => DomEditor.getNodeType(elem) === 'pre')
64 | if (hasPreElem) return true // 选中了 pre 原则,禁用
65 |
66 | return false
67 | }
68 |
69 | getModalPositionNode(editor: IDomEditor): SlateNode | null {
70 | return null // modal 依据选区定位
71 | }
72 |
73 | getModalContentElem(editor: IDomEditor): DOMElement {
74 | const { textareaId, buttonId } = this
75 |
76 | const [textareaContainerElem, textareaElem] = genModalTextareaElems(
77 | t('formula.formula'),
78 | textareaId,
79 | t('formula.placeholder')
80 | )
81 | const $textarea = $(textareaElem)
82 | const [buttonContainerElem] = genModalButtonElems(buttonId, t('formula.ok'))
83 |
84 | if (this.$content == null) {
85 | // 第一次渲染
86 | const $content = $('')
87 |
88 | // 绑定事件(第一次渲染时绑定,不要重复绑定)
89 | $content.on('click', `#${buttonId}`, e => {
90 | e.preventDefault()
91 | const value = $content.find(`#${textareaId}`).val().trim()
92 | this.insertFormula(editor, value)
93 | editor.hidePanelOrModal() // 隐藏 modal
94 | })
95 |
96 | // 记录属性,重要
97 | this.$content = $content
98 | }
99 |
100 | const $content = this.$content
101 | $content.html('') // 先清空内容
102 |
103 | // append textarea and button
104 | $content.append(textareaContainerElem)
105 | $content.append(buttonContainerElem)
106 |
107 | // 设置 input val
108 | $textarea.val('')
109 |
110 | // focus 一个 input(异步,此时 DOM 尚未渲染)
111 | setTimeout(() => {
112 | $textarea.focus()
113 | })
114 |
115 | return $content[0]
116 | }
117 |
118 | private insertFormula(editor: IDomEditor, value: string) {
119 | if (!value) return
120 |
121 | // 还原选区
122 | editor.restoreSelection()
123 |
124 | if (this.isDisabled(editor)) return
125 |
126 | const formulaElem: FormulaElement = {
127 | type: 'formula',
128 | value,
129 | children: [{ text: '' }], // void node 需要有一个空 text
130 | }
131 | editor.insertNode(formulaElem)
132 | }
133 | }
134 |
135 | export default InsertFormulaMenu
136 |
--------------------------------------------------------------------------------
/src/module/menu/EditFormula.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description edit formula menu
3 | * @author wangfupeng
4 | */
5 |
6 | import { IModalMenu } from '@wangeditor/editor'
7 | import {
8 | DomEditor,
9 | IDomEditor,
10 | SlateNode,
11 | SlateTransforms,
12 | SlateRange,
13 | t,
14 | genModalTextareaElems,
15 | genModalButtonElems,
16 | } from '@wangeditor/editor'
17 | import { PENCIL_SVG } from '../../constants/icon-svg'
18 | import $, { Dom7Array, DOMElement } from '../../utils/dom'
19 | import { genRandomStr } from '../../utils/util'
20 | import { FormulaElement } from '../custom-types'
21 |
22 | /**
23 | * 生成唯一的 DOM ID
24 | */
25 | function genDomID(): string {
26 | return genRandomStr('w-e-insert-formula')
27 | }
28 |
29 | class EditFormulaMenu implements IModalMenu {
30 | readonly title = t('formula.edit')
31 | readonly iconSvg = PENCIL_SVG
32 | readonly tag = 'button'
33 | readonly showModal = true // 点击 button 时显示 modal
34 | readonly modalWidth = 300
35 | private $content: Dom7Array | null = null
36 | private readonly textareaId = genDomID()
37 | private readonly buttonId = genDomID()
38 |
39 | private getSelectedElem(editor: IDomEditor): FormulaElement | null {
40 | const node = DomEditor.getSelectedNodeByType(editor, 'formula')
41 | if (node == null) return null
42 | return node as FormulaElement
43 | }
44 |
45 | /**
46 | * 获取公式 value
47 | * @param editor editor
48 | */
49 | getValue(editor: IDomEditor): string | boolean {
50 | const formulaElem = this.getSelectedElem(editor)
51 | if (formulaElem) {
52 | return formulaElem.value || ''
53 | }
54 | return ''
55 | }
56 |
57 | isActive(editor: IDomEditor): boolean {
58 | // 无需 active
59 | return false
60 | }
61 |
62 | exec(editor: IDomEditor, value: string | boolean) {
63 | // 点击菜单时,弹出 modal 之前,不需要执行其他代码
64 | // 此处空着即可
65 | }
66 |
67 | isDisabled(editor: IDomEditor): boolean {
68 | const { selection } = editor
69 | if (selection == null) return true
70 | if (SlateRange.isExpanded(selection)) return true // 选区非折叠,禁用
71 |
72 | // 未匹配到 formula node 则禁用
73 | const formulaElem = this.getSelectedElem(editor)
74 | if (formulaElem == null) return true
75 |
76 | return false
77 | }
78 |
79 | // modal 定位
80 | getModalPositionNode(editor: IDomEditor): SlateNode | null {
81 | return this.getSelectedElem(editor)
82 | }
83 |
84 | getModalContentElem(editor: IDomEditor): DOMElement {
85 | const { textareaId, buttonId } = this
86 |
87 | const [textareaContainerElem, textareaElem] = genModalTextareaElems(
88 | t('formula.formula'),
89 | textareaId,
90 | t('formula.placeholder')
91 | )
92 | const $textarea = $(textareaElem)
93 | const [buttonContainerElem] = genModalButtonElems(buttonId, t('formula.ok'))
94 |
95 | if (this.$content == null) {
96 | // 第一次渲染
97 | const $content = $('')
98 |
99 | // 绑定事件(第一次渲染时绑定,不要重复绑定)
100 | $content.on('click', `#${buttonId}`, e => {
101 | e.preventDefault()
102 | const value = $content.find(`#${textareaId}`).val().trim()
103 | this.updateFormula(editor, value)
104 | editor.hidePanelOrModal() // 隐藏 modal
105 | })
106 |
107 | // 记录属性,重要
108 | this.$content = $content
109 | }
110 |
111 | const $content = this.$content
112 | $content.html('') // 先清空内容
113 |
114 | // append textarea and button
115 | $content.append(textareaContainerElem)
116 | $content.append(buttonContainerElem)
117 |
118 | // 设置 input val
119 | const value = this.getValue(editor)
120 | $textarea.val(value)
121 |
122 | // focus 一个 input(异步,此时 DOM 尚未渲染)
123 | setTimeout(() => {
124 | $textarea.focus()
125 | })
126 |
127 | return $content[0]
128 | }
129 |
130 | private updateFormula(editor: IDomEditor, value: string) {
131 | if (!value) return
132 |
133 | // 还原选区
134 | editor.restoreSelection()
135 |
136 | if (this.isDisabled(editor)) return
137 |
138 | const selectedElem = this.getSelectedElem(editor)
139 | if (selectedElem == null) return
140 |
141 | const path = DomEditor.findPath(editor, selectedElem)
142 | const props: Partial = { value }
143 | SlateTransforms.setNodes(editor, props, { at: path })
144 | }
145 | }
146 |
147 | export default EditFormulaMenu
148 |
--------------------------------------------------------------------------------