├── 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 | ![](./_img/demo.png) 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 | ![](./_img/demo.png) 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 |
23 |
24 |
25 |
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 | --------------------------------------------------------------------------------