├── .browserslistrc ├── .cz-config.js ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── feature.md │ └── question.md └── workflows │ ├── deploy-demos.yml │ ├── deploy-examples.yml.bak │ ├── e2e.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── .vscode └── settings.json ├── .yarnrc ├── CHANGELOG.md ├── LICENSE ├── README-en.md ├── README.md ├── babel.config.json ├── build ├── build-all.sh ├── config │ ├── common.js │ ├── dev.js │ └── prd.js └── create-rollup-config.js ├── commitlint.config.js ├── cypress.json ├── cypress ├── cypress.d.ts ├── fixtures │ └── example.json ├── integration │ └── editor.spec.ts ├── plugins │ └── index.ts ├── support │ ├── commands.ts │ └── index.ts └── tsconfig.json ├── docs ├── README.md ├── dev.md ├── images │ ├── cypress-run.jpg │ ├── cypress.jpg │ ├── editor-en.png │ └── editor.png ├── join.md ├── publish.md └── test.md ├── jest.config.js ├── lerna.json ├── package.json ├── packages ├── basic-modules │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ ├── blockquote │ │ │ ├── blockquote-menu.test.ts │ │ │ ├── elem-to-html.test.ts │ │ │ ├── parse-html.test.ts │ │ │ ├── plugin.test.ts │ │ │ └── render-elem.test.ts │ │ ├── code-block │ │ │ ├── code-block-menu.test.ts │ │ │ ├── elem-to-html.test.ts │ │ │ ├── parse-html.test.ts │ │ │ ├── plugin.test.ts │ │ │ └── render-elem.test.ts │ │ ├── color │ │ │ ├── color-menus.test.ts │ │ │ ├── parse-html.test.ts │ │ │ ├── render-text-style.test.tsx │ │ │ └── text-style-to-html.test.ts │ │ ├── divider │ │ │ ├── elem-to-html.test.ts │ │ │ ├── insert-divider-menu.test.ts │ │ │ ├── parse-html.test.ts │ │ │ ├── plugin.test.ts │ │ │ └── render-elem.test.ts │ │ ├── emotion │ │ │ └── emotion-menu.test.ts │ │ ├── font-size-family │ │ │ ├── menu │ │ │ │ ├── font-family-menu.test.ts │ │ │ │ └── font-size-menu.test.ts │ │ │ ├── parse-html.test.ts │ │ │ ├── render-text-style.test.tsx │ │ │ └── text-style-to-html.test.ts │ │ ├── full-screen │ │ │ └── full-screen-menu.test.ts │ │ ├── header │ │ │ ├── elem-to-html.test.ts │ │ │ ├── helper.test.ts │ │ │ ├── menu │ │ │ │ ├── header-select-menu.test.ts │ │ │ │ └── header1-menu.test.ts │ │ │ ├── parse-html.test.ts │ │ │ ├── plugin.test.ts │ │ │ └── render-elem.test.ts │ │ ├── image │ │ │ ├── elem-to-html.test.ts │ │ │ ├── helper.test.ts │ │ │ ├── menu │ │ │ │ ├── del-image.test.ts │ │ │ │ ├── edit-image.test.ts │ │ │ │ ├── insert-image.test.ts │ │ │ │ ├── view-image-link.test.ts │ │ │ │ └── width-menus.test.ts │ │ │ ├── parse-html.test.ts │ │ │ ├── plugin.test.ts │ │ │ └── render-elem.test.ts │ │ ├── indent │ │ │ ├── menu │ │ │ │ ├── decrease-indent-menu.test.ts │ │ │ │ └── increase-indent-menu.test.ts │ │ │ ├── parse-html.test.ts │ │ │ ├── render-text-style.test.tsx │ │ │ └── text-style-to-html.test.ts │ │ ├── justify │ │ │ ├── menus.test.ts │ │ │ ├── parse-html.test.ts │ │ │ ├── render-text-style.test.tsx │ │ │ └── text-style-to-html.test.ts │ │ ├── line-height │ │ │ ├── line-height-menu.test.ts │ │ │ ├── parse-html.test.ts │ │ │ ├── render-text-style.test.tsx │ │ │ └── text-style-to-html.test.ts │ │ ├── link │ │ │ ├── elem-to-html.test.ts │ │ │ ├── helper.test.ts │ │ │ ├── menu │ │ │ │ ├── edit-link-menu.test.ts │ │ │ │ ├── insert-link-menu.test.ts │ │ │ │ ├── unlink-menu.test.ts │ │ │ │ └── view-link-menu.test.ts │ │ │ ├── parse-html.test.ts │ │ │ ├── plugin.test.ts │ │ │ └── render-elem.test.ts │ │ ├── paragraph │ │ │ ├── elem-to-html.test.ts │ │ │ ├── parse-html.test.ts │ │ │ ├── plugin.test.ts │ │ │ └── render-elem.test.ts │ │ ├── text-style │ │ │ ├── menu │ │ │ │ ├── clear-style-menu.test.ts │ │ │ │ └── menus.test.ts │ │ │ ├── parse-html.test.ts │ │ │ ├── parse-style-html.test.ts │ │ │ ├── text-style.test.tsx │ │ │ └── text-to-html.test.ts │ │ ├── todo │ │ │ ├── elem-to-html.test.ts │ │ │ ├── menu │ │ │ │ └── todo-menu.test.ts │ │ │ ├── parse-html.test.ts │ │ │ ├── plugin.test.ts │ │ │ ├── pre-parse-html.test.ts │ │ │ └── render-elem.test.ts │ │ └── undo-redo │ │ │ ├── redo-menu.test.ts │ │ │ └── undo-menu.test.ts │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── assets │ │ │ ├── blockquote.less │ │ │ ├── code-block.less │ │ │ ├── color.less │ │ │ ├── divider.less │ │ │ ├── emotion.less │ │ │ ├── image.less │ │ │ ├── index.less │ │ │ └── simple-style.less │ │ ├── constants │ │ │ └── icon-svg.ts │ │ ├── index.ts │ │ ├── locale │ │ │ ├── en.ts │ │ │ ├── index.ts │ │ │ └── zh-CN.ts │ │ ├── modules │ │ │ ├── blockquote │ │ │ │ ├── custom-types.ts │ │ │ │ ├── elem-to-html.ts │ │ │ │ ├── index.ts │ │ │ │ ├── menu │ │ │ │ │ ├── BlockquoteMenu.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── parse-elem-html.ts │ │ │ │ ├── plugin.ts │ │ │ │ └── render-elem.tsx │ │ │ ├── code-block │ │ │ │ ├── custom-types.ts │ │ │ │ ├── elem-to-html.ts │ │ │ │ ├── index.ts │ │ │ │ ├── menu │ │ │ │ │ ├── CodeBlockMenu.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── parse-elem-html.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── pre-parse-html.ts │ │ │ │ └── render-elem.tsx │ │ │ ├── color │ │ │ │ ├── custom-types.ts │ │ │ │ ├── index.ts │ │ │ │ ├── menu │ │ │ │ │ ├── BaseMenu.ts │ │ │ │ │ ├── BgColorMenu.ts │ │ │ │ │ ├── ColorMenu.ts │ │ │ │ │ ├── config.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── parse-style-html.ts │ │ │ │ ├── pre-parse-html.ts │ │ │ │ ├── render-style.tsx │ │ │ │ └── style-to-html.ts │ │ │ ├── common │ │ │ │ ├── index.ts │ │ │ │ └── menu │ │ │ │ │ ├── EnterMenu.ts │ │ │ │ │ └── index.ts │ │ │ ├── divider │ │ │ │ ├── README.md │ │ │ │ ├── custom-types.ts │ │ │ │ ├── elem-to-html.ts │ │ │ │ ├── index.ts │ │ │ │ ├── menu │ │ │ │ │ ├── DeleteDividerMenu.ts.bak │ │ │ │ │ ├── InsertDividerMenu.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── parse-elem-html.ts │ │ │ │ ├── plugin.ts │ │ │ │ └── render-elem.tsx │ │ │ ├── emotion │ │ │ │ ├── index.ts │ │ │ │ └── menu │ │ │ │ │ ├── EmotionMenu.ts │ │ │ │ │ ├── config.ts │ │ │ │ │ └── index.ts │ │ │ ├── font-size-family │ │ │ │ ├── custom-types.ts │ │ │ │ ├── index.ts │ │ │ │ ├── menu │ │ │ │ │ ├── BaseMenu.ts │ │ │ │ │ ├── FontFamilyMenu.ts │ │ │ │ │ ├── FontSizeMenu.ts │ │ │ │ │ ├── config.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── parse-style-html.ts │ │ │ │ ├── pre-parse-html.ts │ │ │ │ ├── render-style.tsx │ │ │ │ └── style-to-html.ts │ │ │ ├── full-screen │ │ │ │ ├── index.ts │ │ │ │ └── menu │ │ │ │ │ ├── FullScreen.ts │ │ │ │ │ └── index.ts │ │ │ ├── header │ │ │ │ ├── custom-types.ts │ │ │ │ ├── elem-to-html.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── menu │ │ │ │ │ ├── Header1ButtonMenu.ts │ │ │ │ │ ├── Header2ButtonMenu.ts │ │ │ │ │ ├── Header3ButtonMenu.ts │ │ │ │ │ ├── Header4ButtonMenu.ts │ │ │ │ │ ├── Header5ButtonMenu.ts │ │ │ │ │ ├── HeaderButtonMenuBase.ts │ │ │ │ │ ├── HeaderSelectMenu.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── parse-elem-html.ts │ │ │ │ ├── plugin.ts │ │ │ │ └── render-elem.tsx │ │ │ ├── image │ │ │ │ ├── custom-types.ts │ │ │ │ ├── elem-to-html.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── menu │ │ │ │ │ ├── DeleteImage.ts │ │ │ │ │ ├── EditImage.ts │ │ │ │ │ ├── InsertImage.ts │ │ │ │ │ ├── ViewImageLink.ts │ │ │ │ │ ├── Width100.ts │ │ │ │ │ ├── Width30.ts │ │ │ │ │ ├── Width50.ts │ │ │ │ │ ├── WidthBase.ts │ │ │ │ │ ├── config.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── parse-elem-html.ts │ │ │ │ ├── plugin.ts │ │ │ │ └── render-elem.tsx │ │ │ ├── indent │ │ │ │ ├── custom-types.ts │ │ │ │ ├── index.ts │ │ │ │ ├── menu │ │ │ │ │ ├── BaseMenu.ts │ │ │ │ │ ├── DecreaseIndentMenu.ts │ │ │ │ │ ├── IncreaseIndentMenu.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── parse-style-html.ts │ │ │ │ ├── pre-parse-html.ts │ │ │ │ ├── render-style.tsx │ │ │ │ └── style-to-html.ts │ │ │ ├── justify │ │ │ │ ├── custom-types.ts │ │ │ │ ├── index.ts │ │ │ │ ├── menu │ │ │ │ │ ├── BaseMenu.ts │ │ │ │ │ ├── JustifyCenterMenu.ts │ │ │ │ │ ├── JustifyJustifyMenu.ts │ │ │ │ │ ├── JustifyLeftMenu.ts │ │ │ │ │ ├── JustifyRightMenu.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── parse-style-html.ts │ │ │ │ ├── render-style.tsx │ │ │ │ └── style-to-html.ts │ │ │ ├── line-height │ │ │ │ ├── custom-types.ts │ │ │ │ ├── index.ts │ │ │ │ ├── menu │ │ │ │ │ ├── LineHeightMenu.ts │ │ │ │ │ ├── config.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── parse-style-html.ts │ │ │ │ ├── render-style.tsx │ │ │ │ └── style-to-html.ts │ │ │ ├── link │ │ │ │ ├── custom-types.ts │ │ │ │ ├── elem-to-html.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── menu │ │ │ │ │ ├── EditLink.ts │ │ │ │ │ ├── InsertLink.ts │ │ │ │ │ ├── UnLink.ts │ │ │ │ │ ├── ViewLink.ts │ │ │ │ │ ├── config.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── parse-elem-html.ts │ │ │ │ ├── plugin.ts │ │ │ │ └── render-elem.tsx │ │ │ ├── paragraph │ │ │ │ ├── custom-types.ts │ │ │ │ ├── elem-to-html.ts │ │ │ │ ├── index.ts │ │ │ │ ├── parse-elem-html.ts │ │ │ │ ├── plugin.ts │ │ │ │ └── render-elem.tsx │ │ │ ├── text-style │ │ │ │ ├── custom-types.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── menu │ │ │ │ │ ├── BaseMenu.ts │ │ │ │ │ ├── BoldMenu.ts │ │ │ │ │ ├── ClearStyleMenu.ts │ │ │ │ │ ├── CodeMenu.ts │ │ │ │ │ ├── ItalicMenu.ts │ │ │ │ │ ├── SubMenu.ts │ │ │ │ │ ├── SupMenu.ts │ │ │ │ │ ├── ThroughMenu.ts │ │ │ │ │ ├── UnderlineMenu.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── parse-style-html.ts │ │ │ │ ├── render-style.tsx │ │ │ │ └── style-to-html.ts │ │ │ ├── todo │ │ │ │ ├── custom-types.ts │ │ │ │ ├── elem-to-html.ts │ │ │ │ ├── index.ts │ │ │ │ ├── menu │ │ │ │ │ ├── Todo.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── parse-elem-html.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── pre-parse-html.ts │ │ │ │ └── render-elem.tsx │ │ │ └── undo-redo │ │ │ │ ├── index.ts │ │ │ │ └── menu │ │ │ │ ├── RedoMenu.ts │ │ │ │ ├── UndoMenu.ts │ │ │ │ └── index.ts │ │ └── utils │ │ │ ├── dom.ts │ │ │ ├── util.ts │ │ │ └── vdom.ts │ └── tsconfig.json ├── code-highlight │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ ├── content.ts │ │ ├── decorate.test.ts │ │ ├── elem-to-html.test.ts │ │ ├── parse-html.test.ts │ │ ├── render-text-style.test.tsx │ │ └── select-lang-menu.test.ts │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── assets │ │ │ └── index.less │ │ ├── constants │ │ │ └── svg.ts │ │ ├── custom-types.ts │ │ ├── decorate │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── locale │ │ │ ├── en.ts │ │ │ ├── index.ts │ │ │ └── zh-CN.ts │ │ ├── module │ │ │ ├── elem-to-html.ts │ │ │ ├── index.ts │ │ │ ├── menu │ │ │ │ ├── SelectLangMenu.ts │ │ │ │ ├── config.ts │ │ │ │ └── index.ts │ │ │ ├── parse-style-html.ts │ │ │ └── render-style.tsx │ │ ├── utils │ │ │ ├── dom.ts │ │ │ └── vdom.ts │ │ └── vendor │ │ │ └── prism.ts │ └── tsconfig.json ├── core │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ ├── config │ │ │ ├── editor-config.test.ts │ │ │ ├── menu-config.test.ts │ │ │ └── toolbar-config.test.ts │ │ ├── create-core-editor.ts │ │ ├── create │ │ │ └── content-to-html.test.ts │ │ ├── editor │ │ │ ├── dom-editor.test.ts │ │ │ └── plugins │ │ │ │ ├── with-config.test.ts │ │ │ │ ├── with-content.test.ts │ │ │ │ ├── with-dom.test.ts │ │ │ │ ├── with-emitter.test.ts │ │ │ │ └── with-selection.test.ts │ │ ├── i18n │ │ │ └── index.test.ts │ │ ├── menus │ │ │ ├── README.md │ │ │ └── register-menus │ │ │ │ ├── index.ts │ │ │ │ ├── register-button-menu.ts │ │ │ │ ├── register-modal-menu.ts │ │ │ │ └── register-select-menu.ts │ │ ├── parse-html │ │ │ └── README.md │ │ ├── render │ │ │ └── README.md │ │ ├── to-html │ │ │ └── README.md │ │ ├── upload │ │ │ └── uploader.test.ts │ │ └── utils │ │ │ ├── util.test.ts │ │ │ └── vdom.test.ts │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── assets │ │ │ ├── bar-item.less │ │ │ ├── bar.less │ │ │ ├── common.less │ │ │ ├── drop-panel.less │ │ │ ├── full-screen.less │ │ │ ├── index.less │ │ │ ├── modal.less │ │ │ ├── progress.less │ │ │ ├── select-list.less │ │ │ └── textarea.less │ │ ├── config │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── register.ts │ │ ├── constants │ │ │ ├── index.ts │ │ │ └── svg.ts │ │ ├── create │ │ │ ├── bind-node-relation.ts │ │ │ ├── create-editor.ts │ │ │ ├── create-toolbar.ts │ │ │ ├── helper.ts │ │ │ └── index.ts │ │ ├── editor │ │ │ ├── dom-editor.ts │ │ │ ├── interface.ts │ │ │ └── plugins │ │ │ │ ├── with-config.ts │ │ │ │ ├── with-content.ts │ │ │ │ ├── with-dom.ts │ │ │ │ ├── with-emitter.ts │ │ │ │ ├── with-event-data.ts │ │ │ │ ├── with-max-length.ts │ │ │ │ └── with-selection.ts │ │ ├── i18n │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── menus │ │ │ ├── README.md │ │ │ ├── bar-item │ │ │ │ ├── BaseButton.ts │ │ │ │ ├── DropPanelButton.ts │ │ │ │ ├── GroupButton.ts │ │ │ │ ├── ModalButton.ts │ │ │ │ ├── Select.ts │ │ │ │ ├── SimpleButton.ts │ │ │ │ ├── index.ts │ │ │ │ └── tooltip.ts │ │ │ ├── bar │ │ │ │ ├── HoverBar.ts │ │ │ │ └── Toolbar.ts │ │ │ ├── helpers │ │ │ │ ├── helpers.ts │ │ │ │ └── position.ts │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ ├── panel-and-modal │ │ │ │ ├── BaseClass.ts │ │ │ │ ├── DropPanel.ts │ │ │ │ ├── Modal.ts │ │ │ │ └── SelectList.ts │ │ │ └── register.ts │ │ ├── parse-html │ │ │ ├── README.md │ │ │ ├── helper.ts │ │ │ ├── index.ts │ │ │ ├── parse-common-elem-html.ts │ │ │ ├── parse-elem-html.ts │ │ │ └── parse-text-elem-html.ts │ │ ├── render │ │ │ ├── README.md │ │ │ ├── element │ │ │ │ ├── getRenderElem.tsx │ │ │ │ ├── renderElement.tsx │ │ │ │ └── renderStyle.ts │ │ │ ├── helper.ts │ │ │ ├── index.ts │ │ │ ├── node2Vnode.ts │ │ │ └── text │ │ │ │ ├── genVnode.tsx │ │ │ │ ├── renderStyle.ts │ │ │ │ └── renderText.tsx │ │ ├── text-area │ │ │ ├── TextArea.ts │ │ │ ├── event-handlers │ │ │ │ ├── beforeInput.ts │ │ │ │ ├── blur.ts │ │ │ │ ├── click.ts │ │ │ │ ├── composition.ts │ │ │ │ ├── copy.ts │ │ │ │ ├── cut.ts │ │ │ │ ├── drag.ts │ │ │ │ ├── drop.ts │ │ │ │ ├── focus.ts │ │ │ │ ├── index.ts │ │ │ │ ├── keydown.ts │ │ │ │ ├── keypress.ts │ │ │ │ └── paste.ts │ │ │ ├── helpers.ts │ │ │ ├── place-holder.ts │ │ │ ├── syncSelection.ts │ │ │ └── update-view.ts │ │ ├── to-html │ │ │ ├── README.md │ │ │ ├── elem2html.ts │ │ │ ├── index.ts │ │ │ ├── node2html.ts │ │ │ └── text2html.ts │ │ ├── upload │ │ │ ├── createUploader.ts │ │ │ ├── index.ts │ │ │ └── interface.ts │ │ └── utils │ │ │ ├── dom.ts │ │ │ ├── hotkeys.ts │ │ │ ├── key.ts │ │ │ ├── line.ts │ │ │ ├── ua.ts │ │ │ ├── util.ts │ │ │ ├── vdom.ts │ │ │ └── weak-maps.ts │ └── tsconfig.json ├── custom-types.d.ts ├── editor │ ├── CHANGELOG.md │ ├── README-en.md │ ├── README.md │ ├── __tests__ │ │ └── create.test.ts │ ├── demo │ │ ├── README.md │ │ ├── catalog.html │ │ ├── code-highlight.html │ │ ├── css │ │ │ ├── layout.css │ │ │ └── view.css │ │ ├── extend-menu-drop-panel.html │ │ ├── extend-menu-modal.html │ │ ├── extend-menu-select.html │ │ ├── extend-menu.html │ │ ├── get-html.html │ │ ├── huge-doc.html │ │ ├── index.html │ │ ├── js │ │ │ ├── custom-elem.js │ │ │ └── huge-content.js │ │ ├── like-qq-doc.html │ │ ├── max-length.html │ │ ├── multi-editor.html │ │ ├── set-html.html │ │ └── simple-mode.html │ ├── examples │ │ ├── README.md │ │ ├── batch-destroy.html │ │ ├── check.html │ │ ├── code-highlight.html │ │ ├── content-to-html.html │ │ ├── css │ │ │ ├── editor.css │ │ │ └── view.css │ │ ├── default-mode.html │ │ ├── dom7-demo.html │ │ ├── headers.html │ │ ├── huge-doc.html │ │ ├── i18n.html │ │ ├── index.html │ │ ├── js │ │ │ ├── huge-content.js │ │ │ └── init-content.js │ │ ├── like-yuque.html │ │ ├── maxlength.html │ │ ├── menu.html │ │ ├── modal-appendTo-body.html │ │ ├── multi-editors.html │ │ ├── new-menu.html │ │ ├── parse-html.html │ │ ├── shadow-dom.html │ │ ├── simple-mode.html │ │ ├── theme.html │ │ ├── todo.html │ │ ├── upload-image.html │ │ └── upload-video.html │ ├── favicon.ico │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── Boot.ts │ │ ├── assets │ │ │ └── index.less │ │ ├── constants │ │ │ └── svg.ts │ │ ├── create.ts │ │ ├── index.ts │ │ ├── init-default-config │ │ │ ├── config │ │ │ │ ├── hoverbar.ts │ │ │ │ ├── index.ts │ │ │ │ └── toolbar.ts │ │ │ └── index.ts │ │ ├── locale │ │ │ ├── en.ts │ │ │ ├── index.ts │ │ │ └── zh-CN.ts │ │ ├── register-builtin-modules │ │ │ ├── index.ts │ │ │ └── register.ts │ │ └── utils │ │ │ ├── browser-polyfill.ts │ │ │ ├── dom.ts │ │ │ └── node-polyfill.ts │ └── tsconfig.json ├── list-module │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ ├── elem-to-html.test.ts │ │ ├── menu │ │ │ ├── bulleted-list-menu.test.ts │ │ │ └── numbered-list-menu.test.ts │ │ ├── parse-html.test.ts │ │ ├── plugin.test.ts │ │ └── render-elem.test.ts │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── assets │ │ │ └── index.less │ │ ├── constants │ │ │ └── svg.ts │ │ ├── index.ts │ │ ├── locale │ │ │ ├── en.ts │ │ │ ├── index.ts │ │ │ └── zh-CN.ts │ │ ├── module │ │ │ ├── custom-types.ts │ │ │ ├── elem-to-html.ts │ │ │ ├── index.ts │ │ │ ├── menu │ │ │ │ ├── BaseMenu.ts │ │ │ │ ├── BulletedListMenu.ts │ │ │ │ ├── NumberedListMenu.ts │ │ │ │ └── index.ts │ │ │ ├── parse-elem-html.ts │ │ │ ├── plugin.ts │ │ │ └── render-elem.tsx │ │ └── utils │ │ │ ├── dom.ts │ │ │ └── maps.ts │ └── tsconfig.json ├── table-module │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ ├── elem-to-html.test.ts │ │ ├── menu │ │ │ ├── delete-col.test.ts │ │ │ ├── delete-row.test.ts │ │ │ ├── delete-table.test.ts │ │ │ ├── full-width.test.ts │ │ │ ├── insert-col.test.ts │ │ │ ├── insert-row.test.ts │ │ │ ├── insert-table.test.ts │ │ │ └── table-header.test.ts │ │ ├── parse-html.test.ts │ │ ├── plugin.test.ts │ │ └── render-elem.test.ts │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── assets │ │ │ └── index.less │ │ ├── constants │ │ │ └── svg.ts │ │ ├── index.ts │ │ ├── locale │ │ │ ├── en.ts │ │ │ ├── index.ts │ │ │ └── zh-CN.ts │ │ ├── module │ │ │ ├── custom-types.ts │ │ │ ├── elem-to-html.ts │ │ │ ├── helpers.ts │ │ │ ├── index.ts │ │ │ ├── menu │ │ │ │ ├── DeleteCol.ts │ │ │ │ ├── DeleteRow.ts │ │ │ │ ├── DeleteTable.ts │ │ │ │ ├── FullWidth.ts │ │ │ │ ├── InsertCol.ts │ │ │ │ ├── InsertRow.ts │ │ │ │ ├── InsertTable.ts │ │ │ │ ├── TableHeader.ts │ │ │ │ └── index.ts │ │ │ ├── parse-elem-html.ts │ │ │ ├── plugin.ts │ │ │ ├── pre-parse-html.ts │ │ │ └── render-elem │ │ │ │ ├── index.ts │ │ │ │ ├── render-cell.tsx │ │ │ │ ├── render-row.tsx │ │ │ │ └── render-table.tsx │ │ └── utils │ │ │ ├── dom.ts │ │ │ └── util.ts │ └── tsconfig.json ├── upload-image-module │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ ├── config.test.ts │ │ ├── plugin.test.ts │ │ ├── upload-files.test.ts │ │ └── upload-image-menu.test.ts │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── assets │ │ │ └── index.less │ │ ├── constants │ │ │ └── svg.ts │ │ ├── index.ts │ │ ├── locale │ │ │ ├── en.ts │ │ │ ├── index.ts │ │ │ └── zh-CN.ts │ │ ├── module │ │ │ ├── index.ts │ │ │ ├── menu │ │ │ │ ├── UploadImageMenu.ts │ │ │ │ ├── config.ts │ │ │ │ └── index.ts │ │ │ ├── plugin.ts │ │ │ └── upload-images.ts │ │ └── utils │ │ │ └── dom.ts │ └── tsconfig.json ├── vars.less └── video-module │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ ├── elem-to-html.test.ts │ ├── helpler.test.ts │ ├── menu │ │ ├── delete-video-menu.test.ts.bak │ │ ├── insert-video-menu.test.ts │ │ └── upload-video-menu.test.ts │ ├── parse-html.test.ts │ ├── plugin.test.ts │ ├── render-elem.test.ts │ └── util.test.ts │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── assets │ │ └── index.less │ ├── constants │ │ └── svg.ts │ ├── index.ts │ ├── locale │ │ ├── en.ts │ │ ├── index.ts │ │ └── zh-CN.ts │ ├── module │ │ ├── custom-types.ts │ │ ├── elem-to-html.ts │ │ ├── helper │ │ │ ├── insert-video.ts │ │ │ └── upload-videos.ts │ │ ├── index.ts │ │ ├── menu │ │ │ ├── DeleteVideoMenu.ts.bak │ │ │ ├── EditVideoSizeMenu.ts │ │ │ ├── InsertVideoMenu.ts │ │ │ ├── UploadVideoMenu.ts │ │ │ ├── config.ts │ │ │ └── index.ts │ │ ├── parse-elem-html.ts │ │ ├── plugin.ts │ │ ├── pre-parse-html.ts │ │ └── render-elem.tsx │ └── utils │ │ ├── dom.ts │ │ └── util.ts │ └── tsconfig.json ├── scripts └── release-tag.js ├── tests ├── setup │ └── index.ts └── utils │ ├── create-editor.ts │ ├── create-toolbar.ts │ └── stylesMock.js ├── tsconfig.json └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | not IE 11 3 | maintained node versions -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/ 3 | lib/ 4 | *.html -------------------------------------------------------------------------------- /.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 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 13 | '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. 14 | ], 15 | globals: { 16 | Atomics: 'readonly', 17 | SharedArrayBuffer: 'readonly', 18 | }, 19 | parser: '@typescript-eslint/parser', 20 | parserOptions: { 21 | ecmaVersion: 2018, 22 | sourceType: 'module', 23 | }, 24 | plugins: ['@typescript-eslint', 'prettier'], 25 | rules: { 26 | 'no-unused-vars': 0, 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | open_collective: wangeditor 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 提交 bug 3 | about: 请大家一定要按照该模板填写,以方便我们更快复现,否则该 issue 将不予受理! 4 | --- 5 | 6 | ## bug 描述 7 | 8 | *请输入内容……* 9 | 10 | ## 你预期的样子是? 11 | 12 | *请输入内容……* 13 | 14 | ## 系统和浏览器及版本号 15 | 16 | - 操作系统 17 | - 浏览器和版本 18 | 19 | ## wangEditor 版本 20 | 21 | *请输入内容……* 22 | 23 | ## demo 能否复现该 bug ? 24 | 25 | 能/不能 26 | 27 | - 中文 demo https://www.wangeditor.com/demo/ 28 | - English demo https://www.wangeditor.com/demo/?lang=en 29 | 30 | ## 在线 demo 31 | 32 | *请尽量提供在线 demo (推荐以下网站),帮助我们最低成本复现 bug* 33 | 34 | - https://codesandbox.io/ 35 | - https://codepen.io/ 36 | - https://stackblitz.com/ 37 | 38 | ## 最小成本的复现步骤 39 | 40 | (请告诉我们,如何最快的复现该 bug) 41 | 42 | - 步骤一 43 | - 步骤二 44 | - 步骤三 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 建议增加新功能 3 | about: 请按照该模板填写,以便我们能真正了解你的需求,否则该 issue 将不予受理! 4 | --- 5 | 6 | ## 功能描述 7 | 8 | *请输入内容……* 9 | 10 | ## 提炼几个功能点 11 | 12 | - 功能1 13 | - 功能2 14 | - 功能3 15 | 16 | ## 原型图 17 | 18 | *涉及到 UI 改动的功能,请一定提供原型图。原型图能表明功能即可,不要求规范和美观* 19 | 20 | ## 可参考的案例 21 | 22 | *是否已有可参考的案例(如其他编辑器),有的话请给出链接* 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 使用时遇到了问题(非 bug) 3 | about: 请按照该模板填写,以便我们能真正了解你的问题,否则该 issue 将不予受理! 4 | --- 5 | 6 | ## 问题描述 7 | 8 | *请输入遇到的问题...* 9 | 10 | ## wangEditor 版本 11 | 12 | *请输入内容……* 13 | 14 | ## 是否查阅了文档 ? 15 | 16 | (文档链接 [www.wangeditor.com](https://www.wangeditor.com/) ) 17 | 18 | *是/否* 19 | 20 | ## 最小成本的复现步骤 21 | 22 | (请告诉我们,如何**最快的**复现该问题?) 23 | 24 | - 步骤一 25 | - 步骤二 26 | - 步骤三 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .vscode 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npm.taobao.org" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog Link 2 | - [basic-modules](./packages/basic-modules/CHANGELOG.md) 3 | - [code-highlight](./packages/code-highlight/CHANGELOG.md) 4 | - [core](./packages/core/CHANGELOG.md) 5 | - [editor](./packages/editor/CHANGELOG.md) 6 | - [list-module](./packages/list-module/CHANGELOG.md) 7 | - [table-module](./packages/table-module/CHANGELOG.md) 8 | - [upload-image-module](./packages/upload-image-module/CHANGELOG.md) 9 | - [video-module](./packages/video-module/CHANGELOG.md) -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | # wangEditor 5 2 | 3 | [中文](./README.md) 4 | 5 | ## Introduction 6 | 7 | Open source web rich text editor, run right out of the box. Support JS Vue React. 8 | 9 | - [Document](https://www.wangeditor.com/en/) 10 | - [Demo](https://www.wangeditor.com/demo/?lang=en) 11 | 12 | ![](./docs/images/editor-en.png) 13 | 14 | ## Communication 15 | 16 | You can [commit an issue]((https://github.com/wangeditor-team/wangEditor/issues)) if you have any question. 17 | 18 | ## Donation 19 | 20 | Support wangEditor open-source work https://opencollective.com/wangeditor 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wangEditor 5 2 | 3 | [English](./README-en.md) 4 | 5 | ## 介绍 6 | 7 | 开源 Web 富文本编辑器,开箱即用,配置简单。支持 JS Vue React 。 8 | 9 | - [文档](https://www.wangeditor.com/) 10 | - [demo](https://www.wangeditor.com/demo/) 11 | 12 | ![](./docs/images/editor.png) 13 | 14 | ## 交流 15 | 16 | - [讨论问题和建议](https://github.com/wangeditor-team/wangEditor/issues) 17 | 18 | ## 捐赠 19 | 20 | 支持 wangEditor 开源工作 https://opencollective.com/wangeditor 21 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "useBuiltIns": "usage", 8 | "corejs": 3, 9 | "targets": "ie 11" 10 | } 11 | ], 12 | "@babel/preset-typescript" 13 | ], 14 | "plugins": [ 15 | [ 16 | "@babel/plugin-transform-runtime", 17 | { 18 | "absoluteRuntime": false, 19 | "corejs": 3, 20 | "helpers": true, 21 | "regenerator": true, 22 | "useESModules": false 23 | } 24 | ] 25 | ] 26 | } -------------------------------------------------------------------------------- /build/build-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## 一键打包所有 package 4 | 5 | # 获取 yarn dev/build 类型 6 | buildType=build 7 | if [ -n "$1" ]; then 8 | buildType=$1 9 | fi 10 | 11 | cd ./packages 12 | 13 | # core 要第一个打包 14 | cd ./core 15 | rm -rf dist # 清空 dist 目录 16 | yarn "$buildType" 17 | 18 | cd ../basic-modules 19 | rm -rf dist # 清空 dist 目录 20 | yarn "$buildType" 21 | 22 | # code-highlight 依赖于 basic-modules 中的 code-block 23 | cd ../code-highlight 24 | rm -rf dist # 清空 dist 目录 25 | yarn "$buildType" 26 | 27 | cd ../list-module 28 | rm -rf dist # 清空 dist 目录 29 | yarn "$buildType" 30 | 31 | cd ../table-module 32 | rm -rf dist # 清空 dist 目录 33 | yarn "$buildType" 34 | 35 | # upload-image 依赖于 basic-modules 中的 image 36 | cd ../upload-image-module 37 | rm -rf dist # 清空 dist 目录 38 | yarn "$buildType" 39 | 40 | cd ../video-module 41 | rm -rf dist # 清空 dist 目录 42 | yarn "$buildType" 43 | 44 | # editor 依赖于上述的 core + modules 45 | cd ../editor 46 | rm -rf dist # 清空 dist 目录 47 | yarn "$buildType" 48 | -------------------------------------------------------------------------------- /build/config/dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description rollup dev config 3 | * @author wangfupeng 4 | */ 5 | 6 | import postcss from 'rollup-plugin-postcss' 7 | import autoprefixer from 'autoprefixer' 8 | import genCommonConf from './common' 9 | 10 | /** 11 | * 生成 dev config 12 | * @param {string} format 'umd' 'esm' 13 | */ 14 | function genDevConf(format) { 15 | const { input, output = {}, plugins = [], external } = genCommonConf(format) 16 | 17 | return { 18 | input, 19 | output, 20 | external, 21 | plugins: [ 22 | ...plugins, 23 | 24 | postcss({ 25 | plugins: [autoprefixer()], 26 | extract: 'css/style.css', 27 | }), 28 | ], 29 | } 30 | } 31 | 32 | export default genDevConf 33 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['cz'], 3 | rules: { 4 | 'type-empty': [2, 'never'], 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8881", 3 | "defaultCommandTimeout": 8000, 4 | "video": false 5 | } -------------------------------------------------------------------------------- /cypress/cypress.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface CustomWindow extends Window {} 5 | 6 | interface Chainable { 7 | /** 8 | * Window object with additional properties used during test. 9 | */ 10 | window(options?: Partial): Chainable 11 | 12 | getByClass(dataTestAttribute: string, args?: any): Chainable 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/integration/editor.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Basic Editor', () => { 2 | it('create editor', () => { 3 | cy.visit('/examples/default-mode.html') 4 | 5 | cy.get('#btn-create').click() 6 | 7 | cy.get('#editor-toolbar').should('have.attr', 'data-w-e-toolbar', 'true') 8 | cy.get('#editor-text-area').should('have.attr', 'data-w-e-textarea', 'true') 9 | cy.get('#w-e-textarea-1').contains('一行标题') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /cypress/plugins/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | export default (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | // codeCoverageTask(on, config) 22 | return config 23 | } 24 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | Cypress.Commands.add('getByClass', (selector, ...args) => { 2 | return cy.get(`.w-e-${selector}`, ...args) 3 | }) 4 | -------------------------------------------------------------------------------- /cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | import './commands' 2 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es2015", "dom", "esnext"], 5 | "types": ["cypress"], 6 | "isolatedModules": false, 7 | "allowJs": true, 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | }, 18 | "include": [ 19 | "./**/*.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # 文档 2 | 3 | - [开发文档](./dev.md) 4 | - [发布到 npm](./publish.md) 5 | - [加入研发团队](./join.md) 6 | -------------------------------------------------------------------------------- /docs/dev.md: -------------------------------------------------------------------------------- 1 | # 开发 2 | 3 | ## 准备工作 4 | 5 | - 了解 slate.js 6 | - 了解 vdom 和 snabbdom.js 7 | - 了解 lerna 8 | - 已安装 yarn 9 | 10 | ## 本地启动 11 | 12 | ### 打包 13 | 14 | - 下载代码到本地,进入 `wangEditor` 目录 15 | - 安装所有依赖 `yarn bootstrap` 16 | - 打包所有模块 `yarn dev` 或者 `yarn build` 17 | 18 | ### 运行 demo 19 | 20 | - 进入 `packages/editor` 目录,运行 `yarn example` ,浏览器打开 `http://localhost:8881/examples/` 21 | 22 | ## 注意事项 23 | 24 | - 修改代码、重新打包后,要**强制刷新**浏览器 25 | - 如果本地包依赖有问题,试试 `lerna link` 关联内部包 26 | 27 | ## 记录 28 | 29 | 全局安装一个插件 `yarn add xxx --dev -W` 30 | 31 | 注意合理使用 `peerDependencies` 和 `dependencies` ,不要重复打包一个第三方库 32 | 33 | 执行 `lerna add ...` 之后,需要重新 `lerna link` 建立内部连接 34 | 35 | 分析包体积 36 | - 命令行,进入某个 package ,如 `cd packages/editor` 37 | - 执行 `yarn size-stats` ,等待执行完成 38 | - 结果会记录在 `packages/editor/stats.html` 用浏览器打开 -------------------------------------------------------------------------------- /docs/images/cypress-run.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangeditor-team/wangEditor/f35d8a73a0a3dc159134d483afc5963d0256ea18/docs/images/cypress-run.jpg -------------------------------------------------------------------------------- /docs/images/cypress.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangeditor-team/wangEditor/f35d8a73a0a3dc159134d483afc5963d0256ea18/docs/images/cypress.jpg -------------------------------------------------------------------------------- /docs/images/editor-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangeditor-team/wangEditor/f35d8a73a0a3dc159134d483afc5963d0256ea18/docs/images/editor-en.png -------------------------------------------------------------------------------- /docs/images/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangeditor-team/wangEditor/f35d8a73a0a3dc159134d483afc5963d0256ea18/docs/images/editor.png -------------------------------------------------------------------------------- /docs/join.md: -------------------------------------------------------------------------------- 1 | # 加入团队 2 | 3 | 欢迎加入 wangEditor 研发团队~ 4 | 5 | ## V5 研发人员 6 | 7 | - [王福朋](https://github.com/wangfupeng1988/) - wangEditor 创始人,资深前端工程师,PMP,曾就职于百度、滴滴 8 | - [罗超](https://github.com/echoLC) - 天才就是百分之一的灵感加上百分之九十九的努力 9 | - [TGuoW](https://github.com/TGuoW) 10 | - [刘庆华(火热) ](https://github.com/liuqh0609) - 热爱着,年轻着 11 | - [haha](https://github.com/hahaaha) 12 | 13 | ## 加入条件 14 | 15 | - 熟悉 typescript ,并实际应用过 16 | - 熟悉 webpack 或者 rollup 17 | - 熟悉 React 或者 Vue 18 | - 熟悉 vdom 结构,熟悉 [snabbdom.js](https://github.com/snabbdom/snabbdom) (不了解的可以先去学习一下) 19 | - 熟悉 [slate.js](https://www.slatejs.org/) 包括:熟悉数据模型,熟悉 API,看过源码(不了解的可以先去学习一下) 20 | 21 | ## 申请加入 22 | 23 | - 首先自我评价,符合上述加入条件 24 | - 加入 QQ 群,私聊群主,发送一份个人简历 25 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/packages'], 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)$': '/tests/utils/stylesMock.js', 17 | }, 18 | transformIgnorePatterns: ['node_modules/(?!(html-void-elements)/)'], 19 | setupFilesAfterEnv: ['/tests/setup/index.ts'], 20 | collectCoverageFrom: ['/packages/**/src/**/*.(ts|tsx)'], 21 | coveragePathIgnorePatterns: [ 22 | 'dist', 23 | 'locale', 24 | 'index.ts', 25 | 'config.ts', 26 | 'browser-polyfill.ts', 27 | 'node-polyfill.ts', 28 | ], 29 | } 30 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "independent", 6 | "npmClient": "yarn", 7 | "useWorkspaces": true, 8 | "command": { 9 | "publish": { 10 | "ignoreChanges": ["ignored-file", "*.md"], 11 | "message": "chore(release): publish", 12 | "conventionalCommits": true, 13 | "registry": "https://npm.pkg.github.com" 14 | }, 15 | "version": { 16 | "message": "chore(release): publish", 17 | "allowBranch": "master" 18 | } 19 | }, 20 | "changelog": { 21 | "repo": "wangeditor-team/wangEditor", 22 | "labels": { 23 | "tag: new feature": ":rocket: New Feature", 24 | "tag: breaking change": ":boom: Breaking Change", 25 | "tag: bug fix": ":bug: Bug Fix", 26 | "tag: enhancement": ":nail_care: Enhancement", 27 | "tag: documentation": ":memo: Documentation", 28 | "tag: internal": ":house: Internal" 29 | }, 30 | "cacheDir": ".changelog" 31 | }, 32 | "changelogPreset": "angular" 33 | } 34 | -------------------------------------------------------------------------------- /packages/basic-modules/README.md: -------------------------------------------------------------------------------- 1 | # wangEditor basic-modules 2 | 3 | Basic modules built in [wangEditor](https://www.wangeditor.com/) by default. 4 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/blockquote/elem-to-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description blockquote - elem to html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { quoteToHtmlConf } from '../../src/modules/blockquote/elem-to-html' 7 | 8 | describe('blockquote elem to html', () => { 9 | it('blockquote to html', () => { 10 | expect(quoteToHtmlConf.type).toBe('blockquote') 11 | 12 | const elem = { type: 'blockquote', children: [] } 13 | const html = quoteToHtmlConf.elemToHtml(elem, 'hello') 14 | expect(html).toBe('
hello
') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/blockquote/render-elem.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description blockquote render elem test 3 | * @author wangfupeng 4 | */ 5 | 6 | import createEditor from '../../../../tests/utils/create-editor' 7 | import { renderBlockQuoteConf } from '../../src/modules/blockquote/render-elem' 8 | 9 | describe('blockquote - render elem', () => { 10 | const editor = createEditor() 11 | 12 | it('render blockquote elem', () => { 13 | expect(renderBlockQuoteConf.type).toBe('blockquote') 14 | 15 | const elem = { type: 'blockquote', children: [] } 16 | const vnode = renderBlockQuoteConf.renderElem(elem, null, editor) 17 | expect(vnode.sel).toBe('blockquote') 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/code-block/elem-to-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description code-block elem to html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { codeToHtmlConf, preToHtmlConf } from '../../src/modules/code-block/elem-to-html' 7 | 8 | describe('code-block - elem to html', () => { 9 | it('code to html', () => { 10 | expect(codeToHtmlConf.type).toBe('code') 11 | const elem = { type: 'code', children: [] } 12 | const html = codeToHtmlConf.elemToHtml(elem, 'hello') 13 | expect(html).toBe('hello') 14 | }) 15 | 16 | it('pre to html', () => { 17 | expect(preToHtmlConf.type).toBe('pre') 18 | const elem = { type: 'pre', children: [] } 19 | const html = preToHtmlConf.elemToHtml(elem, 'hello') 20 | expect(html).toBe('
hello
') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/code-block/render-elem.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description code-block render elem test 3 | * @author wangfupeng 4 | */ 5 | 6 | import createEditor from '../../../../tests/utils/create-editor' 7 | import { renderPreConf, renderCodeConf } from '../../src/modules/code-block/render-elem' 8 | 9 | describe('code-block render elem', () => { 10 | const editor = createEditor() 11 | 12 | it('render code elem', () => { 13 | expect(renderCodeConf.type).toBe('code') 14 | 15 | const elem = { type: 'code', children: [] } 16 | const vnode = renderCodeConf.renderElem(elem, null, editor) 17 | expect(vnode.sel).toBe('code') 18 | }) 19 | 20 | it('render pre elem', () => { 21 | expect(renderPreConf.type).toBe('pre') 22 | 23 | const elem = { type: 'pre', children: [] } 24 | const vnode = renderPreConf.renderElem(elem, null, editor) 25 | expect(vnode.sel).toBe('pre') 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/color/render-text-style.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description color - render text style test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { jsx } from 'snabbdom' 7 | import { renderStyle } from '../../src/modules/color/render-style' 8 | 9 | describe('color - render text style', () => { 10 | it('render color style', () => { 11 | const color = 'rgb(51, 51, 51)' 12 | const bgColor = 'rgb(204, 204, 204)' 13 | const textNode = { text: 'hello', color, bgColor } 14 | const vnode = hello 15 | 16 | // @ts-ignore 17 | const newVnode = renderStyle(textNode, vnode) as any 18 | expect(newVnode.sel).toBe('span') 19 | expect(newVnode.data.style.color).toBe(color) 20 | expect(newVnode.data.style.backgroundColor).toBe(bgColor) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/color/text-style-to-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description color - text style to html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { styleToHtml } from '../../src/modules/color/style-to-html' 7 | 8 | describe('color - text style to html', () => { 9 | it('color to html', () => { 10 | const color = 'rgb(51, 51, 51)' 11 | const bgColor = 'rgb(204, 204, 204)' 12 | const textNode = { text: '', color, bgColor } 13 | 14 | const html = styleToHtml(textNode, 'hello') 15 | expect(html).toBe(`hello`) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/divider/elem-to-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description divider - elem to html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { dividerToHtmlConf } from '../../src/modules/divider/elem-to-html' 7 | 8 | describe('divider - elem to html', () => { 9 | it('divider to html', () => { 10 | expect(dividerToHtmlConf.type).toBe('divider') 11 | 12 | const elem = { type: 'divider', children: [{ text: '' }] } 13 | const html = dividerToHtmlConf.elemToHtml(elem, '') 14 | expect(html).toBe('
') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/divider/parse-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { $ } from 'dom7' 7 | import createEditor from '../../../../tests/utils/create-editor' 8 | import { parseHtmlConf } from '../../src/modules/divider/parse-elem-html' 9 | 10 | describe('divider - parse html', () => { 11 | const editor = createEditor() 12 | 13 | it('parse html', () => { 14 | const $hr = $('
') 15 | 16 | // match selector 17 | expect($hr[0].matches(parseHtmlConf.selector)).toBeTruthy() 18 | 19 | // parse 20 | const res = parseHtmlConf.parseElemHtml($hr[0], [], editor) 21 | expect(res).toEqual({ 22 | type: 'divider', 23 | children: [{ text: '' }], // void node 有一个空白 text 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/font-size-family/render-text-style.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description font size and family - render text style test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { jsx } from 'snabbdom' 7 | import { renderStyle } from '../../src/modules/font-size-family/render-style' 8 | 9 | describe('font size and family - render text style', () => { 10 | it('render text style', () => { 11 | const fontSize = '20px' 12 | const fontFamily = '黑体' 13 | const textNode = { text: 'hello', fontSize, fontFamily } 14 | const vnode = hello 15 | 16 | // @ts-ignore 忽略 vnode 格式检查 17 | const newVnode = renderStyle(textNode, vnode) as any 18 | expect(newVnode.data.style.fontSize).toBe(fontSize) 19 | expect(newVnode.data.style.fontFamily).toBe(fontFamily) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/font-size-family/text-style-to-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description font size and family - text style to html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { styleToHtml } from '../../src/modules/font-size-family/style-to-html' 7 | 8 | describe('font size and family - text style to html', () => { 9 | it('text style to html', () => { 10 | const fontSize = '20px' 11 | const fontFamily = '黑体' 12 | const textNode = { text: '', fontSize, fontFamily } 13 | 14 | const html = styleToHtml(textNode, 'hello') 15 | expect(html).toBe( 16 | `hello` 17 | ) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/full-screen/full-screen-menu.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description full screen menu test 3 | * @author wangfupeng 4 | */ 5 | 6 | import createEditor from '../../../../tests/utils/create-editor' 7 | import FullScreen from '../../src/modules/full-screen/menu/FullScreen' 8 | 9 | describe('full screen menu', () => { 10 | const editor = createEditor() 11 | const menu = new FullScreen() 12 | 13 | it('full screen menu', done => { 14 | menu.exec(editor, '') // 设置全屏 15 | expect(menu.isActive(editor)).toBeTruthy() 16 | 17 | menu.exec(editor, '') // 取消全屏(有延迟) 18 | setTimeout(() => { 19 | expect(menu.isActive(editor)).toBeFalsy() 20 | done() 21 | }, 500) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/header/plugin.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description header plugin test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Editor, Transforms } from 'slate' 7 | import createEditor from '../../../../tests/utils/create-editor' 8 | import withHeader from '../../src/modules/header/plugin' 9 | 10 | describe('header plugin', () => { 11 | const editor = withHeader(createEditor()) 12 | const startLocation = Editor.start(editor, []) 13 | 14 | it('header break', () => { 15 | editor.select(startLocation) 16 | 17 | Transforms.setNodes(editor, { type: 'header1' }) 18 | editor.insertBreak() // 在 header 换行,会生成 p 19 | 20 | const paragraphs = editor.getElemsByTypePrefix('paragraph') 21 | expect(paragraphs.length).toBe(1) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/image/elem-to-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description image - elem to html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { imageToHtmlConf } from '../../src/modules/image/elem-to-html' 7 | 8 | describe('image to html', () => { 9 | it('to html', () => { 10 | expect(imageToHtmlConf.type).toBe('image') 11 | 12 | const src = 'https://www.wangeditor.com/imgs/logo.png' 13 | const href = 'https://www.wangeditor.com/' 14 | const elem = { 15 | type: 'image', 16 | src, 17 | alt: 'logo', 18 | href, 19 | style: { width: '100', height: '80' }, 20 | children: [{ text: '' }], // void node 必须包含一个空 text 21 | } 22 | const html = imageToHtmlConf.elemToHtml(elem, '') 23 | 24 | expect(html).toBe( 25 | `logo` 26 | ) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/image/parse-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { $ } from 'dom7' 7 | import createEditor from '../../../../tests/utils/create-editor' 8 | import { parseHtmlConf } from '../../src/modules/image/parse-elem-html' 9 | 10 | describe('image - parse html', () => { 11 | const editor = createEditor() 12 | 13 | it('parse html', () => { 14 | const $img = $( 15 | 'hello' 16 | ) 17 | 18 | // match selector 19 | expect($img[0].matches(parseHtmlConf.selector)).toBeTruthy() 20 | 21 | // parse 22 | const res = parseHtmlConf.parseElemHtml($img[0], [], editor) 23 | expect(res).toEqual({ 24 | type: 'image', 25 | src: 'hello.png', 26 | alt: 'hello', 27 | href: 'http://localhost/', 28 | style: { 29 | width: '10px', 30 | height: '5px', 31 | }, 32 | children: [{ text: '' }], 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/image/plugin.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description image plugin test 3 | * @author wangfupeng 4 | */ 5 | 6 | import createEditor from '../../../../tests/utils/create-editor' 7 | import withImage from '../../src/modules/image/plugin' 8 | 9 | describe('image plugin', () => { 10 | const editor = withImage(createEditor()) 11 | const elem = { type: 'image', children: [{ text: '' }] } 12 | 13 | it('image is inline', () => { 14 | expect(editor.isInline(elem)).toBeTruthy() 15 | }) 16 | 17 | it('image is void', () => { 18 | expect(editor.isVoid(elem)).toBeTruthy() 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/indent/render-text-style.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description indent - render text style 3 | * @author wangfupeng 4 | */ 5 | 6 | import { jsx } from 'snabbdom' 7 | import { renderStyle } from '../../src/modules/indent/render-style' 8 | 9 | describe('indent - render text style', () => { 10 | it('render text style', () => { 11 | const indent = '2em' 12 | const elem = { type: 'paragraph', indent, children: [] } 13 | const vnode =

hello

14 | 15 | // @ts-ignore 16 | const newVnode = renderStyle(elem, vnode) 17 | // @ts-ignore 18 | expect(newVnode.data.style.textIndent).toBe(indent) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/indent/text-style-to-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description indent - text style to html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { styleToHtml } from '../../src/modules/indent/style-to-html' 7 | 8 | describe('indent - text style to html', () => { 9 | it('text style to html', () => { 10 | const indent = '2em' 11 | const elem = { type: 'paragraph', indent, children: [] } 12 | const html = styleToHtml(elem, '

hello

') 13 | expect(html).toBe(`

hello

`) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/justify/parse-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { $ } from 'dom7' 7 | import createEditor from '../../../../tests/utils/create-editor' 8 | import { parseStyleHtml } from '../../src/modules/justify/parse-style-html' 9 | 10 | describe('text align - parse style', () => { 11 | const editor = createEditor() 12 | 13 | it('parse style', () => { 14 | const $p = $('

') 15 | const paragraph = { type: 'paragraph', children: [{ text: 'hello' }] } 16 | 17 | // parse 18 | const res = parseStyleHtml($p[0], paragraph, editor) 19 | expect(res).toEqual({ 20 | type: 'paragraph', 21 | textAlign: 'center', 22 | children: [{ text: 'hello' }], 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/justify/render-text-style.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description justify - render text style test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { jsx } from 'snabbdom' 7 | import { renderStyle } from '../../src/modules/justify/render-style' 8 | 9 | describe('justify - render text style', () => { 10 | it('render text style', () => { 11 | const elem = { type: 'paragraph', textAlign: 'center', children: [] } 12 | const vnode = hello 13 | // @ts-ignore 忽略 vnode 格式 14 | const newVnode = renderStyle(elem, vnode) 15 | // @ts-ignore 忽略 vnode 格式 16 | expect(newVnode.data.style?.textAlign).toBe('center') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/justify/text-style-to-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description justify - text style to html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { styleToHtml } from '../../src/modules/justify/style-to-html' 7 | 8 | describe('justify text-style-to-html', () => { 9 | it('text style to html', () => { 10 | const elem = { type: 'paragraph', textAlign: 'center', children: [] } 11 | const html = styleToHtml(elem, 'hello') 12 | expect(html).toBe('hello') 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/line-height/parse-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { $ } from 'dom7' 7 | import createEditor from '../../../../tests/utils/create-editor' 8 | import { parseStyleHtml } from '../../src/modules/line-height/parse-style-html' 9 | 10 | describe('line height - parse style', () => { 11 | const editor = createEditor() 12 | 13 | it('parse style', () => { 14 | const $p = $('

') 15 | const paragraph = { type: 'paragraph', children: [{ text: 'hello' }] } 16 | 17 | // parse 18 | const res = parseStyleHtml($p[0], paragraph, editor) 19 | expect(res).toEqual({ 20 | type: 'paragraph', 21 | lineHeight: '2.5', 22 | children: [{ text: 'hello' }], 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/line-height/render-text-style.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description line-height render text style test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { jsx } from 'snabbdom' 7 | import { renderStyle } from '../../src/modules/line-height/render-style' 8 | 9 | describe('line-height render-text-style', () => { 10 | it('render text style', () => { 11 | const elem = { type: 'paragraph', lineHeight: '1.5', children: [] } 12 | const vnode = hello 13 | // @ts-ignore 忽略 vnode 格式检查 14 | const newVnode = renderStyle(elem, vnode) 15 | // @ts-ignore 忽略 vnode 格式检查 16 | expect(newVnode.data.style.lineHeight).toBe('1.5') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/line-height/text-style-to-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description line-height text-style-to-html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { styleToHtml } from '../../src/modules/line-height/style-to-html' 7 | 8 | describe('line-height text-style-to-html', () => { 9 | it('text style to html', () => { 10 | const elem = { type: 'paragraph', lineHeight: '1.5', children: [] } 11 | const html = styleToHtml(elem, 'hello') 12 | expect(html).toBe('hello') 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/link/elem-to-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description link - elem to html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { linkToHtmlConf } from '../../src/modules/link/elem-to-html' 7 | 8 | describe('link elem to html', () => { 9 | it('link to html', () => { 10 | expect(linkToHtmlConf.type).toBe('link') 11 | 12 | const url = 'https://www.wangeditor.com/' 13 | const target = '_blank' 14 | const elem = { type: 'link', url, target, children: [] } 15 | 16 | const html = linkToHtmlConf.elemToHtml(elem, 'hello') 17 | expect(html).toBe(`hello`) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/link/render-elem.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description link - render elem test 3 | * @author wangfupeng 4 | */ 5 | 6 | import createEditor from '../../../../tests/utils/create-editor' 7 | import { renderLinkConf } from '../../src/modules/link/render-elem' 8 | 9 | describe('link render elem', () => { 10 | const editor = createEditor() 11 | 12 | it('render elem', () => { 13 | expect(renderLinkConf.type).toBe('link') 14 | 15 | const url = 'https://www.wangeditor.com/' 16 | const target = '_blank' 17 | const elem = { type: 'link', url, target, children: [] } 18 | 19 | const vnode = renderLinkConf.renderElem(elem, null, editor) as any 20 | expect(vnode.sel).toBe('a') 21 | expect(vnode.data.href).toBe(url) 22 | expect(vnode.data.target).toBe(target) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/paragraph/elem-to-html.test.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'dom7' 2 | /** 3 | * @description paragraph - elem to html test 4 | * @author wangfupeng 5 | */ 6 | 7 | import { pToHtmlConf } from '../../src/modules/paragraph/elem-to-html' 8 | 9 | describe('paragraph - elem to html', () => { 10 | it('paragraph to html', () => { 11 | expect(pToHtmlConf.type).toBe('paragraph') 12 | 13 | const elem = { type: 'paragraph', children: [] } 14 | const html = pToHtmlConf.elemToHtml(elem, 'hello') 15 | expect(html).toBe('

hello

') 16 | }) 17 | 18 | it('paragraph to html with empty children', () => { 19 | expect(pToHtmlConf.type).toBe('paragraph') 20 | 21 | const elem = { type: 'paragraph', children: [] } 22 | const html = pToHtmlConf.elemToHtml(elem, '') 23 | expect(html).toBe('


') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/paragraph/render-elem.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description paragraph render elem test 3 | * @author wangfupeng 4 | */ 5 | 6 | import createEditor from '../../../../tests/utils/create-editor' 7 | import { renderParagraphConf } from '../../src/modules/paragraph/render-elem' 8 | 9 | describe('paragraph - render elem', () => { 10 | const editor = createEditor() 11 | 12 | it('render paragraph', () => { 13 | expect(renderParagraphConf.type).toBe('paragraph') 14 | 15 | const elem = { type: 'paragraph', children: [] } 16 | const vnode = renderParagraphConf.renderElem(elem, null, editor) 17 | expect(vnode.sel).toBe('p') 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/text-style/menu/clear-style-menu.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description clear style menu test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Editor } from 'slate' 7 | import createEditor from '../../../../../tests/utils/create-editor' 8 | import ClearStyleMenu from '../../../src/modules/text-style/menu/ClearStyleMenu' 9 | 10 | describe('clear style menu', () => { 11 | let editor = createEditor() 12 | const startLocation = Editor.start(editor, []) 13 | const menu = new ClearStyleMenu() 14 | 15 | afterEach(() => { 16 | editor.select(startLocation) 17 | editor.clear() 18 | }) 19 | 20 | it('exec', () => { 21 | editor.select(startLocation) 22 | editor.insertText('hello') 23 | 24 | editor.select([]) 25 | editor.addMark('bold', true) 26 | editor.addMark('italic', true) 27 | 28 | menu.exec(editor, '') // 清空样式 29 | 30 | const marks = Editor.marks(editor) as any 31 | expect(marks.bold).toBeUndefined() 32 | expect(marks.italic).toBeUndefined() 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/text-style/parse-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse html test 3 | * @author wangfupeng 4 | */ 5 | 6 | // import { $ } from 'dom7' 7 | // import { parseStyleHtml } from '../../../../packages/basic-modules/src/modules/text-style/parse-style-html' 8 | 9 | describe('text style - parse style html', () => { 10 | it('占位', () => { 11 | expect(1 + 1).toBe(2) 12 | }) 13 | // TODO 执行以下代码会有 Dom7 一个怪异的 bug ,先暂且注释,后面再解决 wangfupeng 2022.01.17 14 | 15 | // it('bold', () => { 16 | // const $text = $('') 17 | // const textNode = { text: 'hello' } 18 | 19 | // // parse style 20 | // const res = parseStyleHtml($text[0], textNode) 21 | // expect(res).toEqual({ 22 | // text: 'hello', 23 | // bold: true, 24 | // }) 25 | // }) 26 | 27 | // // italic underline... 等 28 | }) 29 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/text-style/text-to-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description text to html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { styleToHtml } from '../../src/modules/text-style/style-to-html' 7 | 8 | describe('text style - text to html', () => { 9 | it('text to html', () => { 10 | const textNode = { 11 | text: '', 12 | bold: true, 13 | italic: true, 14 | underline: true, 15 | code: true, 16 | through: true, 17 | sub: true, 18 | sup: true, 19 | } 20 | 21 | const html1 = styleToHtml(textNode, 'hello') 22 | expect(html1).toBe( 23 | 'hello' 24 | ) 25 | 26 | const html2 = styleToHtml(textNode, 'world') 27 | expect(html2).toBe( 28 | 'world' 29 | ) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/todo/elem-to-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description todo elem to html test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { todoToHtmlConf } from '../../src/modules/todo/elem-to-html' 7 | 8 | describe('todo - elem to html', () => { 9 | it('todo elem to html', () => { 10 | expect(todoToHtmlConf.type).toBe('todo') 11 | 12 | const todoNode1 = { 13 | type: 'todo', 14 | checked: true, 15 | children: [{ text: '' }], 16 | } 17 | const html1 = todoToHtmlConf.elemToHtml(todoNode1, 'hello') 18 | expect(html1).toBe( 19 | `
hello
` 20 | ) 21 | 22 | const todoNode2 = { 23 | type: 'todo', 24 | checked: false, 25 | children: [{ text: '' }], 26 | } 27 | const html2 = todoToHtmlConf.elemToHtml(todoNode2, 'hello') 28 | expect(html2).toBe(`
hello
`) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/todo/plugin.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description todo plugin test 3 | * @author wangfupeng 4 | */ 5 | 6 | import withTodo from '../../src/modules/todo/plugin' 7 | import createEditor from '../../../../tests/utils/create-editor' 8 | 9 | describe('todo - plugin', () => { 10 | it('delete backward', () => { 11 | const editor = withTodo( 12 | createEditor({ 13 | content: [{ type: 'todo', children: [{ text: '' }] }], 14 | }) 15 | ) 16 | editor.select({ 17 | path: [0, 0], 18 | offset: 0, 19 | }) 20 | 21 | const todoElems1 = editor.getElemsByType('todo') 22 | expect(todoElems1.length).toBe(1) 23 | 24 | editor.deleteBackward('character') 25 | 26 | const todoElems2 = editor.getElemsByType('todo') 27 | expect(todoElems2.length).toBe(0) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/todo/pre-parse-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description todo pre-parse html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { $ } from 'dom7' 7 | import { preParseHtmlConf } from '../../src/modules/todo/pre-parse-html' 8 | 9 | describe('todo - pre-parse html', () => { 10 | it('pre-parse html', () => { 11 | // v4 todo html 格式 12 | const $ul = $( 13 | '
  • hello world
' 14 | ) 15 | 16 | // match selector 17 | expect($ul[0].matches(preParseHtmlConf.selector)).toBeTruthy() 18 | 19 | // parse 20 | const res = preParseHtmlConf.preParseHtml($ul[0]) 21 | expect(res.outerHTML).toBe( 22 | '
hello world
' 23 | ) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/basic-modules/__tests__/todo/render-elem.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description todo render elem 3 | * @author wangfupeng 4 | */ 5 | 6 | import createEditor from '../../../../tests/utils/create-editor' 7 | import { renderTodoConf } from '../../src/modules/todo/render-elem' 8 | 9 | describe('todo - render elem', () => { 10 | const editor = createEditor() 11 | 12 | it('render elem', () => { 13 | expect(renderTodoConf.type).toBe('todo') 14 | 15 | const todo = { type: 'todo', checked: true, children: [{ text: '' }] } 16 | const vnode = renderTodoConf.renderElem(todo, null, editor) as any 17 | expect(vnode.sel).toBe('div') 18 | expect(vnode.children.length).toBe(2) 19 | 20 | const spanForInput = vnode.children[0] 21 | expect(spanForInput.sel).toBe('span') 22 | expect(spanForInput.data.contentEditable).toBe(false) 23 | 24 | const input = spanForInput.children[0] 25 | expect(input.sel).toBe('input') 26 | expect(input.data.type).toBe('checkbox') 27 | expect(input.data.checked).toBe(true) 28 | expect(typeof input.data.on.change).toBe('function') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /packages/basic-modules/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config' 2 | import pkg from './package.json' 3 | 4 | const name = 'WangEditorBasicModules' 5 | 6 | const configList = [] 7 | 8 | // esm 9 | const esmConf = createRollupConfig({ 10 | output: { 11 | file: pkg.module, 12 | format: 'esm', 13 | name, 14 | }, 15 | }) 16 | configList.push(esmConf) 17 | 18 | // umd 19 | const umdConf = createRollupConfig({ 20 | output: { 21 | file: pkg.main, 22 | format: 'umd', 23 | name, 24 | }, 25 | }) 26 | configList.push(umdConf) 27 | 28 | export default configList 29 | -------------------------------------------------------------------------------- /packages/basic-modules/src/assets/blockquote.less: -------------------------------------------------------------------------------- 1 | @import "../../../vars.less"; 2 | 3 | .w-e-text-container [data-slate-editor] blockquote { 4 | display: block; 5 | border-left: 8px solid @textarea-selected-border-color; 6 | padding: 10px 10px; 7 | margin: 10px 0; 8 | line-height: 1.5; 9 | font-size: 100%; 10 | background-color: @textarea-slight-bg-color; 11 | } 12 | -------------------------------------------------------------------------------- /packages/basic-modules/src/assets/code-block.less: -------------------------------------------------------------------------------- 1 | @import "../../../vars.less"; 2 | 3 | .w-e-text-container [data-slate-editor] pre>code { 4 | display: block; 5 | border: 1px solid @textarea-slight-border-color; 6 | border-radius: 4px 4px; 7 | text-indent: 0; 8 | background-color: @textarea-slight-bg-color; 9 | padding: 10px; 10 | font-size: @size; 11 | } 12 | -------------------------------------------------------------------------------- /packages/basic-modules/src/assets/color.less: -------------------------------------------------------------------------------- 1 | @import "../../../vars.less"; 2 | 3 | .w-e-panel-content-color { 4 | list-style: none; 5 | text-align: left; 6 | width: 230px; 7 | 8 | li { 9 | display: inline-block; 10 | padding: 2px; 11 | cursor: pointer; 12 | border-radius: 3px 3px; 13 | border: 1px solid @toolbar-bg-color; 14 | 15 | &:hover { 16 | border-color: @toolbar-color; 17 | } 18 | 19 | .color-block { 20 | width: 17px; 21 | height: 17px; 22 | border: 1px solid @toolbar-border-color; 23 | border-radius: 3px 3px; 24 | } 25 | } 26 | 27 | .active { 28 | border-color: @toolbar-color; 29 | } 30 | 31 | .clear { 32 | width: 100%; 33 | line-height: 1.5; 34 | margin-bottom: 5px; 35 | 36 | svg { 37 | width: 16px; 38 | height: 16px; 39 | margin-bottom: -4px; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /packages/basic-modules/src/assets/divider.less: -------------------------------------------------------------------------------- 1 | @import "../../../vars.less"; 2 | 3 | .w-e-textarea-divider { 4 | padding: 20px 20px; 5 | margin: 20px auto; 6 | border-radius: 3px; 7 | 8 | // &:hover { 9 | // background-color: @textarea-slight-bg-color; 10 | // } 11 | 12 | hr { 13 | display: block; 14 | border: 0; 15 | height: 1px; 16 | background-color: @textarea-border-color; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/basic-modules/src/assets/emotion.less: -------------------------------------------------------------------------------- 1 | @import "../../../vars.less"; 2 | 3 | .w-e-panel-content-emotion { 4 | list-style: none; 5 | text-align: left; 6 | width: 300px; 7 | font-size: 20px; 8 | 9 | li { 10 | display: inline-block; 11 | padding: 0 5px; 12 | cursor: pointer; 13 | border-radius: 3px 3px; 14 | 15 | &:hover { 16 | background-color: @textarea-slight-bg-color; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /packages/basic-modules/src/assets/index.less: -------------------------------------------------------------------------------- 1 | @import "simple-style.less"; 2 | @import "color.less"; 3 | @import "blockquote.less"; 4 | @import "emotion.less"; 5 | @import "divider.less"; 6 | @import "blockquote.less"; 7 | @import "code-block.less"; 8 | @import "image.less"; 9 | -------------------------------------------------------------------------------- /packages/basic-modules/src/assets/simple-style.less: -------------------------------------------------------------------------------- 1 | @import "../../../vars.less"; 2 | 3 | .w-e-text-container [data-slate-editor] code { 4 | font-family: monospace; 5 | background-color: @textarea-slight-bg-color; 6 | padding: 3px; 7 | border-radius: 3px; 8 | } 9 | -------------------------------------------------------------------------------- /packages/basic-modules/src/locale/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { i18nAddResources } from '@wangeditor/core' 7 | import enResources from './en' 8 | import zhResources from './zh-CN' 9 | 10 | i18nAddResources('en', enResources) 11 | i18nAddResources('zh-CN', zhResources) 12 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/blockquote/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 自定义 element 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Text } from 'slate' 7 | 8 | //【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts 9 | 10 | export type BlockQuoteElement = { 11 | type: 'blockquote' 12 | children: Text[] 13 | } 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/blockquote/elem-to-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description to html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element } from 'slate' 7 | 8 | function quoteToHtml(elem: Element, childrenHtml: string): string { 9 | return `
${childrenHtml}
` 10 | } 11 | 12 | export const quoteToHtmlConf = { 13 | type: 'blockquote', 14 | elemToHtml: quoteToHtml, 15 | } 16 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/blockquote/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description blockquote entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import { renderBlockQuoteConf } from './render-elem' 8 | import { quoteToHtmlConf } from './elem-to-html' 9 | import { parseHtmlConf } from './parse-elem-html' 10 | import { blockquoteMenuConf } from './menu/index' 11 | import withBlockquote from './plugin' 12 | 13 | const blockquote: Partial = { 14 | renderElems: [renderBlockQuoteConf], 15 | elemsToHtml: [quoteToHtmlConf], 16 | parseElemsHtml: [parseHtmlConf], 17 | menus: [blockquoteMenuConf], 18 | editorPlugin: withBlockquote, 19 | } 20 | 21 | export default blockquote 22 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/blockquote/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description block quote menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import BlockquoteMenu from './BlockquoteMenu' 7 | 8 | export const blockquoteMenuConf = { 9 | key: 'blockquote', 10 | factory() { 11 | return new BlockquoteMenu() 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/blockquote/parse-elem-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Descendant, Text } from 'slate' 7 | import $, { DOMElement } from '../../utils/dom' 8 | import { IDomEditor } from '@wangeditor/core' 9 | import { BlockQuoteElement } from './custom-types' 10 | 11 | function parseHtml( 12 | elem: DOMElement, 13 | children: Descendant[], 14 | editor: IDomEditor 15 | ): BlockQuoteElement { 16 | const $elem = $(elem) 17 | 18 | children = children.filter(child => { 19 | if (Text.isText(child)) return true 20 | if (editor.isInline(child)) return true 21 | return false 22 | }) 23 | 24 | // 无 children ,则用纯文本 25 | if (children.length === 0) { 26 | children = [{ text: $elem.text().replace(/\s+/gm, ' ') }] 27 | } 28 | 29 | return { 30 | type: 'blockquote', 31 | // @ts-ignore 32 | children, 33 | } 34 | } 35 | 36 | export const parseHtmlConf = { 37 | selector: 'blockquote:not([data-w-e-type])', // data-w-e-type 属性,留给自定义元素,保证扩展性 38 | parseElemHtml: parseHtml, 39 | } 40 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/blockquote/render-elem.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description render elem 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element as SlateElement } from 'slate' 7 | import { jsx, VNode } from 'snabbdom' 8 | import { IDomEditor } from '@wangeditor/core' 9 | 10 | /** 11 | * render block quote elem 12 | * @param elemNode slate elem 13 | * @param children children 14 | * @param editor editor 15 | * @returns vnode 16 | */ 17 | function renderBlockQuote( 18 | elemNode: SlateElement, 19 | children: VNode[] | null, 20 | editor: IDomEditor 21 | ): VNode { 22 | const vnode =
{children}
23 | return vnode 24 | } 25 | 26 | export const renderBlockQuoteConf = { 27 | type: 'blockquote', 28 | renderElem: renderBlockQuote, 29 | } 30 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/code-block/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 自定义 element 3 | * @author wangfupeng 4 | */ 5 | 6 | //【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts 7 | 8 | type PureText = { 9 | text: string 10 | } 11 | 12 | export type PreElement = { 13 | type: 'pre' 14 | children: CodeElement[] 15 | } 16 | 17 | export type CodeElement = { 18 | type: 'code' 19 | language: string 20 | children: PureText[] 21 | } 22 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/code-block/elem-to-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description to html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element } from 'slate' 7 | 8 | function codeToHtml(elem: Element, childrenHtml: string): string { 9 | // 代码高亮 `class="language-xxx"` 在 code-highlight 中实现 10 | return `${childrenHtml}` 11 | } 12 | 13 | export const codeToHtmlConf = { 14 | type: 'code', 15 | elemToHtml: codeToHtml, 16 | } 17 | 18 | function preToHtml(elem: Element, childrenHtml: string): string { 19 | return `
${childrenHtml}
` 20 | } 21 | 22 | export const preToHtmlConf = { 23 | type: 'pre', 24 | elemToHtml: preToHtml, 25 | } 26 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/code-block/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description code block module 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import { codeBlockMenuConf } from './menu/index' 8 | import withCodeBlock from './plugin' 9 | import { renderPreConf, renderCodeConf } from './render-elem' 10 | import { preParseHtmlConf } from './pre-parse-html' 11 | import { parseCodeHtmlConf, parsePreHtmlConf } from './parse-elem-html' 12 | import { codeToHtmlConf, preToHtmlConf } from './elem-to-html' 13 | 14 | const codeBlockModule: Partial = { 15 | menus: [codeBlockMenuConf], 16 | editorPlugin: withCodeBlock, 17 | renderElems: [renderPreConf, renderCodeConf], 18 | elemsToHtml: [codeToHtmlConf, preToHtmlConf], 19 | preParseHtml: [preParseHtmlConf], 20 | parseElemsHtml: [parseCodeHtmlConf, parsePreHtmlConf], 21 | } 22 | 23 | export default codeBlockModule 24 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/code-block/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description code-block menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import CodeBlockMenu from './CodeBlockMenu' 7 | 8 | export const codeBlockMenuConf = { 9 | key: 'codeBlock', 10 | factory() { 11 | return new CodeBlockMenu() 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/code-block/pre-parse-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description pre parse html 3 | * @author wangfupeng 4 | */ 5 | 6 | import $, { DOMElement } from '../../utils/dom' 7 | import { getTagName } from '../../utils/dom' 8 | 9 | /** 10 | * pre-prase ,去掉其中的 (兼容 V4) 11 | * @param codeElem codeElem 12 | */ 13 | function preParse(codeElem: DOMElement): DOMElement { 14 | const $code = $(codeElem) 15 | const tagName = getTagName($code) 16 | if (tagName !== 'code') return codeElem 17 | 18 | const $xmp = $code.find('xmp') 19 | if ($xmp.length === 0) return codeElem // 不是 V4 格式 20 | 21 | const codeText = $xmp.text() 22 | $xmp.remove() 23 | $code.text(codeText) 24 | 25 | return $code[0] 26 | } 27 | 28 | export const preParseHtmlConf = { 29 | selector: 'pre>code', // 匹配 <pre> 下的 <code> 30 | preParseHtml: preParse, 31 | } 32 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/code-block/render-elem.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description render elem 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element as SlateElement } from 'slate' 7 | import { jsx, VNode } from 'snabbdom' 8 | import { IDomEditor } from '@wangeditor/core' 9 | 10 | function renderPre(elemNode: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode { 11 | const vnode = <pre>{children}</pre> 12 | return vnode 13 | } 14 | 15 | function renderCode(elemNode: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode { 16 | // 和 basic/simple-style module 的“行内代码”并不冲突。一个是根据 mark 渲染,一个是根据 node.type 渲染 17 | const vnode = <code>{children}</code> 18 | return vnode 19 | } 20 | 21 | export const renderPreConf = { 22 | type: 'pre', 23 | renderElem: renderPre, 24 | } 25 | 26 | export const renderCodeConf = { 27 | type: 'code', 28 | renderElem: renderCode, 29 | } 30 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/color/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 自定义 element 3 | * @author wangfupeng 4 | */ 5 | 6 | //【注意】需要把自定义的 Text 引入到最外层的 custom-types.d.ts 7 | 8 | export type ColorText = { 9 | text: string 10 | color?: string 11 | bgColor?: string 12 | } 13 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/color/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description color bgColor 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import { renderStyle } from './render-style' 8 | import { styleToHtml } from './style-to-html' 9 | import { preParseHtmlConf } from './pre-parse-html' 10 | import { parseStyleHtml } from './parse-style-html' 11 | import { colorMenuConf, bgColorMenuConf } from './menu/index' 12 | 13 | const color: Partial<IModuleConf> = { 14 | renderStyle, 15 | styleToHtml, 16 | preParseHtml: [preParseHtmlConf], 17 | parseStyleHtml, 18 | menus: [colorMenuConf, bgColorMenuConf], 19 | } 20 | 21 | export default color 22 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/color/menu/BgColorMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description bg color menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { t } from '@wangeditor/core' 7 | import BaseMenu from './BaseMenu' 8 | import { BG_COLOR_SVG } from '../../../constants/icon-svg' 9 | 10 | class BgColorMenu extends BaseMenu { 11 | readonly title = t('color.bgColor') 12 | readonly iconSvg = BG_COLOR_SVG 13 | readonly mark = 'bgColor' 14 | } 15 | 16 | export default BgColorMenu 17 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/color/menu/ColorMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description color menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { t } from '@wangeditor/core' 7 | import BaseMenu from './BaseMenu' 8 | import { FONT_COLOR_SVG } from '../../../constants/icon-svg' 9 | 10 | class ColorMenu extends BaseMenu { 11 | readonly title = t('color.color') 12 | readonly iconSvg = FONT_COLOR_SVG 13 | readonly mark = 'color' 14 | } 15 | 16 | export default ColorMenu 17 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/color/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description menu entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import ColorMenu from './ColorMenu' 7 | import BgColorMenu from './BgColorMenu' 8 | import { genColors, genBgColors } from './config' 9 | 10 | export const colorMenuConf = { 11 | key: 'color', 12 | factory() { 13 | return new ColorMenu() 14 | }, 15 | 16 | // 默认的菜单菜单配置,将存储在 editorConfig.MENU_CONF[key] 中 17 | // 创建编辑器时,可通过 editorConfig.MENU_CONF[key] = {...} 来修改 18 | config: { 19 | colors: genColors(), 20 | }, 21 | } 22 | 23 | export const bgColorMenuConf = { 24 | key: 'bgColor', 25 | factory() { 26 | return new BgColorMenu() 27 | }, 28 | config: { 29 | colors: genBgColors(), 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/color/parse-style-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse style html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Descendant, Text } from 'slate' 7 | import { IDomEditor } from '@wangeditor/core' 8 | import { ColorText } from './custom-types' 9 | import $, { DOMElement, getStyleValue } from '../../utils/dom' 10 | 11 | export function parseStyleHtml(text: DOMElement, node: Descendant, editor: IDomEditor): Descendant { 12 | const $text = $(text) 13 | if (!Text.isText(node)) return node 14 | 15 | const textNode = node as ColorText 16 | 17 | const color = getStyleValue($text, 'color') 18 | if (color) { 19 | textNode.color = color 20 | } 21 | 22 | let bgColor = getStyleValue($text, 'background-color') 23 | if (!bgColor) bgColor = getStyleValue($text, 'background') // word 背景色 24 | if (bgColor) { 25 | textNode.bgColor = bgColor 26 | } 27 | 28 | return textNode 29 | } 30 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/color/pre-parse-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description pre-parse html 3 | * @author wangfupeng 4 | */ 5 | 6 | import $, { DOMElement, getTagName } from '../../utils/dom' 7 | 8 | /** 9 | * pre-prase font ,兼容 V4 10 | * @param fontElem fontElem 11 | */ 12 | function preParse(fontElem: DOMElement): DOMElement { 13 | const $font = $(fontElem) 14 | const tagName = getTagName($font) 15 | if (tagName !== 'font') return fontElem 16 | 17 | // 处理 color (V4 使用 <font color="#ccc">xx</font> 格式) 18 | const color = $font.attr('color') || '' 19 | if (color) { 20 | $font.removeAttr('color') 21 | $font.css('color', color) 22 | } 23 | 24 | return $font[0] 25 | } 26 | 27 | export const preParseHtmlConf = { 28 | selector: 'font', 29 | preParseHtml: preParse, 30 | } 31 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/color/render-style.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description render color style 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Descendant } from 'slate' 7 | import { jsx, VNode } from 'snabbdom' 8 | import { addVnodeStyle } from '../../utils/vdom' 9 | import { ColorText } from './custom-types' 10 | 11 | /** 12 | * 添加样式 13 | * @param node text node 14 | * @param vnode vnode 15 | * @returns vnode 16 | */ 17 | export function renderStyle(node: Descendant, vnode: VNode): VNode { 18 | const { color, bgColor } = node as ColorText 19 | let styleVnode: VNode = vnode 20 | 21 | if (color) { 22 | addVnodeStyle(styleVnode, { color }) 23 | } 24 | if (bgColor) { 25 | addVnodeStyle(styleVnode, { backgroundColor: bgColor }) 26 | } 27 | 28 | return styleVnode 29 | } 30 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/common/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description common module 3 | * @author wangfupeng 4 | */ 5 | import { IModuleConf } from '@wangeditor/core' 6 | import { enterMenuConf } from './menu/index' 7 | 8 | const commonModule: Partial<IModuleConf> = { 9 | menus: [enterMenuConf], 10 | } 11 | 12 | export default commonModule 13 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/common/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description common menu config 3 | * @author wangfupeng 4 | */ 5 | 6 | import EnterMenu from './EnterMenu' 7 | 8 | export const enterMenuConf = { 9 | key: 'enter', 10 | factory() { 11 | return new EnterMenu() 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/divider/README.md: -------------------------------------------------------------------------------- 1 | # 分割线 -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/divider/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description divider element 3 | * @author wangfupeng 4 | */ 5 | 6 | //【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts 7 | 8 | type EmptyText = { 9 | text: '' 10 | } 11 | 12 | export type DividerElement = { 13 | type: 'divider' 14 | children: EmptyText[] 15 | } 16 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/divider/elem-to-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description to html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element } from 'slate' 7 | 8 | function dividerToHtml(elem: Element, childrenHtml: string): string { 9 | return `<hr/>` 10 | } 11 | 12 | export const dividerToHtmlConf = { 13 | type: 'divider', 14 | elemToHtml: dividerToHtml, 15 | } 16 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/divider/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description divider module 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import withDivider from './plugin' 8 | import { renderDividerConf } from './render-elem' 9 | import { dividerToHtmlConf } from './elem-to-html' 10 | import { parseHtmlConf } from './parse-elem-html' 11 | import { insertDividerMenuConf } from './menu/index' 12 | 13 | const image: Partial<IModuleConf> = { 14 | renderElems: [renderDividerConf], 15 | elemsToHtml: [dividerToHtmlConf], 16 | parseElemsHtml: [parseHtmlConf], 17 | menus: [insertDividerMenuConf], 18 | editorPlugin: withDivider, 19 | } 20 | 21 | export default image 22 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/divider/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description divider menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import InsertDividerMenu from './InsertDividerMenu' 7 | // import DeleteDividerMenu from './DeleteDividerMenu.ts' 8 | 9 | export const insertDividerMenuConf = { 10 | key: 'divider', 11 | factory() { 12 | return new InsertDividerMenu() 13 | }, 14 | } 15 | 16 | // export const deleteDividerMenuConf = { 17 | // key: 'deleteDivider', 18 | // factory() { 19 | // return new DeleteDividerMenu() 20 | // }, 21 | // } 22 | // divider 可用键盘删除了,所以注释掉该菜单 wangfupeng 22.02.23 23 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/divider/parse-elem-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Descendant } from 'slate' 7 | import $, { DOMElement } from '../../utils/dom' 8 | import { IDomEditor } from '@wangeditor/core' 9 | import { DividerElement } from './custom-types' 10 | 11 | function parseHtml(elem: DOMElement, children: Descendant[], editor: IDomEditor): DividerElement { 12 | return { 13 | type: 'divider', 14 | children: [{ text: '' }], // void node 有一个空白 text 15 | } 16 | } 17 | 18 | export const parseHtmlConf = { 19 | selector: 'hr:not([data-w-e-type])', // data-w-e-type 属性,留给自定义元素,保证扩展性 20 | parseElemHtml: parseHtml, 21 | } 22 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/emotion/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description emotion entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import { emotionMenuConf } from './menu/index' 8 | 9 | const emotion: Partial<IModuleConf> = { 10 | menus: [emotionMenuConf], 11 | } 12 | 13 | export default emotion 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/emotion/menu/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description menu config 3 | * @author wangfupeng 4 | */ 5 | 6 | export function genConfig() { 7 | const emotions = 8 | '😀 😃 😄 😁 😆 😅 😂 🤣 😊 😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 😛 😝 😜 🤓 😎 😏 😒 😞 😔 😟 😕 🙁 😣 😖 😫 😩 😢 😭 😤 😠 😡 😳 😱 😨 🤗 🤔 😶 😑 😬 🙄 😯 😴 😷 🤑 😈 🤡 💩 👻 💀 👀 👣 👐 🙌 👏 🤝 👍 👎 👊 ✊ 🤛 🤜 🤞 ✌️ 🤘 👌 👈 👉 👆 👇 ☝️ ✋ 🤚 🖐 🖖 👋 🤙 💪 🖕 ✍️ 🙏' 9 | return emotions.split(' ') 10 | } 11 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/emotion/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description emotion menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import EmotionMenu from './EmotionMenu' 7 | import { genConfig } from './config' 8 | 9 | export const emotionMenuConf = { 10 | key: 'emotion', 11 | factory() { 12 | return new EmotionMenu() 13 | }, 14 | 15 | // 默认的菜单菜单配置,将存储在 editorConfig.MENU_CONF[key] 中 16 | // 创建编辑器时,可通过 editorConfig.MENU_CONF[key] = {...} 来修改 17 | config: { 18 | emotions: genConfig(), 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/font-size-family/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 自定义 element 3 | * @author wangfupeng 4 | */ 5 | 6 | //【注意】需要把自定义的 Text 引入到最外层的 custom-types.d.ts 7 | 8 | export type FontSizeAndFamilyText = { 9 | text: string 10 | fontSize?: string 11 | fontFamily?: string 12 | } 13 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/font-size-family/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description font-size font-family 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import { renderStyle } from './render-style' 8 | import { styleToHtml } from './style-to-html' 9 | import { preParseHtmlConf } from './pre-parse-html' 10 | import { parseStyleHtml } from './parse-style-html' 11 | import { fontSizeMenuConf, fontFamilyMenuConf } from './menu/index' 12 | 13 | const fontSizeAndFamily: Partial<IModuleConf> = { 14 | renderStyle, 15 | styleToHtml, 16 | preParseHtml: [preParseHtmlConf], 17 | parseStyleHtml, 18 | menus: [fontSizeMenuConf, fontFamilyMenuConf], 19 | } 20 | 21 | export default fontSizeAndFamily 22 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/font-size-family/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description font-size font-family menu entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import FontSizeMenu from './FontSizeMenu' 7 | import FontFamilyMenu from './FontFamilyMenu' 8 | import { genFontSizeConfig, getFontFamilyConfig } from './config' 9 | 10 | export const fontSizeMenuConf = { 11 | key: 'fontSize', 12 | factory() { 13 | return new FontSizeMenu() 14 | }, 15 | 16 | // 默认的菜单菜单配置,将存储在 editorConfig.MENU_CONF[key] 中 17 | // 创建编辑器时,可通过 editorConfig.MENU_CONF[key] = {...} 来修改 18 | config: { 19 | fontSizeList: genFontSizeConfig(), 20 | }, 21 | } 22 | 23 | export const fontFamilyMenuConf = { 24 | key: 'fontFamily', 25 | factory() { 26 | return new FontFamilyMenu() 27 | }, 28 | config: { 29 | fontFamilyList: getFontFamilyConfig(), 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/font-size-family/render-style.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description render font-size font-family style 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Descendant } from 'slate' 7 | import { jsx, VNode } from 'snabbdom' 8 | import { addVnodeStyle } from '../../utils/vdom' 9 | import { FontSizeAndFamilyText } from './custom-types' 10 | 11 | /** 12 | * 添加样式 13 | * @param node slate elem 14 | * @param vnode vnode 15 | * @returns vnode 16 | */ 17 | export function renderStyle(node: Descendant, vnode: VNode): VNode { 18 | const { fontSize, fontFamily } = node as FontSizeAndFamilyText 19 | let styleVnode: VNode = vnode 20 | 21 | if (fontSize) { 22 | addVnodeStyle(styleVnode, { fontSize }) 23 | } 24 | if (fontFamily) { 25 | addVnodeStyle(styleVnode, { fontFamily }) 26 | } 27 | 28 | return styleVnode 29 | } 30 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/full-screen/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 全屏 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import { fullScreenConf } from './menu/index' 8 | 9 | const fullScreen: Partial<IModuleConf> = { 10 | menus: [fullScreenConf], 11 | } 12 | 13 | export default fullScreen 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/full-screen/menu/FullScreen.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description redo menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IButtonMenu, IDomEditor, t } from '@wangeditor/core' 7 | import { FULL_SCREEN_SVG } from '../../../constants/icon-svg' 8 | 9 | class FullScreen implements IButtonMenu { 10 | title = t('fullScreen.title') 11 | iconSvg = FULL_SCREEN_SVG 12 | tag = 'button' 13 | alwaysEnable = true 14 | 15 | getValue(editor: IDomEditor): string | boolean { 16 | return '' 17 | } 18 | 19 | isActive(editor: IDomEditor): boolean { 20 | return editor.isFullScreen 21 | } 22 | 23 | isDisabled(editor: IDomEditor): boolean { 24 | return false 25 | } 26 | 27 | exec(editor: IDomEditor, value: string | boolean) { 28 | if (editor.isFullScreen) { 29 | editor.unFullScreen() 30 | } else { 31 | editor.fullScreen() 32 | } 33 | } 34 | } 35 | 36 | export default FullScreen 37 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/full-screen/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description menu entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import FullScreen from './FullScreen' 7 | 8 | export const fullScreenConf = { 9 | key: 'fullScreen', 10 | factory() { 11 | return new FullScreen() 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/header/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 自定义 element 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Text } from 'slate' 7 | 8 | //【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts 9 | 10 | export type Header1Element = { 11 | type: 'header1' 12 | children: Text[] 13 | } 14 | 15 | export type Header2Element = { 16 | type: 'header2' 17 | children: Text[] 18 | } 19 | 20 | export type Header3Element = { 21 | type: 'header3' 22 | children: Text[] 23 | } 24 | 25 | export type Header4Element = { 26 | type: 'header4' 27 | children: Text[] 28 | } 29 | 30 | export type Header5Element = { 31 | type: 'header5' 32 | children: Text[] 33 | } 34 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/header/elem-to-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description to html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element } from 'slate' 7 | 8 | function genToHtmlFn(level: number) { 9 | function headerToHtml(elem: Element, childrenHtml: string): string { 10 | return `<h${level}>${childrenHtml}</h${level}>` 11 | } 12 | return headerToHtml 13 | } 14 | 15 | export const header1ToHtmlConf = { 16 | type: 'header1', 17 | elemToHtml: genToHtmlFn(1), 18 | } 19 | 20 | export const header2ToHtmlConf = { 21 | type: 'header2', 22 | elemToHtml: genToHtmlFn(2), 23 | } 24 | 25 | export const header3ToHtmlConf = { 26 | type: 'header3', 27 | elemToHtml: genToHtmlFn(3), 28 | } 29 | 30 | export const header4ToHtmlConf = { 31 | type: 'header4', 32 | elemToHtml: genToHtmlFn(4), 33 | } 34 | 35 | export const header5ToHtmlConf = { 36 | type: 'header5', 37 | elemToHtml: genToHtmlFn(5), 38 | } 39 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/header/menu/Header1ButtonMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description header1 button menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import HeaderButtonMenuBase from './HeaderButtonMenuBase' 7 | 8 | class Header1ButtonMenu extends HeaderButtonMenuBase { 9 | title = 'H1' 10 | type = 'header1' 11 | } 12 | 13 | export default Header1ButtonMenu 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/header/menu/Header2ButtonMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description header2 button menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import HeaderButtonMenuBase from './HeaderButtonMenuBase' 7 | 8 | class Header2ButtonMenu extends HeaderButtonMenuBase { 9 | title = 'H2' 10 | type = 'header2' 11 | } 12 | 13 | export default Header2ButtonMenu 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/header/menu/Header3ButtonMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description header3 button menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import HeaderButtonMenuBase from './HeaderButtonMenuBase' 7 | 8 | class Header3ButtonMenu extends HeaderButtonMenuBase { 9 | title = 'H3' 10 | type = 'header3' 11 | } 12 | 13 | export default Header3ButtonMenu 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/header/menu/Header4ButtonMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description header4 button menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import HeaderButtonMenuBase from './HeaderButtonMenuBase' 7 | 8 | class Header4ButtonMenu extends HeaderButtonMenuBase { 9 | title = 'H4' 10 | type = 'header4' 11 | } 12 | 13 | export default Header4ButtonMenu 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/header/menu/Header5ButtonMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description header5 button menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import HeaderButtonMenuBase from './HeaderButtonMenuBase' 7 | 8 | class Header5ButtonMenu extends HeaderButtonMenuBase { 9 | title = 'H5' 10 | type = 'header5' 11 | } 12 | 13 | export default Header5ButtonMenu 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/image/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description image element 3 | * @author wangfupeng 4 | */ 5 | 6 | //【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts 7 | 8 | type EmptyText = { 9 | text: '' 10 | } 11 | 12 | export type ImageStyle = { 13 | width?: string 14 | height?: string 15 | } 16 | 17 | export type ImageElement = { 18 | type: 'image' 19 | src: string 20 | alt?: string 21 | href?: string 22 | style?: ImageStyle 23 | children: EmptyText[] 24 | } 25 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/image/elem-to-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description to html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element } from 'slate' 7 | import { ImageElement } from './custom-types' 8 | 9 | function imageToHtml(elemNode: Element, childrenHtml: string): string { 10 | const { src, alt = '', href = '', style = {} } = elemNode as ImageElement 11 | const { width = '', height = '' } = style 12 | 13 | let styleStr = '' 14 | if (width) styleStr += `width: ${width};` 15 | if (height) styleStr += `height: ${height};` 16 | return `<img src="${src}" alt="${alt}" data-href="${href}" style="${styleStr}"/>` 17 | } 18 | 19 | export const imageToHtmlConf = { 20 | type: 'image', 21 | elemToHtml: imageToHtml, 22 | } 23 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/image/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description image module entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import withImage from './plugin' 8 | import { renderImageConf } from './render-elem' 9 | import { imageToHtmlConf } from './elem-to-html' 10 | import { parseHtmlConf } from './parse-elem-html' 11 | import { 12 | insertImageMenuConf, 13 | deleteImageMenuConf, 14 | editImageMenuConf, 15 | viewImageLinkMenuConf, 16 | imageWidth30MenuConf, 17 | imageWidth50MenuConf, 18 | imageWidth100MenuConf, 19 | } from './menu/index' 20 | 21 | const image: Partial<IModuleConf> = { 22 | renderElems: [renderImageConf], 23 | elemsToHtml: [imageToHtmlConf], 24 | parseElemsHtml: [parseHtmlConf], 25 | menus: [ 26 | insertImageMenuConf, 27 | deleteImageMenuConf, 28 | editImageMenuConf, 29 | viewImageLinkMenuConf, 30 | imageWidth30MenuConf, 31 | imageWidth50MenuConf, 32 | imageWidth100MenuConf, 33 | ], 34 | editorPlugin: withImage, 35 | } 36 | 37 | export default image 38 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/image/menu/Width100.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description image width 100% 3 | * @author wangfupeng 4 | */ 5 | 6 | import ImageWidthBaseClass from './WidthBase' 7 | 8 | class ImageWidth100 extends ImageWidthBaseClass { 9 | readonly title = '100%' // 菜单标题 10 | readonly value = '100%' // css width 的值 11 | } 12 | 13 | export default ImageWidth100 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/image/menu/Width30.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description image width 30% 3 | * @author wangfupeng 4 | */ 5 | 6 | import ImageWidthBaseClass from './WidthBase' 7 | 8 | class ImageWidth30 extends ImageWidthBaseClass { 9 | readonly title = '30%' // 菜单标题 10 | readonly value = '30%' // css width 的值 11 | } 12 | 13 | export default ImageWidth30 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/image/menu/Width50.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description image width 50% 3 | * @author wangfupeng 4 | */ 5 | 6 | import ImageWidthBaseClass from './WidthBase' 7 | 8 | class ImageWidth50 extends ImageWidthBaseClass { 9 | readonly title = '50%' // 菜单标题 10 | readonly value = '50%' // css width 的值 11 | } 12 | 13 | export default ImageWidth50 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/image/parse-elem-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Descendant } from 'slate' 7 | import { IDomEditor } from '@wangeditor/core' 8 | import { ImageElement } from './custom-types' 9 | import $, { DOMElement, getStyleValue } from '../../utils/dom' 10 | 11 | function parseHtml(elem: DOMElement, children: Descendant[], editor: IDomEditor): ImageElement { 12 | const $elem = $(elem) 13 | let href = $elem.attr('data-href') || '' 14 | href = decodeURIComponent(href) // 兼容 V4 15 | 16 | return { 17 | type: 'image', 18 | src: $elem.attr('src') || '', 19 | alt: $elem.attr('alt') || '', 20 | href, 21 | style: { 22 | width: getStyleValue($elem, 'width'), 23 | height: getStyleValue($elem, 'height'), 24 | }, 25 | children: [{ text: '' }], // void node 有一个空白 text 26 | } 27 | } 28 | 29 | export const parseHtmlConf = { 30 | selector: 'img:not([data-w-e-type])', // data-w-e-type 属性,留给自定义元素,保证扩展性 31 | parseElemHtml: parseHtml, 32 | } 33 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/image/plugin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description editor 插件,重写 editor API 3 | * @author wangfupeng 4 | */ 5 | 6 | // import { Editor, Path, Operation } from 'slate' 7 | import { IDomEditor } from '@wangeditor/core' 8 | 9 | function withImage<T extends IDomEditor>(editor: T): T { 10 | const { isInline, isVoid, insertNode } = editor 11 | const newEditor = editor 12 | 13 | // 重写 isInline 14 | newEditor.isInline = elem => { 15 | const { type } = elem 16 | 17 | if (type === 'image') { 18 | return true 19 | } 20 | 21 | return isInline(elem) 22 | } 23 | 24 | // 重写 isVoid 25 | newEditor.isVoid = elem => { 26 | const { type } = elem 27 | 28 | if (type === 'image') { 29 | return true 30 | } 31 | 32 | return isVoid(elem) 33 | } 34 | 35 | // 返回 editor ,重要! 36 | return newEditor 37 | } 38 | 39 | export default withImage 40 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/indent/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 自定义 element 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Text } from 'slate' 7 | 8 | //【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts 9 | 10 | export type IndentElement = { 11 | type: string 12 | indent?: string | null 13 | children: Text[] 14 | } 15 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/indent/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description indent entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import { renderStyle } from './render-style' 8 | import { styleToHtml } from './style-to-html' 9 | import { preParseHtmlConf } from './pre-parse-html' 10 | import { parseStyleHtml } from './parse-style-html' 11 | import { indentMenuConf, delIndentMenuConf } from './menu/index' 12 | 13 | const indent: Partial<IModuleConf> = { 14 | renderStyle, 15 | styleToHtml, 16 | preParseHtml: [preParseHtmlConf], 17 | parseStyleHtml, 18 | menus: [indentMenuConf, delIndentMenuConf], 19 | } 20 | 21 | export default indent 22 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/indent/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description indent menu entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import DecreaseIndentMenu from './DecreaseIndentMenu' 7 | import IncreaseIndentMenu from './IncreaseIndentMenu' 8 | 9 | export const indentMenuConf = { 10 | key: 'indent', 11 | factory() { 12 | return new IncreaseIndentMenu() 13 | }, 14 | } 15 | 16 | export const delIndentMenuConf = { 17 | key: 'delIndent', 18 | factory() { 19 | return new DecreaseIndentMenu() 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/indent/parse-style-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse style html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Descendant, Element } from 'slate' 7 | import { IDomEditor } from '@wangeditor/core' 8 | import { IndentElement } from './custom-types' 9 | import $, { DOMElement, getStyleValue } from '../../utils/dom' 10 | 11 | export function parseStyleHtml(elem: DOMElement, node: Descendant, editor: IDomEditor): Descendant { 12 | const $elem = $(elem) 13 | if (!Element.isElement(node)) return node 14 | 15 | const elemNode = node as IndentElement 16 | 17 | const indent = getStyleValue($elem, 'text-indent') 18 | const indentNumber = parseInt(indent, 10) 19 | if (indent && indentNumber > 0) { 20 | elemNode.indent = indent 21 | } 22 | 23 | return elemNode 24 | } 25 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/indent/pre-parse-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description pre-parse html 3 | * @author wangfupeng 4 | */ 5 | 6 | import $, { DOMElement, getStyleValue } from '../../utils/dom' 7 | 8 | /** 9 | * pre-prase text-indent 兼容 V4 和 V5 早期格式(都使用 padding-left) 10 | * @param elem elem 11 | */ 12 | function preParse(elem: DOMElement): DOMElement { 13 | const $elem = $(elem) 14 | const paddingLeft = getStyleValue($elem, 'padding-left') 15 | 16 | if (/\dem/.test(paddingLeft)) { 17 | // 如 '2em' ,V4 格式 18 | $elem.css('text-indent', '2em') 19 | } 20 | 21 | if (/\dpx/.test(paddingLeft)) { 22 | // px 单位 23 | const num = parseInt(paddingLeft, 10) 24 | if (num % 32 === 0) { 25 | // 如 32px 64px ,V5 早期格式 26 | $elem.css('text-indent', '2em') 27 | } 28 | } 29 | 30 | return $elem[0] 31 | } 32 | 33 | export const preParseHtmlConf = { 34 | selector: 'p,h1,h2,h3,h4,h5', 35 | preParseHtml: preParse, 36 | } 37 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/indent/render-style.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description render indent style 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element, Descendant } from 'slate' 7 | import { jsx, VNode } from 'snabbdom' 8 | import { addVnodeStyle } from '../../utils/vdom' 9 | import { IndentElement } from './custom-types' 10 | 11 | /** 12 | * 添加样式 13 | * @param node slate elem 14 | * @param vnode vnode 15 | * @returns vnode 16 | */ 17 | export function renderStyle(node: Descendant, vnode: VNode): VNode { 18 | if (!Element.isElement(node)) return vnode 19 | 20 | const { indent } = node as IndentElement // 如 '2em' 21 | let styleVnode: VNode = vnode 22 | 23 | if (indent) { 24 | addVnodeStyle(styleVnode, { textIndent: indent }) 25 | } 26 | 27 | return styleVnode 28 | } 29 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/indent/style-to-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description textStyle to html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element, Descendant } from 'slate' 7 | import $, { getOuterHTML } from '../../utils/dom' 8 | import { IndentElement } from './custom-types' 9 | 10 | export function styleToHtml(node: Descendant, elemHtml: string): string { 11 | if (!Element.isElement(node)) return elemHtml 12 | 13 | const { indent } = node as IndentElement // 如 '2em' 14 | if (!indent) return elemHtml 15 | 16 | // 设置样式 17 | const $elem = $(elemHtml) 18 | $elem.css('text-indent', indent) 19 | 20 | // 输出 html 21 | return getOuterHTML($elem) 22 | } 23 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/justify/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 自定义 element 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Text } from 'slate' 7 | 8 | //【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts 9 | 10 | export type JustifyElement = { 11 | type: string 12 | textAlign?: string 13 | children: Text[] 14 | } 15 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/justify/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description justify module entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import { renderStyle } from './render-style' 8 | import { styleToHtml } from './style-to-html' 9 | import { parseStyleHtml } from './parse-style-html' 10 | import { 11 | justifyLeftMenuConf, 12 | justifyRightMenuConf, 13 | justifyCenterMenuConf, 14 | justifyJustifyMenuConf, 15 | } from './menu/index' 16 | 17 | const justify: Partial<IModuleConf> = { 18 | renderStyle, 19 | styleToHtml, 20 | parseStyleHtml, 21 | menus: [justifyLeftMenuConf, justifyRightMenuConf, justifyCenterMenuConf, justifyJustifyMenuConf], 22 | } 23 | 24 | export default justify 25 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/justify/menu/JustifyCenterMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description justify center menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Transforms, Element } from 'slate' 7 | import { IDomEditor, t } from '@wangeditor/core' 8 | import BaseMenu from './BaseMenu' 9 | import { JUSTIFY_CENTER_SVG } from '../../../constants/icon-svg' 10 | 11 | class JustifyCenterMenu extends BaseMenu { 12 | readonly title = t('justify.center') 13 | readonly iconSvg = JUSTIFY_CENTER_SVG 14 | 15 | exec(editor: IDomEditor, value: string | boolean): void { 16 | Transforms.setNodes( 17 | editor, 18 | { 19 | textAlign: 'center', 20 | }, 21 | { match: n => Element.isElement(n) && !editor.isInline(n) } // inline 元素设置text-align 是没作用的 22 | ) 23 | } 24 | } 25 | 26 | export default JustifyCenterMenu 27 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/justify/menu/JustifyJustifyMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 两端对齐 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Transforms, Element } from 'slate' 7 | import { IDomEditor, t } from '@wangeditor/core' 8 | import BaseMenu from './BaseMenu' 9 | import { JUSTIFY_JUSTIFY_SVG } from '../../../constants/icon-svg' 10 | 11 | class JustifyJustifyMenu extends BaseMenu { 12 | readonly title = t('justify.justify') 13 | readonly iconSvg = JUSTIFY_JUSTIFY_SVG 14 | 15 | exec(editor: IDomEditor, value: string | boolean): void { 16 | Transforms.setNodes( 17 | editor, 18 | { 19 | textAlign: 'justify', 20 | }, 21 | { match: n => Element.isElement(n) && !editor.isInline(n) } 22 | ) 23 | } 24 | } 25 | 26 | export default JustifyJustifyMenu 27 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/justify/menu/JustifyLeftMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description justify left menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Transforms, Element } from 'slate' 7 | import { IDomEditor, t } from '@wangeditor/core' 8 | import BaseMenu from './BaseMenu' 9 | import { JUSTIFY_LEFT_SVG } from '../../../constants/icon-svg' 10 | 11 | class JustifyLeftMenu extends BaseMenu { 12 | readonly title = t('justify.left') 13 | readonly iconSvg = JUSTIFY_LEFT_SVG 14 | 15 | exec(editor: IDomEditor, value: string | boolean): void { 16 | Transforms.setNodes( 17 | editor, 18 | { 19 | textAlign: 'left', 20 | }, 21 | { match: n => Element.isElement(n) && !editor.isInline(n) } 22 | ) 23 | } 24 | } 25 | 26 | export default JustifyLeftMenu 27 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/justify/menu/JustifyRightMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description justify right menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Transforms, Element } from 'slate' 7 | import { IDomEditor, t } from '@wangeditor/core' 8 | import BaseMenu from './BaseMenu' 9 | import { JUSTIFY_RIGHT_SVG } from '../../../constants/icon-svg' 10 | 11 | class JustifyRightMenu extends BaseMenu { 12 | readonly title = t('justify.right') 13 | readonly iconSvg = JUSTIFY_RIGHT_SVG 14 | 15 | exec(editor: IDomEditor, value: string | boolean): void { 16 | Transforms.setNodes( 17 | editor, 18 | { 19 | textAlign: 'right', 20 | }, 21 | { match: n => Element.isElement(n) && !editor.isInline(n) } 22 | ) 23 | } 24 | } 25 | 26 | export default JustifyRightMenu 27 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/justify/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description justify menu entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import JustifyLeftMenu from './JustifyLeftMenu' 7 | import JustifyRightMenu from './JustifyRightMenu' 8 | import JustifyCenterMenu from './JustifyCenterMenu' 9 | import JustifyJustifyMenu from './JustifyJustifyMenu' 10 | 11 | export const justifyLeftMenuConf = { 12 | key: 'justifyLeft', 13 | factory() { 14 | return new JustifyLeftMenu() 15 | }, 16 | } 17 | 18 | export const justifyRightMenuConf = { 19 | key: 'justifyRight', 20 | factory() { 21 | return new JustifyRightMenu() 22 | }, 23 | } 24 | 25 | export const justifyCenterMenuConf = { 26 | key: 'justifyCenter', 27 | factory() { 28 | return new JustifyCenterMenu() 29 | }, 30 | } 31 | 32 | export const justifyJustifyMenuConf = { 33 | key: 'justifyJustify', 34 | factory() { 35 | return new JustifyJustifyMenu() 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/justify/parse-style-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse style html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Descendant, Element } from 'slate' 7 | import { IDomEditor } from '@wangeditor/core' 8 | import { JustifyElement } from './custom-types' 9 | import $, { DOMElement, getStyleValue } from '../../utils/dom' 10 | 11 | export function parseStyleHtml(elem: DOMElement, node: Descendant, editor: IDomEditor): Descendant { 12 | const $elem = $(elem) 13 | if (!Element.isElement(node)) return node 14 | 15 | const elemNode = node as JustifyElement 16 | 17 | const textAlign = getStyleValue($elem, 'text-align') 18 | if (textAlign) { 19 | elemNode.textAlign = textAlign 20 | } 21 | 22 | return elemNode 23 | } 24 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/justify/render-style.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description render justify style 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Descendant, Element } from 'slate' 7 | import { jsx, VNode } from 'snabbdom' 8 | import { addVnodeStyle } from '../../utils/vdom' 9 | import { JustifyElement } from './custom-types' 10 | 11 | /** 12 | * 添加样式 13 | * @param node slate elem 14 | * @param vnode vnode 15 | * @returns vnode 16 | */ 17 | export function renderStyle(node: Descendant, vnode: VNode): VNode { 18 | if (!Element.isElement(node)) return vnode 19 | 20 | const { textAlign } = node as JustifyElement // 如 'left'/'right'/'center' 等 21 | let styleVnode: VNode = vnode 22 | 23 | if (textAlign) { 24 | addVnodeStyle(styleVnode, { textAlign }) 25 | } 26 | 27 | return styleVnode 28 | } 29 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/justify/style-to-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description textStyle to html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element, Descendant } from 'slate' 7 | import $, { getOuterHTML } from '../../utils/dom' 8 | import { JustifyElement } from './custom-types' 9 | 10 | export function styleToHtml(node: Descendant, elemHtml: string): string { 11 | if (!Element.isElement(node)) return elemHtml 12 | 13 | const { textAlign } = node as JustifyElement // 如 'left'/'right'/'center' 等 14 | if (!textAlign) return elemHtml 15 | 16 | // 设置样式 17 | const $elem = $(elemHtml) 18 | $elem.css('text-align', textAlign) 19 | 20 | // 输出 html 21 | const outerHtml = getOuterHTML($elem) 22 | return outerHtml 23 | } 24 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/line-height/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 自定义 element 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Text } from 'slate' 7 | 8 | //【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts 9 | 10 | export type LineHeightElement = { 11 | type: string 12 | lineHeight?: string 13 | children: Text[] 14 | } 15 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/line-height/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description line-height module entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import { renderStyle } from './render-style' 8 | import { styleToHtml } from './style-to-html' 9 | import { lineHeightMenuConf } from './menu/index' 10 | import { parseStyleHtml } from './parse-style-html' 11 | 12 | const lineHeight: Partial<IModuleConf> = { 13 | renderStyle, 14 | styleToHtml, 15 | parseStyleHtml, 16 | menus: [lineHeightMenuConf], 17 | } 18 | 19 | export default lineHeight 20 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/line-height/menu/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description line-height config 3 | * @author wangfupeng 4 | */ 5 | 6 | export function genLineHeightConfig() { 7 | return ['1', '1.15', '1.5', '2', '2.5', '3'] 8 | } 9 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/line-height/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description line-height menu entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import LineHeightMenu from './LineHeightMenu' 7 | import { genLineHeightConfig } from './config' 8 | 9 | export const lineHeightMenuConf = { 10 | key: 'lineHeight', 11 | factory() { 12 | return new LineHeightMenu() 13 | }, 14 | 15 | // 默认的菜单菜单配置,将存储在 editorConfig.MENU_CONF[key] 中 16 | // 创建编辑器时,可通过 editorConfig.MENU_CONF[key] = {...} 来修改 17 | config: { 18 | lineHeightList: genLineHeightConfig(), 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/line-height/parse-style-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse style html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Descendant, Element } from 'slate' 7 | import { IDomEditor } from '@wangeditor/core' 8 | import { LineHeightElement } from './custom-types' 9 | import $, { DOMElement, getStyleValue } from '../../utils/dom' 10 | 11 | export function parseStyleHtml(elem: DOMElement, node: Descendant, editor: IDomEditor): Descendant { 12 | const $elem = $(elem) 13 | if (!Element.isElement(node)) return node 14 | 15 | const elemNode = node as LineHeightElement 16 | 17 | const { lineHeightList = [] } = editor.getMenuConfig('lineHeight') 18 | const lineHeight = getStyleValue($elem, 'line-height') 19 | if (lineHeight && lineHeightList.includes(lineHeight)) { 20 | elemNode.lineHeight = lineHeight 21 | } 22 | 23 | return elemNode 24 | } 25 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/line-height/render-style.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description render line-height style 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element, Descendant } from 'slate' 7 | import { jsx, VNode } from 'snabbdom' 8 | import { addVnodeStyle } from '../../utils/vdom' 9 | import { LineHeightElement } from './custom-types' 10 | 11 | /** 12 | * 添加样式 13 | * @param node slate elem 14 | * @param vnode vnode 15 | * @returns vnode 16 | */ 17 | export function renderStyle(node: Descendant, vnode: VNode): VNode { 18 | if (!Element.isElement(node)) return vnode 19 | 20 | const { lineHeight } = node as LineHeightElement // 如 '1' '1.5' 21 | let styleVnode: VNode = vnode 22 | 23 | if (lineHeight) { 24 | addVnodeStyle(styleVnode, { lineHeight }) 25 | } 26 | 27 | return styleVnode 28 | } 29 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/line-height/style-to-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description textStyle to html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element, Descendant } from 'slate' 7 | import $, { getOuterHTML } from '../../utils/dom' 8 | import { LineHeightElement } from './custom-types' 9 | 10 | export function styleToHtml(node: Descendant, elemHtml: string): string { 11 | if (!Element.isElement(node)) return elemHtml 12 | 13 | const { lineHeight } = node as LineHeightElement // 如 '1' '1.5' 14 | if (!lineHeight) return elemHtml 15 | 16 | // 设置样式 17 | const $elem = $(elemHtml) 18 | $elem.css('line-height', lineHeight) 19 | 20 | // 输出 html 21 | return getOuterHTML($elem) 22 | } 23 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/link/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 自定义 element 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Text } from 'slate' 7 | 8 | //【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts 9 | 10 | export type LinkElement = { 11 | type: 'link' 12 | url: string 13 | target?: string 14 | children: Text[] 15 | } 16 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/link/elem-to-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description to html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element } from 'slate' 7 | import { LinkElement } from './custom-types' 8 | 9 | function linkToHtml(elem: Element, childrenHtml: string): string { 10 | const { url, target = '_blank' } = elem as LinkElement 11 | 12 | return `<a href="${url}" target="${target}">${childrenHtml}</a>` 13 | } 14 | 15 | export const linkToHtmlConf = { 16 | type: 'link', 17 | elemToHtml: linkToHtml, 18 | } 19 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/link/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description link entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import withLink from './plugin' 8 | import { renderLinkConf } from './render-elem' 9 | import { linkToHtmlConf } from './elem-to-html' 10 | import { parseHtmlConf } from './parse-elem-html' 11 | import { 12 | insertLinkMenuConf, 13 | editLinkMenuConf, 14 | unLinkMenuConf, 15 | viewLinkMenuConf, 16 | } from './menu/index' 17 | 18 | const link: Partial<IModuleConf> = { 19 | renderElems: [renderLinkConf], 20 | elemsToHtml: [linkToHtmlConf], 21 | parseElemsHtml: [parseHtmlConf], 22 | menus: [insertLinkMenuConf, editLinkMenuConf, unLinkMenuConf, viewLinkMenuConf], 23 | editorPlugin: withLink, 24 | } 25 | 26 | export default link 27 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/link/menu/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description link menu config 3 | * @author wangfupeng 4 | */ 5 | 6 | export function genLinkMenuConfig() { 7 | return { 8 | /** 9 | * 检查链接,支持 async fn 10 | * @param text link text 11 | * @param url link url 12 | */ 13 | checkLink(text: string, url: string): boolean | string | undefined { 14 | // 1. 返回 true ,说明检查通过 15 | // 2. 返回一个字符串,说明检查未通过,编辑器会阻止插入。会 alert 出错误信息(即返回的字符串) 16 | // 3. 返回 undefined(即没有任何返回),说明检查未通过,编辑器会阻止插入 17 | return true 18 | }, 19 | 20 | /** 21 | * parse link url 22 | * @param url url 23 | * @returns newUrl 24 | */ 25 | parseLinkUrl(url: string): string { 26 | return url 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/link/parse-elem-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Descendant, Text } from 'slate' 7 | import { IDomEditor } from '@wangeditor/core' 8 | import { LinkElement } from './custom-types' 9 | import $, { DOMElement } from '../../utils/dom' 10 | 11 | function parseHtml(elem: DOMElement, children: Descendant[], editor: IDomEditor): LinkElement { 12 | const $elem = $(elem) 13 | children = children.filter(child => { 14 | if (Text.isText(child)) return true 15 | if (editor.isInline(child)) return true 16 | return false 17 | }) 18 | 19 | // 无 children ,则用纯文本 20 | if (children.length === 0) { 21 | children = [{ text: $elem.text().replace(/\s+/gm, ' ') }] 22 | } 23 | 24 | return { 25 | type: 'link', 26 | url: $elem.attr('href') || '', 27 | target: $elem.attr('target') || '', 28 | // @ts-ignore 29 | children, 30 | } 31 | } 32 | 33 | export const parseHtmlConf = { 34 | selector: 'a:not([data-w-e-type])', // data-w-e-type 属性,留给自定义元素,保证扩展性 35 | parseElemHtml: parseHtml, 36 | } 37 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/link/render-elem.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description render link elem 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element as SlateElement } from 'slate' 7 | import { jsx, VNode } from 'snabbdom' 8 | import { IDomEditor } from '@wangeditor/core' 9 | import { LinkElement } from './custom-types' 10 | 11 | /** 12 | * render link elem 13 | * @param elemNode slate elem 14 | * @param children children 15 | * @param editor editor 16 | * @returns vnode 17 | */ 18 | function renderLink(elemNode: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode { 19 | const { url, target = '_blank' } = elemNode as LinkElement 20 | const vnode = ( 21 | <a href={url} target={target}> 22 | {children} 23 | </a> 24 | ) 25 | 26 | return vnode 27 | } 28 | 29 | const renderLinkConf = { 30 | type: 'link', // 和 elemNode.type 一致 31 | renderElem: renderLink, 32 | } 33 | 34 | export { renderLinkConf } 35 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/paragraph/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 自定义 element 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Text } from 'slate' 7 | 8 | //【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts 9 | 10 | export type ParagraphElement = { 11 | type: 'paragraph' 12 | children: Text[] 13 | } 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/paragraph/elem-to-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description to html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element } from 'slate' 7 | 8 | function pToHtml(elem: Element, childrenHtml: string): string { 9 | if (childrenHtml === '') { 10 | return '<p><br></p>' 11 | } 12 | return `<p>${childrenHtml}</p>` 13 | } 14 | 15 | export const pToHtmlConf = { 16 | type: 'paragraph', 17 | elemToHtml: pToHtml, 18 | } 19 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/paragraph/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description paragraph entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import { renderParagraphConf } from './render-elem' 8 | import { pToHtmlConf } from './elem-to-html' 9 | import { parseParagraphHtmlConf } from './parse-elem-html' 10 | import withParagraph from './plugin' 11 | 12 | const p: Partial<IModuleConf> = { 13 | renderElems: [renderParagraphConf], 14 | elemsToHtml: [pToHtmlConf], 15 | parseElemsHtml: [parseParagraphHtmlConf], 16 | editorPlugin: withParagraph, 17 | } 18 | 19 | export default p 20 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/paragraph/parse-elem-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Descendant, Text } from 'slate' 7 | import { IDomEditor } from '@wangeditor/core' 8 | import { ParagraphElement } from './custom-types' 9 | import $, { DOMElement } from '../../utils/dom' 10 | 11 | function parseParagraphHtml( 12 | elem: DOMElement, 13 | children: Descendant[], 14 | editor: IDomEditor 15 | ): ParagraphElement { 16 | const $elem = $(elem) 17 | 18 | children = children.filter(child => { 19 | if (Text.isText(child)) return true 20 | if (editor.isInline(child)) return true 21 | return false 22 | }) 23 | 24 | // 无 children ,则用纯文本 25 | if (children.length === 0) { 26 | children = [{ text: $elem.text().replace(/\s+/gm, ' ') }] 27 | } 28 | 29 | return { 30 | type: 'paragraph', 31 | // @ts-ignore 32 | children, 33 | } 34 | } 35 | 36 | export const parseParagraphHtmlConf = { 37 | selector: 'p:not([data-w-e-type])', // data-w-e-type 属性,留给自定义元素,保证扩展性 38 | parseElemHtml: parseParagraphHtml, 39 | } 40 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/paragraph/render-elem.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description render paragraph elem 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element as SlateElement } from 'slate' 7 | import { jsx, VNode } from 'snabbdom' 8 | import { IDomEditor } from '@wangeditor/core' 9 | 10 | /** 11 | * render paragraph elem 12 | * @param elemNode slate elem 13 | * @param children children 14 | * @param editor editor 15 | * @returns vnode 16 | */ 17 | function renderParagraph( 18 | elemNode: SlateElement, 19 | children: VNode[] | null, 20 | editor: IDomEditor 21 | ): VNode { 22 | const vnode = <p>{children}</p> 23 | return vnode 24 | } 25 | 26 | export const renderParagraphConf = { 27 | type: 'paragraph', 28 | renderElem: renderParagraph, 29 | } 30 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/text-style/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 自定义 element 3 | * @author wangfupeng 4 | */ 5 | 6 | //【注意】需要把自定义的 Text 引入到最外层的 custom-types.d.ts 7 | 8 | export type StyledText = { 9 | text: string 10 | bold?: boolean 11 | code?: boolean 12 | italic?: boolean 13 | through?: boolean 14 | underline?: boolean 15 | sup?: boolean 16 | sub?: boolean 17 | } 18 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/text-style/helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description helper 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Editor, Node } from 'slate' 7 | import { IDomEditor, DomEditor } from '@wangeditor/core' 8 | 9 | export function isMenuDisabled(editor: IDomEditor, mark?: string): boolean { 10 | if (editor.selection == null) return true 11 | 12 | const [match] = Editor.nodes(editor, { 13 | match: n => { 14 | const type = DomEditor.getNodeType(n) 15 | 16 | if (type === 'pre') return true // 代码块 17 | if (Editor.isVoid(editor, n)) return true // void node 18 | 19 | return false 20 | }, 21 | universal: true, 22 | }) 23 | 24 | // 命中,则禁用 25 | if (match) return true 26 | return false 27 | } 28 | 29 | export function removeMarks(editor: IDomEditor, textNode: Node) { 30 | // 遍历 text node 属性,清除样式 31 | const keys = Object.keys(textNode as object) 32 | keys.forEach(key => { 33 | if (key === 'text') { 34 | // 保留 text 属性,text node 必须的 35 | return 36 | } 37 | // 其他属性,全部清除 38 | Editor.removeMark(editor, key) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/text-style/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description text style entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import { renderStyle } from './render-style' 8 | import { styleToHtml } from './style-to-html' 9 | import { parseStyleHtml } from './parse-style-html' 10 | import { 11 | boldMenuConf, 12 | underlineMenuConf, 13 | italicMenuConf, 14 | throughMenuConf, 15 | codeMenuConf, 16 | subMenuConf, 17 | supMenuConf, 18 | clearStyleMenuConf, 19 | } from './menu/index' 20 | 21 | const textStyle: Partial<IModuleConf> = { 22 | renderStyle, 23 | menus: [ 24 | boldMenuConf, 25 | underlineMenuConf, 26 | italicMenuConf, 27 | throughMenuConf, 28 | codeMenuConf, 29 | subMenuConf, 30 | supMenuConf, 31 | clearStyleMenuConf, 32 | ], 33 | styleToHtml, 34 | parseStyleHtml, 35 | } 36 | 37 | export default textStyle 38 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/text-style/menu/BoldMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description bold menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { t } from '@wangeditor/core' 7 | import BaseMenu from './BaseMenu' 8 | import { BOLD_SVG } from '../../../constants/icon-svg' 9 | 10 | class BoldMenu extends BaseMenu { 11 | readonly mark = 'bold' 12 | readonly title = t('textStyle.bold') 13 | readonly iconSvg = BOLD_SVG 14 | readonly hotkey = 'mod+b' 15 | } 16 | 17 | export default BoldMenu 18 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/text-style/menu/CodeMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description code menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { t } from '@wangeditor/core' 7 | import BaseMenu from './BaseMenu' 8 | import { CODE_SVG } from '../../../constants/icon-svg' 9 | 10 | class CodeMenu extends BaseMenu { 11 | readonly mark = 'code' 12 | readonly title = t('textStyle.code') 13 | readonly iconSvg = CODE_SVG 14 | readonly hotkey = 'mod+e' 15 | } 16 | 17 | export default CodeMenu 18 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/text-style/menu/ItalicMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description italic menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { t } from '@wangeditor/core' 7 | import BaseMenu from './BaseMenu' 8 | import { ITALIC_SVG } from '../../../constants/icon-svg' 9 | 10 | class ItalicMenu extends BaseMenu { 11 | readonly mark = 'italic' 12 | readonly title = t('textStyle.italic') 13 | readonly iconSvg = ITALIC_SVG 14 | readonly hotkey = 'mod+i' 15 | } 16 | 17 | export default ItalicMenu 18 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/text-style/menu/SubMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description sub menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { t } from '@wangeditor/core' 7 | import BaseMenu from './BaseMenu' 8 | import { SUB_SVG } from '../../../constants/icon-svg' 9 | 10 | class SubMenu extends BaseMenu { 11 | readonly mark = 'sub' 12 | readonly marksNeedToRemove = ['sup'] // sub 和 sup 不能共存 13 | readonly title = t('textStyle.sub') 14 | readonly iconSvg = SUB_SVG 15 | readonly hotkey = '' 16 | } 17 | 18 | export default SubMenu 19 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/text-style/menu/SupMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description sup menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { t } from '@wangeditor/core' 7 | import BaseMenu from './BaseMenu' 8 | import { SUP_SVG } from '../../../constants/icon-svg' 9 | 10 | class SupMenu extends BaseMenu { 11 | readonly mark = 'sup' 12 | readonly marksNeedToRemove = ['sub'] // sup 和 sub 不能共存 13 | readonly title = t('textStyle.sup') 14 | readonly iconSvg = SUP_SVG 15 | readonly hotkey = '' 16 | } 17 | 18 | export default SupMenu 19 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/text-style/menu/ThroughMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description through menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { t } from '@wangeditor/core' 7 | import BaseMenu from './BaseMenu' 8 | import { THROUGH_SVG } from '../../../constants/icon-svg' 9 | 10 | class ThroughMenu extends BaseMenu { 11 | readonly mark = 'through' 12 | readonly title = t('textStyle.through') 13 | readonly iconSvg = THROUGH_SVG 14 | readonly hotkey = 'mod+shift+x' 15 | } 16 | 17 | export default ThroughMenu 18 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/text-style/menu/UnderlineMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description underline menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { t } from '@wangeditor/core' 7 | import BaseMenu from './BaseMenu' 8 | import { UNDER_LINE_SVG } from '../../../constants/icon-svg' 9 | 10 | class UnderlineMenu extends BaseMenu { 11 | readonly mark = 'underline' 12 | readonly title = t('textStyle.underline') 13 | readonly iconSvg = UNDER_LINE_SVG 14 | readonly hotkey = 'mod+u' 15 | } 16 | 17 | export default UnderlineMenu 18 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/todo/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 自定义 element 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Text } from 'slate' 7 | 8 | //【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts 9 | 10 | export type TodoElement = { 11 | type: 'todo' 12 | checked: boolean 13 | children: Text[] 14 | } 15 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/todo/elem-to-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description to html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element } from 'slate' 7 | import { TodoElement } from './custom-types' 8 | 9 | function todoToHtml(elem: Element, childrenHtml: string): string { 10 | const { checked } = elem as TodoElement 11 | const checkedAttr = checked ? 'checked' : '' 12 | return `<div data-w-e-type="todo"><input type="checkbox" disabled ${checkedAttr}>${childrenHtml}</div>` 13 | } 14 | 15 | export const todoToHtmlConf = { 16 | type: 'todo', 17 | elemToHtml: todoToHtml, 18 | } 19 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/todo/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description todo entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import { renderTodoConf } from './render-elem' 8 | import withTodo from './plugin' 9 | import { todoMenuConf } from './menu/index' 10 | import { todoToHtmlConf } from './elem-to-html' 11 | import { parseHtmlConf } from './parse-elem-html' 12 | import { preParseHtmlConf } from './pre-parse-html' 13 | 14 | const todo: Partial<IModuleConf> = { 15 | renderElems: [renderTodoConf], 16 | elemsToHtml: [todoToHtmlConf], 17 | preParseHtml: [preParseHtmlConf], 18 | parseElemsHtml: [parseHtmlConf], 19 | menus: [todoMenuConf], 20 | editorPlugin: withTodo, 21 | } 22 | 23 | export default todo 24 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/todo/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description todo menu entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import TodoMenu from './Todo' 7 | 8 | export const todoMenuConf = { 9 | key: 'todo', 10 | factory() { 11 | return new TodoMenu() 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/todo/plugin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description editor 插件,重写 editor API 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Node, Transforms, Range } from 'slate' 7 | import { DomEditor, IDomEditor } from '@wangeditor/core' 8 | 9 | function withTodo<T extends IDomEditor>(editor: T): T { 10 | const { deleteBackward } = editor 11 | const newEditor = editor 12 | 13 | /** 14 | * 删除 todo 无内容时,变为 paragraph 15 | */ 16 | newEditor.deleteBackward = unit => { 17 | const { selection } = editor 18 | 19 | if (selection && Range.isCollapsed(selection)) { 20 | // 获取选中的 todo 21 | const selectedTodo = DomEditor.getSelectedNodeByType(editor, 'todo') 22 | if (selectedTodo) { 23 | if (Node.string(selectedTodo).length === 0) { 24 | // 当前 todo 已经没有文字,则转换为 paragraph 25 | Transforms.setNodes(editor, { type: 'paragraph' }, { mode: 'highest' }) 26 | return 27 | } 28 | } 29 | } 30 | 31 | deleteBackward(unit) 32 | } 33 | 34 | return newEditor 35 | } 36 | 37 | export default withTodo 38 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/undo-redo/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description undo redo 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import { redoMenuConf, undoMenuConf } from './menu/index' 8 | 9 | const undoRedo: Partial<IModuleConf> = { 10 | menus: [redoMenuConf, undoMenuConf], 11 | } 12 | 13 | export default undoRedo 14 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/undo-redo/menu/RedoMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description redo menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IButtonMenu, IDomEditor, t } from '@wangeditor/core' 7 | import { REDO_SVG } from '../../../constants/icon-svg' 8 | 9 | class RedoMenu implements IButtonMenu { 10 | title = t('undo.redo') 11 | iconSvg = REDO_SVG 12 | tag = 'button' 13 | 14 | getValue(editor: IDomEditor): string | boolean { 15 | return '' 16 | } 17 | 18 | isActive(editor: IDomEditor): boolean { 19 | return false 20 | } 21 | 22 | isDisabled(editor: IDomEditor): boolean { 23 | if (editor.selection == null) return true 24 | return false 25 | } 26 | 27 | exec(editor: IDomEditor, value: string | boolean) { 28 | if (typeof editor.redo === 'function') { 29 | editor.redo() 30 | } 31 | } 32 | } 33 | 34 | export default RedoMenu 35 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/undo-redo/menu/UndoMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description undo menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IButtonMenu, IDomEditor, t } from '@wangeditor/core' 7 | import { UNDO_SVG } from '../../../constants/icon-svg' 8 | 9 | class UndoMenu implements IButtonMenu { 10 | title = t('undo.undo') 11 | iconSvg = UNDO_SVG 12 | tag = 'button' 13 | 14 | getValue(editor: IDomEditor): string | boolean { 15 | return '' 16 | } 17 | 18 | isActive(editor: IDomEditor): boolean { 19 | return false 20 | } 21 | 22 | isDisabled(editor: IDomEditor): boolean { 23 | if (editor.selection == null) return true 24 | return false 25 | } 26 | 27 | exec(editor: IDomEditor, value: string | boolean) { 28 | if (typeof editor.undo === 'function') { 29 | editor.undo() 30 | } 31 | } 32 | } 33 | 34 | export default UndoMenu 35 | -------------------------------------------------------------------------------- /packages/basic-modules/src/modules/undo-redo/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description menu entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import RedoMenu from './RedoMenu' 7 | import UndoMenu from './UndoMenu' 8 | 9 | export const undoMenuConf = { 10 | key: 'undo', 11 | factory() { 12 | return new UndoMenu() 13 | }, 14 | } 15 | 16 | export const redoMenuConf = { 17 | key: 'redo', 18 | factory() { 19 | return new RedoMenu() 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /packages/basic-modules/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, '&lt;').replace(/>/g, '&gt;') 19 | } 20 | -------------------------------------------------------------------------------- /packages/basic-modules/src/utils/vdom.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description vdom utils fn 3 | * @author wangfupeng 4 | */ 5 | 6 | import { VNode, VNodeStyle, Dataset } from 'snabbdom' 7 | 8 | // /** 9 | // * 给 vnode 添加 dataset 10 | // * @param vnode vnode 11 | // * @param newDataset { key: val } 12 | // */ 13 | // export function addVnodeDataset(vnode: VNode, newDataset: Dataset) { 14 | // if (vnode.data == null) vnode.data = {} 15 | // const data = vnode.data 16 | // if (data.dataset == null) data.dataset = {} 17 | 18 | // Object.assign(data.dataset, newDataset) 19 | // } 20 | 21 | /** 22 | * 给 vnode 添加样式 23 | * @param vnode vnode 24 | * @param newStyle { key: val } 25 | */ 26 | export function addVnodeStyle(vnode: VNode, newStyle: VNodeStyle) { 27 | if (vnode.data == null) vnode.data = {} 28 | const data = vnode.data 29 | if (data.style == null) data.style = {} 30 | 31 | Object.assign(data.style, newStyle) 32 | } 33 | -------------------------------------------------------------------------------- /packages/basic-modules/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": {}, 3 | "extends": "../../tsconfig.json", 4 | "include": [ 5 | "./src/**/*", 6 | "../custom-types.d.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/code-highlight/README.md: -------------------------------------------------------------------------------- 1 | # wangEditor code highlight 2 | 3 | Code highlight module built in [wangEditor](https://www.wangeditor.com/) by default. 4 | -------------------------------------------------------------------------------- /packages/code-highlight/__tests__/content.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description code content 3 | * @author wangfupeng 4 | */ 5 | 6 | export const text = 'const a = 100;' 7 | 8 | export const textNode = { text: text } 9 | 10 | export const language = 'javascript' 11 | 12 | export const codeNode = { 13 | type: 'code', 14 | language, 15 | children: [textNode], 16 | } 17 | 18 | export const preNode = { 19 | type: 'pre', 20 | children: [codeNode], 21 | } 22 | 23 | export const content = [{ type: 'paragraph', children: [{ text: 'hello world' }] }, preNode] 24 | 25 | export const textNodePath = [1, 0, 0] 26 | 27 | export const codeLocation = { 28 | anchor: { offset: text.length, path: textNodePath }, 29 | focus: { offset: text.length, path: textNodePath }, 30 | } 31 | 32 | export const paragraphLocation = { 33 | anchor: { offset: 0, path: [0, 0] }, 34 | focus: { offset: 0, path: [0, 0] }, 35 | } 36 | 37 | describe('加一个 case 防止报错~', () => { 38 | it('1 + 1 = 2', () => { 39 | expect(1 + 1).toBe(2) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /packages/code-highlight/__tests__/decorate.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description code-highlight decorate test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IDomEditor } from '@wangeditor/core' 7 | import createEditor from '../../../tests/utils/create-editor' 8 | import codeHighLightDecorate from '../src/decorate/index' 9 | import { content, textNode, textNodePath } from './content' 10 | 11 | describe('code-highlight decorate', () => { 12 | let editor: IDomEditor | null = null 13 | 14 | beforeAll(() => { 15 | // 把 content 创建到一个编辑器中 16 | editor = createEditor({ 17 | content, 18 | }) 19 | }) 20 | 21 | afterAll(() => { 22 | // 销毁 editor 23 | if (editor == null) return 24 | editor.destroy() 25 | editor = null 26 | }) 27 | 28 | it('code-highlight decorate 拆分代码字符串', () => { 29 | const ranges = codeHighLightDecorate([textNode, textNodePath]) 30 | expect(ranges.length).toBe(4) // 把 textNode 内容拆分为 4 段 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/code-highlight/__tests__/elem-to-html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description code-hight elem-to-html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IDomEditor } from '@wangeditor/core' 7 | import createEditor from '../../../tests/utils/create-editor' 8 | import { codeToHtmlConf } from '../src/module/elem-to-html' 9 | import { content, codeNode, language } from './content' 10 | 11 | describe('code-highlight elem to html', () => { 12 | let editor: IDomEditor | null = null 13 | 14 | beforeAll(() => { 15 | // 把 content 创建到一个编辑器中 16 | editor = createEditor({ 17 | content, 18 | }) 19 | }) 20 | 21 | afterAll(() => { 22 | // 销毁 editor 23 | if (editor == null) return 24 | editor.destroy() 25 | editor = null 26 | }) 27 | 28 | it('codeNode to html', () => { 29 | expect(codeToHtmlConf.type).toBe('code') 30 | 31 | if (editor == null) throw new Error('editor is null') 32 | const text = 'var n = 100;' 33 | const html = codeToHtmlConf.elemToHtml(codeNode, text) 34 | expect(html).toBe(`<code class="language-${language}">${text}</code>`) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /packages/code-highlight/__tests__/render-text-style.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description code-highlight render text style test 3 | * @author wangfupeng 4 | */ 5 | 6 | import { renderStyle } from '../src/module/render-style' 7 | import { jsx } from 'snabbdom' 8 | 9 | describe('code-highlight render text style', () => { 10 | it('code text style', () => { 11 | const leafNode = { text: 'let', keyword: true } // 定义一个 keyword leaf text node 12 | const vnode = <span>let</span> 13 | 14 | // @ts-ignore 忽略 vnode 格式检查 15 | const newVnode = renderStyle(leafNode, vnode) 16 | expect(newVnode.data?.props?.className).toBe('token keyword') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /packages/code-highlight/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config' 2 | import pkg from './package.json' 3 | 4 | const name = 'WangEditorCodeHighLight' 5 | 6 | const configList = [] 7 | 8 | // esm 9 | const esmConf = createRollupConfig({ 10 | output: { 11 | file: pkg.module, 12 | format: 'esm', 13 | name, 14 | }, 15 | }) 16 | configList.push(esmConf) 17 | 18 | // umd 19 | const umdConf = createRollupConfig({ 20 | output: { 21 | file: pkg.main, 22 | format: 'umd', 23 | name, 24 | }, 25 | }) 26 | configList.push(umdConf) 27 | 28 | export default configList 29 | -------------------------------------------------------------------------------- /packages/code-highlight/src/constants/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 | export const JS_SVG = 13 | '<svg viewBox="0 0 1024 1024"><path d="M64 64v896h896V64H64z m487.6 698.8c0 87.2-51.2 127-125.8 127-67.4 0-106.4-34.8-126.4-77l68.6-41.4c13.2 23.4 25.2 43.2 54.2 43.2 27.6 0 45.2-10.8 45.2-53V475.4h84.2v287.4z m199.2 127c-78.2 0-128.8-37.2-153.4-86l68.6-39.6c18 29.4 41.6 51.2 83 51.2 34.8 0 57.2-17.4 57.2-41.6 0-28.8-22.8-39-61.4-56l-21-9c-60.8-25.8-101-58.4-101-127 0-63.2 48.2-111.2 123.2-111.2 53.6 0 92 18.6 119.6 67.4L800 580c-14.4-25.8-30-36-54.2-36-24.6 0-40.2 15.6-40.2 36 0 25.2 15.6 35.4 51.8 51.2l21 9c71.6 30.6 111.8 62 111.8 132.4 0 75.6-59.6 117.2-139.4 117.2z"></path></svg>' 14 | -------------------------------------------------------------------------------- /packages/code-highlight/src/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 自定义 element 3 | * @author wangfupeng 4 | */ 5 | 6 | // 拷贝自 basic-modules/src/modules/code-block/custom-types.ts 7 | 8 | type PureText = { 9 | text: string 10 | } 11 | 12 | export type PreElement = { 13 | type: 'pre' 14 | children: CodeElement[] 15 | } 16 | 17 | export type CodeElement = { 18 | type: 'code' 19 | language: string 20 | children: PureText[] 21 | } 22 | -------------------------------------------------------------------------------- /packages/code-highlight/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description code-highlight 3 | * @author wangfupeng 4 | */ 5 | 6 | import './assets/index.less' 7 | 8 | // 配置多语言 9 | import './locale/index' 10 | 11 | import wangEditorCodeHighlightModule from './module/index' 12 | import wangEditorCodeHighLightDecorate from './decorate' 13 | 14 | export { wangEditorCodeHighlightModule, wangEditorCodeHighLightDecorate } 15 | -------------------------------------------------------------------------------- /packages/code-highlight/src/locale/en.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n en 3 | * @author wangfupeng 4 | */ 5 | 6 | export default { 7 | highLightModule: { 8 | selectLang: 'Language', 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /packages/code-highlight/src/locale/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { i18nAddResources } from '@wangeditor/core' 7 | import enResources from './en' 8 | import zhResources from './zh-CN' 9 | 10 | i18nAddResources('en', enResources) 11 | i18nAddResources('zh-CN', zhResources) 12 | -------------------------------------------------------------------------------- /packages/code-highlight/src/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n zh-CN 3 | * @author wangfupeng 4 | */ 5 | 6 | export default { 7 | highLightModule: { 8 | selectLang: '选择语言', 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /packages/code-highlight/src/module/elem-to-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description to html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element } from 'slate' 7 | import { CodeElement } from '../custom-types' 8 | 9 | function codeToHtml(elem: Element, childrenHtml: string): string { 10 | const { language = '' } = elem as CodeElement 11 | 12 | const cssClass = language 13 | ? `class="language-${language}"` // prism.js 根据 language 代码高亮 14 | : '' 15 | 16 | return `<code ${cssClass}>${childrenHtml}</code>` 17 | } 18 | 19 | // 覆盖 basic-module 中的 code to html 20 | export const codeToHtmlConf = { 21 | type: 'code', 22 | elemToHtml: codeToHtml, 23 | } 24 | -------------------------------------------------------------------------------- /packages/code-highlight/src/module/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description code highlight module 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import { renderStyle } from './render-style' 8 | import { parseCodeStyleHtml } from './parse-style-html' 9 | import { selectLangMenuConf } from './menu/index' 10 | import { codeToHtmlConf } from './elem-to-html' 11 | 12 | const codeHighlightModule: Partial<IModuleConf> = { 13 | renderStyle, 14 | parseStyleHtml: parseCodeStyleHtml, 15 | menus: [selectLangMenuConf], 16 | elemsToHtml: [codeToHtmlConf], 17 | } 18 | 19 | export default codeHighlightModule 20 | -------------------------------------------------------------------------------- /packages/code-highlight/src/module/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description code-highlight menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import SelectLangMenu from './SelectLangMenu' 7 | import { genCodeLangs } from './config' 8 | 9 | export const selectLangMenuConf = { 10 | key: 'codeSelectLang', 11 | factory() { 12 | return new SelectLangMenu() 13 | }, 14 | config: { 15 | codeLangs: genCodeLangs(), 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /packages/code-highlight/src/module/parse-style-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse style html 3 | * @author wangfupeng 4 | */ 5 | 6 | import $, { DOMElement } from '../utils/dom' 7 | import { Descendant, Element } from 'slate' 8 | import { DomEditor, IDomEditor } from '@wangeditor/core' 9 | import { CodeElement } from '../custom-types' 10 | 11 | export function parseCodeStyleHtml( 12 | elem: DOMElement, 13 | node: Descendant, 14 | editor: IDomEditor 15 | ): Descendant { 16 | const $elem = $(elem) 17 | 18 | if (!Element.isElement(node)) return node 19 | if (DomEditor.getNodeType(node) !== 'code') return node // 只针对 pre/code 元素 20 | 21 | const elemNode = node as CodeElement 22 | 23 | const langAttr = $elem.attr('class') || '' 24 | if (langAttr.indexOf('language-') === 0) { 25 | // V5 版本,格式如 class="language-javascript" 26 | elemNode.language = langAttr.split('-')[1] || '' // 获取 'javascript' 27 | } else { 28 | // 兼容 V4 版本,格式如 class="Javascript" 29 | elemNode.language = langAttr.toLowerCase() 30 | } 31 | 32 | return elemNode 33 | } 34 | -------------------------------------------------------------------------------- /packages/code-highlight/src/module/render-style.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description render code highlight style 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Text as SlateText, Descendant } from 'slate' 7 | import { jsx, VNode } from 'snabbdom' 8 | import { addVnodeClassName } from '../utils/vdom' 9 | import { prismTokenTypes } from '../vendor/prism' 10 | 11 | /** 12 | * 添加样式 13 | * @param node slate text 14 | * @param vnode vnode 15 | * @returns vnode 16 | */ 17 | export function renderStyle(node: Descendant, vnode: VNode): VNode { 18 | const leafNode = node as SlateText & { [key: string]: string } 19 | let styleVnode: VNode = vnode 20 | 21 | let className = '' 22 | prismTokenTypes.forEach(type => { 23 | if (leafNode[type]) className = type 24 | }) 25 | 26 | if (className) { 27 | className = `token ${className}` // 如 'token keyword' - prismjs 渲染的规则 28 | addVnodeClassName(styleVnode, className) 29 | } 30 | 31 | return styleVnode 32 | } 33 | -------------------------------------------------------------------------------- /packages/code-highlight/src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description DOM 操作 3 | * @author wangfupeng 4 | */ 5 | 6 | import $, { attr } from 'dom7' 7 | 8 | if (attr) $.fn.attr = attr 9 | 10 | export { Dom7Array } from 'dom7' 11 | 12 | export default $ 13 | 14 | // COMPAT: This is required to prevent TypeScript aliases from doing some very 15 | // weird things for Slate's types with the same name as globals. (2019/11/27) 16 | // https://github.com/microsoft/TypeScript/issues/35002 17 | import DOMNode = globalThis.Node 18 | import DOMComment = globalThis.Comment 19 | import DOMElement = globalThis.Element 20 | import DOMText = globalThis.Text 21 | import DOMRange = globalThis.Range 22 | import DOMSelection = globalThis.Selection 23 | import DOMStaticRange = globalThis.StaticRange 24 | export { DOMNode, DOMComment, DOMElement, DOMText, DOMRange, DOMSelection, DOMStaticRange } 25 | -------------------------------------------------------------------------------- /packages/code-highlight/src/utils/vdom.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description vdom utils fn 3 | * @author wangfupeng 4 | */ 5 | 6 | import { VNode, VNodeStyle } from 'snabbdom' 7 | 8 | /** 9 | * 给 vnode 添加 className 10 | * @param vnode vnode 11 | * @param className css class 12 | */ 13 | export function addVnodeClassName(vnode: VNode, className: string) { 14 | if (vnode.data == null) vnode.data = {} 15 | const data = vnode.data 16 | if (data.props == null) data.props = {} 17 | 18 | Object.assign(data.props, { className }) 19 | } 20 | 21 | /** 22 | * 给 vnode 添加样式 23 | * @param vnode vnode 24 | * @param newStyle { key: val } 25 | */ 26 | export function addVnodeStyle(vnode: VNode, newStyle: VNodeStyle) { 27 | if (vnode.data == null) vnode.data = {} 28 | const data = vnode.data 29 | if (data.style == null) data.style = {} 30 | 31 | Object.assign(data.style, newStyle) 32 | } 33 | -------------------------------------------------------------------------------- /packages/code-highlight/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": {}, 3 | "extends": "../../tsconfig.json", 4 | "include": [ 5 | "./src/**/*", 6 | "../custom-types.d.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # wangEditor core 2 | 3 | [wangEditor](https://www.wangeditor.com/) core. 4 | 5 | ## Main Functionalities 6 | - View( model -> vdom -> DOM ) + Selection 7 | - Menus + toolbar + hoverbar 8 | - Core editor APIs and events 9 | - Register third-party modules (menus, plugins...) 10 | 11 | ## Main dependencies 12 | - [slate.js](https://docs.slatejs.org/) 13 | - [snabbdom.js](https://github.com/snabbdom/snabbdom) 14 | -------------------------------------------------------------------------------- /packages/core/__tests__/config/menu-config.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description menu config test 3 | * @author wangfupeng 4 | */ 5 | 6 | import createCoreEditor from '../create-core-editor' // packages/core 不依赖 packages/editor ,不能使用后者的 createEditor 7 | import { registerGlobalMenuConf } from '../../src/config/register' 8 | 9 | describe('menu config', () => { 10 | it('set and get', () => { 11 | // 先注册一下菜单 key ,再设置配置(专为单元测试,用户使用时不涉及) 12 | registerGlobalMenuConf('bold', {}) 13 | 14 | const menuKey = 'bold' // 必须是一个存在的 menu key 15 | const menuConfig = { 16 | x: 100, 17 | } 18 | 19 | const editor = createCoreEditor({ 20 | config: { 21 | MENU_CONF: { 22 | [menuKey]: menuConfig, 23 | }, 24 | }, 25 | }) 26 | 27 | expect(editor.getMenuConfig(menuKey)).toEqual(menuConfig) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/core/__tests__/create-core-editor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description create editor - 用于 packages/core 单元测试 3 | * @author wangfupeng 4 | */ 5 | 6 | import createEditor from '../src/create/create-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 | -------------------------------------------------------------------------------- /packages/core/__tests__/i18n/index.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n test 3 | * @author wangfupeng 4 | */ 5 | 6 | import i18next, { i18nAddResources, i18nChangeLanguage, t } from '../../src/i18n' 7 | 8 | describe('i18n', () => { 9 | // 添加语言项 10 | i18nAddResources('en', { 11 | module1: { 12 | hello: 'hello', 13 | }, 14 | }) 15 | i18nAddResources('zh-CN', { 16 | module1: { 17 | hello: '你好', 18 | }, 19 | }) 20 | 21 | it('default lang', () => { 22 | expect(i18next.language).toBe('zh-CN') 23 | expect(t('module1.hello')).toBe('你好') 24 | }) 25 | 26 | it('change lang', () => { 27 | i18nChangeLanguage('en') 28 | expect(i18next.language).toBe('en') 29 | expect(t('module1.hello')).toBe('hello') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /packages/core/__tests__/menus/README.md: -------------------------------------------------------------------------------- 1 | # menus test 2 | 3 | TODO 各个 modules 中没有这块代码的测试,待编写... 4 | -------------------------------------------------------------------------------- /packages/core/__tests__/menus/register-menus/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 注册菜单,入口 3 | * @author wangfupeng 4 | */ 5 | 6 | import './register-button-menu' 7 | import './register-select-menu' 8 | import './register-modal-menu' 9 | -------------------------------------------------------------------------------- /packages/core/__tests__/menus/register-menus/register-button-menu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 注册菜单 - button menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { registerMenu, IButtonMenu } from '../../../src/menus/index' 7 | import { IDomEditor } from '../../../src/editor/interface' 8 | 9 | class MyButtonMenu implements IButtonMenu { 10 | readonly title = 'My Button Menu' 11 | readonly tag = 'button' 12 | getValue(editor: IDomEditor) { 13 | return '' 14 | } 15 | isActive(editor: IDomEditor) { 16 | return false 17 | } 18 | isDisabled(editor: IDomEditor) { 19 | return false 20 | } 21 | exec(editor: IDomEditor, value: string | boolean) { 22 | console.log('do..') 23 | } 24 | } 25 | 26 | registerMenu({ 27 | key: 'myButtonMenu', 28 | factory() { 29 | return new MyButtonMenu() 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /packages/core/__tests__/menus/register-menus/register-modal-menu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 注册菜单 - modal menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { registerMenu, IModalMenu } from '../../../src/menus/index' 7 | import { IDomEditor } from '../../../src/editor/interface' 8 | 9 | class MyModalMenu implements IModalMenu { 10 | readonly title = 'My Modal Menu' 11 | readonly tag = 'button' 12 | readonly showModal = true 13 | readonly modalWidth = 300 14 | getValue(editor: IDomEditor) { 15 | return '' 16 | } 17 | isActive(editor: IDomEditor) { 18 | return false 19 | } 20 | isDisabled(editor: IDomEditor) { 21 | return false 22 | } 23 | exec(editor: IDomEditor, value: string | boolean) { 24 | console.log('do..') 25 | } 26 | getModalContentElem(editor: IDomEditor) { 27 | return document.createElement('div') 28 | } 29 | getModalPositionNode(editor: IDomEditor) { 30 | return null 31 | } 32 | } 33 | 34 | registerMenu({ 35 | key: 'myModalMenu', 36 | factory() { 37 | return new MyModalMenu() 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /packages/core/__tests__/menus/register-menus/register-select-menu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 注册菜单 - select menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { registerMenu, ISelectMenu, IOption } from '../../../src/menus/index' 7 | import { IDomEditor } from '../../../src/editor/interface' 8 | 9 | class MySelectMenu implements ISelectMenu { 10 | readonly title = 'My Select Menu' 11 | readonly tag = 'select' 12 | getValue(editor: IDomEditor) { 13 | return '' 14 | } 15 | isActive(editor: IDomEditor) { 16 | return false 17 | } 18 | isDisabled(editor: IDomEditor) { 19 | return false 20 | } 21 | exec(editor: IDomEditor, value: string | boolean) { 22 | console.log('do..') 23 | } 24 | getOptions(): IOption[] { 25 | return [ 26 | { value: 'a', text: 'a' }, 27 | { value: 'b', text: 'b' }, 28 | ] 29 | } 30 | } 31 | 32 | registerMenu({ 33 | key: 'mySelectMenu', 34 | factory() { 35 | return new MySelectMenu() 36 | }, 37 | }) 38 | -------------------------------------------------------------------------------- /packages/core/__tests__/parse-html/README.md: -------------------------------------------------------------------------------- 1 | # parse-html test 2 | 3 | 各个 module `parseHtml` 已经测试了该模块的代码。 4 | -------------------------------------------------------------------------------- /packages/core/__tests__/render/README.md: -------------------------------------------------------------------------------- 1 | # render test 2 | 3 | 各个 module `renderElem` 已经测试了该模块的代码。 4 | -------------------------------------------------------------------------------- /packages/core/__tests__/to-html/README.md: -------------------------------------------------------------------------------- 1 | # to-html test 2 | 3 | 各个 module 中的 `editor.getHtml()` API 会测试到这部分代码。 4 | -------------------------------------------------------------------------------- /packages/core/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config' 2 | import pkg from './package.json' 3 | 4 | const name = 'WangEditorCore' 5 | 6 | const configList = [] 7 | 8 | // esm 9 | const esmConf = createRollupConfig({ 10 | output: { 11 | file: pkg.module, 12 | format: 'esm', 13 | name, 14 | }, 15 | }) 16 | configList.push(esmConf) 17 | 18 | // umd 19 | const umdConf = createRollupConfig({ 20 | output: { 21 | file: pkg.main, 22 | format: 'umd', 23 | name, 24 | }, 25 | }) 26 | configList.push(umdConf) 27 | 28 | export default configList 29 | -------------------------------------------------------------------------------- /packages/core/src/assets/bar.less: -------------------------------------------------------------------------------- 1 | @import "../../../vars.less"; // var and mixin 2 | 3 | .w-e-bar { 4 | background-color: @toolbar-bg-color; 5 | padding: 0 5px; 6 | font-size: @size; 7 | color: @toolbar-color; 8 | 9 | svg { 10 | width: @size; 11 | height: @size; 12 | fill: @toolbar-color; 13 | } 14 | } 15 | .w-e-bar-show { 16 | display: flex; 17 | } 18 | .w-e-bar-hidden { 19 | display: none; 20 | } 21 | 22 | .w-e-hover-bar { 23 | position: absolute; 24 | .shadowBordered(); 25 | } 26 | 27 | .w-e-toolbar { 28 | flex-wrap: wrap; 29 | position: relative; 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/assets/common.less: -------------------------------------------------------------------------------- 1 | .w-e-text-container *, 2 | .w-e-toolbar * { 3 | padding: 0; 4 | margin: 0; 5 | box-sizing: border-box; 6 | outline: none; 7 | } 8 | 9 | .w-e-text-container { 10 | p, li, td, th, blockquote { 11 | line-height: 1.5; 12 | } 13 | } 14 | 15 | .w-e-toolbar * { 16 | line-height: 1.5; 17 | } -------------------------------------------------------------------------------- /packages/core/src/assets/drop-panel.less: -------------------------------------------------------------------------------- 1 | @import "../../../vars.less"; // var and mixin 2 | 3 | .w-e-drop-panel { 4 | z-index: 1; 5 | background-color: @toolbar-bg-color; 6 | position: absolute; 7 | top: 0; 8 | .shadowBordered(10px); 9 | margin-top: @toolbar-height; 10 | min-width: 200px; 11 | padding: 10px; 12 | } 13 | 14 | // 当 bar 处于页面下方,则 dropPanel 要显示在 bar 上方 15 | .w-e-bar-bottom .w-e-drop-panel { 16 | top: inherit; 17 | bottom: 0; 18 | margin-top: 0; 19 | margin-bottom: @toolbar-height; 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/assets/full-screen.less: -------------------------------------------------------------------------------- 1 | .w-e-full-screen-container { 2 | position: fixed; 3 | margin: 0 !important; 4 | padding: 0 !important; 5 | top: 0 !important; 6 | left: 0 !important; 7 | right: 0 !important; 8 | bottom: 0 !important; 9 | height: 100% !important; 10 | width: 100% !important; 11 | display: flex !important; 12 | flex-direction: column !important; 13 | 14 | // [data-w-e-toolbar="true"] { 15 | // } 16 | 17 | [data-w-e-textarea="true"] { 18 | flex: 1 !important; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/assets/index.less: -------------------------------------------------------------------------------- 1 | @import "common.less"; 2 | @import "textarea.less"; 3 | @import "bar.less"; 4 | @import "bar-item.less"; 5 | @import "select-list.less"; 6 | @import "drop-panel.less"; 7 | @import "modal.less"; 8 | @import "progress.less"; 9 | @import "full-screen.less"; 10 | -------------------------------------------------------------------------------- /packages/core/src/assets/progress.less: -------------------------------------------------------------------------------- 1 | @import "../../../vars.less"; 2 | 3 | .w-e-progress-bar { 4 | position: absolute; 5 | width: 0; 6 | height: 1px; 7 | background-color: @textarea-handler-bg-color; 8 | transition: width 0.3s; 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/config/register.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description config register 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IMenuConfig, ISingleMenuConfig } from '../config/interface' 7 | 8 | // 全局的菜单配置 9 | export const GLOBAL_MENU_CONF: IMenuConfig = {} 10 | 11 | /** 12 | * 注册全局菜单配置 13 | * @param key menu key 14 | * @param config config 15 | */ 16 | export function registerGlobalMenuConf(key: string, config?: ISingleMenuConfig) { 17 | if (config == null) return 18 | GLOBAL_MENU_CONF[key] = config 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const IGNORE_TAGS = new Set([ 2 | 'doctype', 3 | '!doctype', 4 | 'meta', 5 | 'script', 6 | 'style', 7 | 'link', 8 | 'frame', 9 | 'iframe', 10 | 'title', 11 | 'svg', // TODO 暂时忽略 12 | ]) 13 | -------------------------------------------------------------------------------- /packages/core/src/create/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description create entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import coreCreateEditor from './create-editor' 7 | import coreCreateToolbar from './create-toolbar' 8 | 9 | export { coreCreateEditor, coreCreateToolbar } 10 | -------------------------------------------------------------------------------- /packages/core/src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import i18next from 'i18next' 7 | 8 | // i18n nameSpace 9 | const NS = 'translation' 10 | 11 | i18next.init({ 12 | lng: 'zh-CN', 13 | // debug: true, 14 | resources: {}, // 资源为空,随后添加 15 | }) 16 | 17 | /** 18 | * 添加多语言配置 19 | * @param lng 语言 20 | * @param resources 多语言配置 21 | */ 22 | export function i18nAddResources(lng: string, resources: object) { 23 | i18next.addResourceBundle(lng, NS, resources, true, true) 24 | } 25 | 26 | /** 27 | * 设置语言 28 | * @param lng 语言 29 | */ 30 | export function i18nChangeLanguage(lng: string) { 31 | i18next.changeLanguage(lng) 32 | } 33 | 34 | /** 35 | * 获取多语言配置 36 | * @param lng lang 37 | */ 38 | export function i18nGetResources(lng: string) { 39 | return i18next.getResourceBundle(lng, NS) 40 | } 41 | 42 | /** 43 | * 翻译 44 | */ 45 | export const t = i18next.t.bind(i18next) 46 | 47 | export default i18next 48 | -------------------------------------------------------------------------------- /packages/core/src/menus/README.md: -------------------------------------------------------------------------------- 1 | # menus 2 | 3 | 统一注册 menu ,menu 支持 4 | - classic toolbar 5 | - hovering toolbar 6 | - tooltip 7 | - contextMenu 8 | -------------------------------------------------------------------------------- /packages/core/src/menus/bar-item/SimpleButton.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description button class 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IButtonMenu } from '../interface' 7 | import BaseButton from './BaseButton' 8 | 9 | class SimpleButton extends BaseButton { 10 | constructor(key: string, menu: IButtonMenu, inGroup = false) { 11 | super(key, menu, inGroup) 12 | } 13 | onButtonClick() { 14 | // menu.exec 已经在 BaseButton 实现了 15 | // 所以,此处不用做任何逻辑 16 | } 17 | } 18 | 19 | export default SimpleButton 20 | -------------------------------------------------------------------------------- /packages/core/src/menus/bar-item/tooltip.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description tooltip 功能 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Dom7Array } from '../../utils/dom' 7 | import { IS_APPLE } from '../../utils/ua' 8 | 9 | export function addTooltip( 10 | $button: Dom7Array, 11 | iconSvg: string, 12 | title: string, 13 | hotkey: string, 14 | inGroup = false 15 | ) { 16 | if (!iconSvg) { 17 | // 没有 icon 直接显示 title ,不用 tooltip 18 | return 19 | } 20 | 21 | if (hotkey) { 22 | const fnKey = IS_APPLE ? 'cmd' : 'ctrl' // mac OS 转换为 cmd ,windows 转换为 ctrl 23 | hotkey = hotkey.replace('mod', fnKey) 24 | } 25 | 26 | if (inGroup) { 27 | // in groupButton ,tooltip 只显示 快捷键 28 | if (hotkey) { 29 | $button.attr('data-tooltip', hotkey) 30 | $button.addClass('w-e-menu-tooltip-v5') 31 | $button.addClass('tooltip-right') // tooltip 显示在右侧 32 | } 33 | } else { 34 | // 非 in groupButton ,正常实现 tooltip 35 | const tooltip = hotkey ? `${title}\n${hotkey}` : title 36 | $button.attr('data-tooltip', tooltip) 37 | $button.addClass('w-e-menu-tooltip-v5') 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/core/src/menus/helpers/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description menu helpers 3 | * @author wangfupeng 4 | */ 5 | 6 | import $, { Dom7Array } from '../../utils/dom' 7 | import { SVG_DOWN_ARROW } from '../../constants/svg' 8 | 9 | /** 10 | * 清理 svg 的样式 11 | * @param $elem svg elem 12 | */ 13 | export function clearSvgStyle($elem: Dom7Array) { 14 | $elem.removeAttr('width') 15 | $elem.removeAttr('height') 16 | $elem.removeAttr('fill') 17 | $elem.removeAttr('class') 18 | $elem.removeAttr('t') 19 | $elem.removeAttr('p-id') 20 | 21 | const children = $elem.children() 22 | if (children.length) { 23 | clearSvgStyle(children) 24 | } 25 | } 26 | 27 | /** 28 | * 向下箭头 icon svg 29 | */ 30 | export function gen$downArrow() { 31 | const $downArrow = $(SVG_DOWN_ARROW) 32 | return $downArrow 33 | } 34 | 35 | /** 36 | * bar item 分割线 37 | */ 38 | export function gen$barItemDivider() { 39 | return $('<div class="w-e-bar-divider"></div>') 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/src/menus/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description menus entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import Toolbar from './bar/Toolbar' 7 | 8 | // 注册 9 | export { registerMenu } from './register' 10 | 11 | // menu 相关接口 12 | export { 13 | IButtonMenu, 14 | ISelectMenu, 15 | IDropPanelMenu, 16 | IModalMenu, 17 | IRegisterMenuConf, 18 | IOption, 19 | } from './interface' 20 | 21 | // 输出 modal 相关方法 22 | export { 23 | genModalInputElems, 24 | genModalButtonElems, 25 | genModalTextareaElems, 26 | } from './panel-and-modal/Modal' 27 | 28 | export { Toolbar } 29 | -------------------------------------------------------------------------------- /packages/core/src/menus/panel-and-modal/DropPanel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description dropPanel class 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IDomEditor } from '../../editor/interface' 7 | import $, { Dom7Array } from '../../utils/dom' 8 | import PanelAndModal from './BaseClass' 9 | 10 | class DropPanel extends PanelAndModal { 11 | type = 'dropPanel' 12 | readonly $elem: Dom7Array = $(`<div class="w-e-drop-panel"></div>`) 13 | 14 | constructor(editor: IDomEditor) { 15 | super(editor) 16 | } 17 | 18 | genSelfElem(): Dom7Array | null { 19 | return null 20 | } 21 | } 22 | 23 | export default DropPanel 24 | -------------------------------------------------------------------------------- /packages/core/src/menus/register.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description register menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { MenuFactoryType, IRegisterMenuConf } from './interface' 7 | import { registerGlobalMenuConf } from '../config/register' 8 | 9 | // menu item 的工厂函数 - 集合 10 | export const MENU_ITEM_FACTORIES: { 11 | [key: string]: MenuFactoryType 12 | } = {} 13 | 14 | /** 15 | * 注册菜单配置 16 | * @param registerMenuConf { key, factory, config } ,各个 menu key 不能重复 17 | * @param customConfig 自定义 menu config 18 | */ 19 | export function registerMenu( 20 | registerMenuConf: IRegisterMenuConf, 21 | customConfig?: { [key: string]: any } 22 | ) { 23 | const { key, factory, config } = registerMenuConf 24 | 25 | // 合并 config 26 | const newConfig = { ...config, ...(customConfig || {}) } 27 | 28 | // 注册 menu 29 | if (MENU_ITEM_FACTORIES[key] != null) { 30 | throw new Error(`Duplicated key '${key}' in menu items`) 31 | } 32 | MENU_ITEM_FACTORIES[key] = factory 33 | 34 | // 将 config 保存到全局 35 | registerGlobalMenuConf(key, newConfig) 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/parse-html/README.md: -------------------------------------------------------------------------------- 1 | # parse html 2 | 3 | 把 html 转换为 JSON content 4 | -------------------------------------------------------------------------------- /packages/core/src/parse-html/helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description parse-html helper fns 3 | * @author wangfupeng 4 | */ 5 | 6 | const REPLACE_SPACE_160_REG = new RegExp(String.fromCharCode(160), 'g') 7 | 8 | /** 9 | * 把 charCode 160 的空格(`&nbsp` 转换的),替换为 charCode 32 的空格(JS 默认的) 10 | * @param str str 11 | * @returns str 12 | */ 13 | export function replaceSpace160(str: string): string { 14 | const res = str.replace(REPLACE_SPACE_160_REG, ' ') 15 | return res 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/render/README.md: -------------------------------------------------------------------------------- 1 | # render 2 | 3 | 把 JSON content 转换为 vdom 4 | -------------------------------------------------------------------------------- /packages/core/src/render/element/getRenderElem.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 获取 elem render 函数 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element as SlateElement } from 'slate' 7 | import { jsx, VNode } from 'snabbdom' 8 | import { IDomEditor } from '../../editor/interface' 9 | import { RENDER_ELEM_CONF, RenderElemFnType } from '../index' 10 | 11 | /** 12 | * 默认的 render elem 13 | * @param elemNode elem 14 | * @param editor editor 15 | * @param children children vnode 16 | * @returns vnode 17 | */ 18 | function defaultRender( 19 | elemNode: SlateElement, 20 | children: VNode[] | null, 21 | editor: IDomEditor 22 | ): VNode { 23 | const Tag = editor.isInline(elemNode) ? 'span' : 'div' 24 | 25 | const vnode = <Tag>{children}</Tag> 26 | 27 | return vnode 28 | } 29 | 30 | /** 31 | * 根据 elemNode.type 获取 renderElement 函数 32 | * @param type elemNode.type 33 | */ 34 | function getRenderElem(type: string): RenderElemFnType { 35 | const fn = RENDER_ELEM_CONF[type] 36 | return fn || defaultRender 37 | } 38 | 39 | export default getRenderElem 40 | -------------------------------------------------------------------------------- /packages/core/src/render/element/renderStyle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 添加文本相关的样式 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element as SlateElement } from 'slate' 7 | import { VNode } from 'snabbdom' 8 | import { RENDER_STYLE_HANDLER_LIST } from '../index' 9 | 10 | /** 11 | * 渲染样式 12 | * @param elem slate elem node 13 | * @param vnode elem Vnode 14 | */ 15 | function renderStyle(elem: SlateElement, vnode: VNode): VNode { 16 | let newVnode = vnode 17 | 18 | RENDER_STYLE_HANDLER_LIST.forEach(styleHandler => { 19 | newVnode = styleHandler(elem, vnode) 20 | }) 21 | 22 | return newVnode 23 | } 24 | 25 | export default renderStyle 26 | -------------------------------------------------------------------------------- /packages/core/src/render/helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description formats helper 3 | * @author wangfupeng 4 | */ 5 | 6 | export function genElemId(id: string) { 7 | return `w-e-element-${id}` 8 | } 9 | 10 | export function genTextId(id: string) { 11 | return `w-e-text-${id}` 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/render/text/renderStyle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description text 样式 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Text as SlateText } from 'slate' 7 | import { VNode } from 'snabbdom' 8 | import { RENDER_STYLE_HANDLER_LIST } from '../index' 9 | 10 | /** 11 | * 给字符串增加样式 12 | * @param leafNode slate text leaf node 13 | * @param textVnode textVnode 14 | */ 15 | function addTextVnodeStyle(leafNode: SlateText, textVnode: VNode): VNode { 16 | let newTextVnode = textVnode 17 | 18 | RENDER_STYLE_HANDLER_LIST.forEach(styleHandler => { 19 | newTextVnode = styleHandler(leafNode, newTextVnode) 20 | }) 21 | 22 | return newTextVnode 23 | } 24 | 25 | export default addTextVnodeStyle 26 | -------------------------------------------------------------------------------- /packages/core/src/text-area/event-handlers/copy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 处理 copy 事件 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IDomEditor } from '../../editor/interface' 7 | // import { DomEditor } from '../../editor/dom-editor' 8 | import TextArea from '../TextArea' 9 | import { hasEditableTarget } from '../helpers' 10 | 11 | function handleOnCopy(e: Event, textarea: TextArea, editor: IDomEditor) { 12 | const event = e as ClipboardEvent 13 | 14 | if (!hasEditableTarget(editor, event.target)) return 15 | event.preventDefault() 16 | 17 | const data = event.clipboardData 18 | if (data == null) return 19 | editor.setFragmentData(data) 20 | } 21 | 22 | export default handleOnCopy 23 | -------------------------------------------------------------------------------- /packages/core/src/text-area/event-handlers/focus.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 处理 onfocus 事件 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IDomEditor } from '../../editor/interface' 7 | import { DomEditor } from '../../editor/dom-editor' 8 | import TextArea from '../TextArea' 9 | import { IS_FIREFOX } from '../../utils/ua' 10 | import { IS_FOCUSED } from '../../utils/weak-maps' 11 | 12 | function handleOnFocus(event: Event, textarea: TextArea, editor: IDomEditor) { 13 | const el = DomEditor.toDOMNode(editor, editor) 14 | const root = DomEditor.findDocumentOrShadowRoot(editor) 15 | textarea.latestElement = root.activeElement 16 | 17 | // COMPAT: If the editor has nested editable elements, the focus 18 | // can go to them. In Firefox, this must be prevented because it 19 | // results in issues with keyboard navigation. (2017/03/30) 20 | if (IS_FIREFOX && event.target !== el) { 21 | el.focus() 22 | return 23 | } 24 | 25 | IS_FOCUSED.set(editor, true) 26 | } 27 | 28 | export default handleOnFocus 29 | -------------------------------------------------------------------------------- /packages/core/src/text-area/event-handlers/keypress.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 监听 keypress 事件 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Editor } from 'slate' 7 | import { IDomEditor } from '../../editor/interface' 8 | import TextArea from '../TextArea' 9 | import { HAS_BEFORE_INPUT_SUPPORT } from '../../utils/ua' 10 | import { hasEditableTarget } from '../helpers' 11 | 12 | // 【注意】虽然 keypress 事件已经过时(建议用 keydown 取代),但这里是为了兼容 beforeinput ,所以不会在高级浏览器生效,不用升级 keydown 13 | 14 | function handleKeypress(event: Event, textarea: TextArea, editor: IDomEditor) { 15 | // 这里是兼容不完全支持 beforeInput 的浏览器。对于支持 beforeInput 的浏览器,会用 beforeinput 事件处理 16 | if (HAS_BEFORE_INPUT_SUPPORT) return 17 | 18 | const { readOnly } = editor.getConfig() 19 | if (readOnly) return 20 | if (!hasEditableTarget(editor, event.target)) return 21 | 22 | event.preventDefault() 23 | 24 | const text = (event as any).key as string 25 | 26 | // 这里只兼容 beforeInput 的 insertText 类型,其他的(如删除、换行)使用 keydown 来兼容 27 | Editor.insertText(editor, text) 28 | } 29 | 30 | export default handleKeypress 31 | -------------------------------------------------------------------------------- /packages/core/src/to-html/README.md: -------------------------------------------------------------------------------- 1 | # to html 2 | 3 | 把 content 为 html 4 | -------------------------------------------------------------------------------- /packages/core/src/to-html/node2html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description node -> html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element, Descendant } from 'slate' 7 | import { IDomEditor } from '../editor/interface' 8 | import elemToHtml from './elem2html' 9 | import textToHtml from './text2html' 10 | 11 | function node2html(node: Descendant, editor: IDomEditor): string { 12 | if (Element.isElement(node)) { 13 | // elem node 14 | return elemToHtml(node, editor) 15 | } else { 16 | // text node 17 | return textToHtml(node, editor) 18 | } 19 | } 20 | 21 | export default node2html 22 | -------------------------------------------------------------------------------- /packages/core/src/upload/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description upload entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import createUploader from './createUploader' 7 | import { IUploadConfig } from './interface' 8 | 9 | export { createUploader, IUploadConfig } 10 | 11 | // TODO upload 能力,写到文档中,二次开发使用 12 | -------------------------------------------------------------------------------- /packages/core/src/upload/interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description upload interface 3 | * @author wangfupeng 4 | */ 5 | 6 | import { UppyFile } from '@uppy/core' 7 | 8 | type FilesType = { [key: string]: UppyFile<{}, {}> } 9 | 10 | /** 11 | * 配置参考 https://uppy.io/docs/uppy/ 12 | */ 13 | export interface IUploadConfig { 14 | server: string 15 | fieldName?: string 16 | maxFileSize?: number 17 | maxNumberOfFiles?: number 18 | meta?: Record<string, unknown> 19 | metaWithUrl: boolean 20 | headers?: 21 | | Headers 22 | | ((file: UppyFile<Record<string, unknown>, Record<string, unknown>>) => Headers) 23 | | undefined 24 | withCredentials?: boolean 25 | timeout?: number 26 | onBeforeUpload?: (files: FilesType) => boolean | FilesType 27 | onSuccess: (file: UppyFile<{}, {}>, response: any) => void 28 | onProgress?: (progress: number) => void 29 | onFailed: (file: UppyFile<{}, {}>, response: any) => void 30 | onError: (file: UppyFile<{}, {}>, error: any, res: any) => void 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/utils/key.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An auto-incrementing identifier for keys. 3 | */ 4 | 5 | let n = 0 6 | 7 | /** 8 | * A class that keeps track of a key string. We use a full class here because we 9 | * want to be able to use them as keys in `WeakMap` objects. 10 | */ 11 | export class Key { 12 | id: string 13 | 14 | constructor() { 15 | this.id = `${n++}` 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": {}, 3 | "extends": "../../tsconfig.json", 4 | "include": [ 5 | "./src/**/*", 6 | "../custom-types.d.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/editor/README-en.md: -------------------------------------------------------------------------------- 1 | # wangEditor editor 2 | 3 | [中文](./README.md) 4 | 5 | Open source web rich text editor, run right out of the box. Support JS Vue React. 6 | 7 | - [Document](https://www.wangeditor.com/en/) 8 | - [Demo](https://www.wangeditor.com/demo/?lang=en) 9 | 10 | ![](../../docs/images/editor-en.png) 11 | 12 | You can [commit an issue]((https://github.com/wangeditor-team/wangEditor/issues)) if you have any question. 13 | -------------------------------------------------------------------------------- /packages/editor/README.md: -------------------------------------------------------------------------------- 1 | # wangEditor editor 2 | 3 | [English](./README-en.md) 4 | 5 | 开源 Web 富文本编辑器,开箱即用,配置简单。支持 JS Vue React 。 6 | 7 | - [文档](https://www.wangeditor.com/) 8 | - [demo](https://www.wangeditor.com/demo/) 9 | 10 | ![](../../docs/images/editor.png) 11 | 12 | 交流 13 | - [提交问题和建议](https://github.com/wangeditor-team/wangEditor/issues) 14 | - 加入 QQ 群([官网](https://www.wangeditor.com/)有群号) 15 | -------------------------------------------------------------------------------- /packages/editor/demo/README.md: -------------------------------------------------------------------------------- 1 | # wangEditor demo 2 | 3 | 修改左侧目录,在 demo 目录搜索 `MENU_CONF` 4 | 5 | demo 部署参考 `deploy-demos.yml` 配置 6 | -------------------------------------------------------------------------------- /packages/editor/demo/css/layout.css: -------------------------------------------------------------------------------- 1 | /* body { 2 | margin: 20px; 3 | } */ 4 | 5 | .page-container { 6 | margin-top: 15px; 7 | display: flex; 8 | } 9 | 10 | .page-left { 11 | width: 150px; 12 | padding: 0 10px; 13 | } 14 | 15 | .page-right { 16 | padding: 0 10px; 17 | flex: 1; 18 | width: calc(100vw - 170px); 19 | } -------------------------------------------------------------------------------- /packages/editor/examples/README.md: -------------------------------------------------------------------------------- 1 | # examples 2 | 3 | - 本地测试 4 | - 提交 `master` 会发布到测试机 5 | -------------------------------------------------------------------------------- /packages/editor/examples/css/editor.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0 10px; 3 | } 4 | 5 | .editor-toolbar { 6 | border: 1px solid #ccc; 7 | } 8 | 9 | .editor-text-area { 10 | border: 1px solid #ccc; 11 | border-top: 0; 12 | height: 400px; 13 | } -------------------------------------------------------------------------------- /packages/editor/examples/css/view.css: -------------------------------------------------------------------------------- 1 | .editor-content-view { 2 | border: 1px solid #ccc; 3 | padding: 10px; 4 | margin-top: 30px; 5 | overflow-x: auto; 6 | } 7 | 8 | .editor-content-view p, 9 | .editor-content-view li { 10 | white-space: pre-wrap; /* 保留空格 */ 11 | } 12 | 13 | .editor-content-view blockquote { 14 | border-left: 8px solid #d0e5f2; 15 | padding: 10px 10px; 16 | margin: 10px 0; 17 | background-color: #f1f1f1; 18 | } 19 | 20 | .editor-content-view code { 21 | font-family: monospace; 22 | background-color: #eee; 23 | padding: 3px; 24 | border-radius: 3px; 25 | } 26 | .editor-content-view pre>code { 27 | display: block; 28 | padding: 10px; 29 | } 30 | 31 | .editor-content-view table { 32 | border-collapse: collapse; 33 | } 34 | .editor-content-view td, 35 | .editor-content-view th { 36 | border: 1px solid #ccc; 37 | min-width: 50px; 38 | height: 20px; 39 | } 40 | .editor-content-view th { 41 | background-color: #f1f1f1; 42 | } -------------------------------------------------------------------------------- /packages/editor/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangeditor-team/wangEditor/f35d8a73a0a3dc159134d483afc5963d0256ea18/packages/editor/favicon.ico -------------------------------------------------------------------------------- /packages/editor/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createRollupConfig, IS_PRD, IS_DEV } from '../../build/create-rollup-config' 2 | import pkg from './package.json' 3 | 4 | const name = 'wangEditor' 5 | 6 | const configList = [] 7 | 8 | // umd 9 | const umdConf = createRollupConfig({ 10 | output: { 11 | file: pkg.main, 12 | format: 'umd', 13 | name, 14 | }, 15 | }) 16 | configList.push(umdConf) 17 | 18 | // esm 19 | const esmConf = createRollupConfig({ 20 | output: { 21 | file: pkg.module, 22 | format: 'esm', 23 | name, 24 | }, 25 | }) 26 | configList.push(esmConf) 27 | 28 | export default configList 29 | -------------------------------------------------------------------------------- /packages/editor/src/assets/index.less: -------------------------------------------------------------------------------- 1 | // 集中定义 css vars ,否则会被重复定义多次 2 | :root, :host { 3 | // textarea - css vars 4 | --w-e-textarea-bg-color: #fff; 5 | --w-e-textarea-color: #333; 6 | --w-e-textarea-border-color: #ccc; 7 | --w-e-textarea-slight-border-color: #e8e8e8; 8 | --w-e-textarea-slight-color: #d4d4d4; 9 | --w-e-textarea-slight-bg-color: #f5f2f0; 10 | --w-e-textarea-selected-border-color: #B4D5FF; // 选中的元素,如选中了分割线 11 | --w-e-textarea-handler-bg-color: #4290f7; // 工具,如图片拖拽按钮 12 | 13 | // toolbar - css vars 14 | --w-e-toolbar-color: #595959; 15 | --w-e-toolbar-bg-color: #fff; 16 | --w-e-toolbar-active-color: #333; 17 | --w-e-toolbar-active-bg-color: #f1f1f1; 18 | --w-e-toolbar-disabled-color: #999; 19 | --w-e-toolbar-border-color: #e8e8e8; 20 | 21 | // modal - css vars 22 | --w-e-modal-button-bg-color: #fafafa; 23 | --w-e-modal-button-border-color: #d9d9d9; 24 | } 25 | -------------------------------------------------------------------------------- /packages/editor/src/init-default-config/config/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 获取编辑器默认配置 3 | * @author wangfupeng 4 | */ 5 | 6 | import { genDefaultToolbarKeys, genSimpleToolbarKeys } from './toolbar' 7 | import { genDefaultHoverbarKeys, genSimpleHoverbarKeys } from './hoverbar' 8 | 9 | export function getDefaultEditorConfig() { 10 | return { 11 | hoverbarKeys: genDefaultHoverbarKeys(), 12 | } 13 | } 14 | 15 | export function getSimpleEditorConfig() { 16 | return { 17 | hoverbarKeys: genSimpleHoverbarKeys(), 18 | } 19 | } 20 | 21 | export function getDefaultToolbarConfig() { 22 | return { 23 | toolbarKeys: genDefaultToolbarKeys(), 24 | } 25 | } 26 | 27 | export function getSimpleToolbarConfig() { 28 | return { 29 | toolbarKeys: genSimpleToolbarKeys(), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/editor/src/init-default-config/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description set default config 3 | * @author wangfupeng 4 | */ 5 | 6 | import Boot from '../Boot' 7 | import { 8 | getDefaultEditorConfig, 9 | getDefaultToolbarConfig, 10 | getSimpleEditorConfig, 11 | getSimpleToolbarConfig, 12 | } from './config' 13 | 14 | import { wangEditorCodeHighLightDecorate } from '@wangeditor/code-highlight' 15 | 16 | const defaultEditorConfig = getDefaultEditorConfig() 17 | Boot.setEditorConfig({ 18 | ...defaultEditorConfig, 19 | decorate: wangEditorCodeHighLightDecorate, // 代码高亮 20 | }) 21 | 22 | const simpleEditorConfig = getSimpleEditorConfig() 23 | Boot.setSimpleEditorConfig({ 24 | ...simpleEditorConfig, 25 | decorate: wangEditorCodeHighLightDecorate, // 代码高亮 26 | }) 27 | 28 | const defaultToolbarConfig = getDefaultToolbarConfig() 29 | Boot.setToolbarConfig(defaultToolbarConfig) 30 | 31 | const simpleToolbarConfig = getSimpleToolbarConfig() 32 | Boot.setSimpleToolbarConfig(simpleToolbarConfig) 33 | -------------------------------------------------------------------------------- /packages/editor/src/locale/en.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n en 3 | * @author wangfupeng 4 | */ 5 | 6 | export default { 7 | editor: { 8 | more: 'More', 9 | justify: 'Justify', 10 | indent: 'Indent', 11 | image: 'Image', 12 | video: 'Video', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /packages/editor/src/locale/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { i18nAddResources } from '@wangeditor/core' 7 | import enResources from './en' 8 | import zhResources from './zh-CN' 9 | 10 | i18nAddResources('en', enResources) 11 | i18nAddResources('zh-CN', zhResources) 12 | -------------------------------------------------------------------------------- /packages/editor/src/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n zh-CN 3 | * @author wangfupeng 4 | */ 5 | 6 | export default { 7 | editor: { 8 | more: '更多', 9 | justify: '对齐', 10 | indent: '缩进', 11 | image: '图片', 12 | video: '视频', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /packages/editor/src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description dom utils 3 | * @author wangfupeng 4 | */ 5 | 6 | import DOMElement = globalThis.Element 7 | 8 | export { DOMElement } 9 | -------------------------------------------------------------------------------- /packages/editor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./src/**/*", 5 | "../custom-types.d.ts" 6 | ] 7 | } -------------------------------------------------------------------------------- /packages/list-module/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.0.5](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/list-module@1.0.4...@wangeditor/list-module@1.0.5) (2022-09-27) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * list-item - 遇到 style 是 toHtml 出错 ([9854308](https://github.com/wangeditor-team/wangEditor/commit/98543083a1cb09207aceb2a4d8f3c1ce020b106d)) 12 | 13 | 14 | 15 | 16 | 17 | ## [1.0.4](https://github.com/wangeditor-team/wangEditor/compare/@wangeditor/list-module@1.0.3...@wangeditor/list-module@1.0.4) (2022-09-27) 18 | 19 | **Note:** Version bump only for package @wangeditor/list-module 20 | -------------------------------------------------------------------------------- /packages/list-module/README.md: -------------------------------------------------------------------------------- 1 | # wangEditor list-module 2 | 3 | List module built in [wangEditor](https://www.wangeditor.com/) by default. 4 | -------------------------------------------------------------------------------- /packages/list-module/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config' 2 | import pkg from './package.json' 3 | 4 | const name = 'WangEditorListModule' 5 | 6 | const configList = [] 7 | 8 | // esm - 开发环境不需要 CDN 引入,只需要 npm 引入,所以优先输出 esm 9 | const esmConf = createRollupConfig({ 10 | output: { 11 | file: pkg.module, 12 | format: 'esm', 13 | name, 14 | }, 15 | }) 16 | configList.push(esmConf) 17 | 18 | // umd 19 | const umdConf = createRollupConfig({ 20 | output: { 21 | file: pkg.main, 22 | format: 'umd', 23 | name, 24 | }, 25 | }) 26 | configList.push(umdConf) 27 | 28 | export default configList 29 | -------------------------------------------------------------------------------- /packages/list-module/src/assets/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangeditor-team/wangEditor/f35d8a73a0a3dc159134d483afc5963d0256ea18/packages/list-module/src/assets/index.less -------------------------------------------------------------------------------- /packages/list-module/src/constants/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 BULLETED_LIST_SVG = 14 | '<svg viewBox="0 0 1024 1024"><path d="M384 64h640v128H384V64z m0 384h640v128H384v-128z m0 384h640v128H384v-128zM0 128a128 128 0 1 1 256 0 128 128 0 0 1-256 0z m0 384a128 128 0 1 1 256 0 128 128 0 0 1-256 0z m0 384a128 128 0 1 1 256 0 128 128 0 0 1-256 0z"></path></svg>' 15 | 16 | // 有序列表 17 | export const NUMBERED_LIST_SVG = 18 | '<svg viewBox="0 0 1024 1024"><path d="M384 832h640v128H384z m0-384h640v128H384z m0-384h640v128H384zM192 0v256H128V64H64V0zM128 526.016v50.016h128v64H64v-146.016l128-60V384H64v-64h192v146.016zM256 704v320H64v-64h128v-64H64v-64h128v-64H64v-64z"></path></svg>' 19 | -------------------------------------------------------------------------------- /packages/list-module/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description list module 3 | * @author wangfupeng 4 | */ 5 | 6 | import './assets/index.less' 7 | 8 | // 配置多语言 9 | import './locale/index' 10 | 11 | // 导出 module 12 | import wangEditorListModule from './module/index' 13 | export default wangEditorListModule 14 | -------------------------------------------------------------------------------- /packages/list-module/src/locale/en.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n en 3 | * @author wangfupeng 4 | */ 5 | 6 | export default { 7 | listModule: { 8 | unOrderedList: 'Unordered list', 9 | orderedList: 'Ordered list', 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /packages/list-module/src/locale/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { i18nAddResources } from '@wangeditor/core' 7 | import enResources from './en' 8 | import zhResources from './zh-CN' 9 | 10 | i18nAddResources('en', enResources) 11 | i18nAddResources('zh-CN', zhResources) 12 | -------------------------------------------------------------------------------- /packages/list-module/src/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n zh-CN 3 | * @author wangfupeng 4 | */ 5 | 6 | export default { 7 | listModule: { 8 | unOrderedList: '无序列表', 9 | orderedList: '有序列表', 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /packages/list-module/src/module/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description list element 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Text } from 'slate' 7 | 8 | //【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts 9 | 10 | export type ListItemElement = { 11 | type: 'list-item' 12 | ordered: boolean // 有序/无序 13 | level: number // 层级:0 1 2 ... 14 | children: Text[] 15 | } 16 | -------------------------------------------------------------------------------- /packages/list-module/src/module/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description list module entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import renderListItemConf from './render-elem' 8 | import withList from './plugin' 9 | import { bulletedListMenuConf, numberedListMenuConf } from './menu/index' 10 | import listItemToHtmlConf from './elem-to-html' 11 | import { parseItemHtmlConf, parseListHtmlConf } from './parse-elem-html' 12 | 13 | const list: Partial<IModuleConf> = { 14 | renderElems: [renderListItemConf], 15 | editorPlugin: withList, 16 | menus: [bulletedListMenuConf, numberedListMenuConf], 17 | elemsToHtml: [listItemToHtmlConf], 18 | parseElemsHtml: [parseListHtmlConf, parseItemHtmlConf], 19 | } 20 | 21 | export default list 22 | -------------------------------------------------------------------------------- /packages/list-module/src/module/menu/BulletedListMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description bulleted list menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { t } from '@wangeditor/core' 7 | import BaseMenu from './BaseMenu' 8 | import { BULLETED_LIST_SVG } from '../../constants/svg' 9 | 10 | class BulletedListMenu extends BaseMenu { 11 | readonly ordered = false 12 | readonly title = t('listModule.unOrderedList') 13 | readonly iconSvg = BULLETED_LIST_SVG 14 | } 15 | 16 | export default BulletedListMenu 17 | -------------------------------------------------------------------------------- /packages/list-module/src/module/menu/NumberedListMenu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description numbered list menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import { t } from '@wangeditor/core' 7 | import BaseMenu from './BaseMenu' 8 | import { NUMBERED_LIST_SVG } from '../../constants/svg' 9 | 10 | class NumberedListMenu extends BaseMenu { 11 | readonly ordered = true 12 | readonly title = t('listModule.orderedList') 13 | readonly iconSvg = NUMBERED_LIST_SVG 14 | } 15 | 16 | export default NumberedListMenu 17 | -------------------------------------------------------------------------------- /packages/list-module/src/module/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description menu entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import BulletedListMenu from './BulletedListMenu' 7 | import NumberedListMenu from './NumberedListMenu' 8 | 9 | export const bulletedListMenuConf = { 10 | key: 'bulletedList', 11 | factory() { 12 | return new BulletedListMenu() 13 | }, 14 | } 15 | 16 | export const numberedListMenuConf = { 17 | key: 'numberedList', 18 | factory() { 19 | return new NumberedListMenu() 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /packages/list-module/src/utils/maps.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description maps 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element as SlateElement } from 'slate' 7 | import { IDomEditor } from '@wangeditor/core' 8 | 9 | export const ELEM_TO_EDITOR = new WeakMap<SlateElement, IDomEditor>() 10 | -------------------------------------------------------------------------------- /packages/list-module/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": {}, 4 | "include": [ 5 | "./src/**/*", 6 | "../custom-types.d.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/table-module/README.md: -------------------------------------------------------------------------------- 1 | # wangEditor table-module 2 | 3 | Table module built in [wangEditor](https://www.wangeditor.com/) by default. 4 | -------------------------------------------------------------------------------- /packages/table-module/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config' 2 | import pkg from './package.json' 3 | 4 | const name = 'WangEditorTableModule' 5 | 6 | const configList = [] 7 | 8 | // esm 9 | const esmConf = createRollupConfig({ 10 | output: { 11 | file: pkg.module, 12 | format: 'esm', 13 | name, 14 | }, 15 | }) 16 | configList.push(esmConf) 17 | 18 | // umd 19 | const umdConf = createRollupConfig({ 20 | output: { 21 | file: pkg.main, 22 | format: 'umd', 23 | name, 24 | }, 25 | }) 26 | configList.push(umdConf) 27 | 28 | export default configList 29 | -------------------------------------------------------------------------------- /packages/table-module/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description table entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import './assets/index.less' 7 | 8 | // 配置多语言 9 | import './locale/index' 10 | 11 | import wangEditorTableModule from './module/index' 12 | export default wangEditorTableModule 13 | -------------------------------------------------------------------------------- /packages/table-module/src/locale/en.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n en 3 | * @author wangfupeng 4 | */ 5 | 6 | export default { 7 | tableModule: { 8 | deleteCol: 'Delete column', 9 | deleteRow: 'Delete row', 10 | deleteTable: 'Delete table', 11 | widthAuto: 'Width auto', 12 | insertCol: 'Insert column', 13 | insertRow: 'Insert row', 14 | insertTable: 'Insert table', 15 | header: 'Header', 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /packages/table-module/src/locale/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { i18nAddResources } from '@wangeditor/core' 7 | import enResources from './en' 8 | import zhResources from './zh-CN' 9 | 10 | i18nAddResources('en', enResources) 11 | i18nAddResources('zh-CN', zhResources) 12 | -------------------------------------------------------------------------------- /packages/table-module/src/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n zh-CN 3 | * @author wangfupeng 4 | */ 5 | 6 | export default { 7 | tableModule: { 8 | deleteCol: '删除列', 9 | deleteRow: '删除行', 10 | deleteTable: '删除表格', 11 | widthAuto: '宽度自适应', 12 | insertCol: '插入列', 13 | insertRow: '插入行', 14 | insertTable: '插入表格', 15 | header: '表头', 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /packages/table-module/src/module/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 自定义 element 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Text } from 'slate' 7 | 8 | //【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts 9 | 10 | export type TableCellElement = { 11 | type: 'table-cell' 12 | isHeader?: boolean // td/th 只作用于第一行 13 | colSpan?: number 14 | rowSpan?: number 15 | width?: string // 只作用于第一行(尚未考虑单元格合并!) 16 | children: Text[] 17 | } 18 | 19 | export type TableRowElement = { 20 | type: 'table-row' 21 | children: TableCellElement[] 22 | } 23 | 24 | export type TableElement = { 25 | type: 'table' 26 | width: string 27 | children: TableRowElement[] 28 | } 29 | -------------------------------------------------------------------------------- /packages/table-module/src/module/pre-parse-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description pre parse html 3 | * @author wangfupeng 4 | */ 5 | 6 | import $, { getTagName, DOMElement } from '../utils/dom' 7 | 8 | /** 9 | * pre-prase table ,去掉 <tbody> 10 | * @param table table elem 11 | */ 12 | function preParse(tableElem: DOMElement): DOMElement { 13 | const $table = $(tableElem) 14 | const tagName = getTagName($table) 15 | if (tagName !== 'table') return tableElem 16 | 17 | // 没有 <tbody> 则直接返回 18 | const $tbody = $table.find('tbody') 19 | if ($tbody.length === 0) return tableElem 20 | 21 | // 去掉 <tbody> ,把 <tr> 移动到 <table> 下面 22 | const $tr = $table.find('tr') 23 | $table.append($tr) 24 | $tbody.remove() 25 | 26 | return $table[0] 27 | } 28 | 29 | export const preParseTableHtmlConf = { 30 | selector: 'table', 31 | preParseHtml: preParse, 32 | } 33 | -------------------------------------------------------------------------------- /packages/table-module/src/module/render-elem/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description render elem 3 | * @author wangfupeng 4 | */ 5 | 6 | import renderTable from './render-table' 7 | import renderTableRow from './render-row' 8 | import renderTableCell from './render-cell' 9 | 10 | export const renderTableConf = { 11 | type: 'table', 12 | renderElem: renderTable, 13 | } 14 | 15 | export const renderTableRowConf = { 16 | type: 'table-row', 17 | renderElem: renderTableRow, 18 | } 19 | 20 | export const renderTableCellConf = { 21 | type: 'table-cell', 22 | renderElem: renderTableCell, 23 | } 24 | -------------------------------------------------------------------------------- /packages/table-module/src/module/render-elem/render-row.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description render row 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element as SlateElement } from 'slate' 7 | import { jsx, VNode } from 'snabbdom' 8 | import { IDomEditor } from '@wangeditor/core' 9 | 10 | function renderTableRow( 11 | elemNode: SlateElement, 12 | children: VNode[] | null, 13 | editor: IDomEditor 14 | ): VNode { 15 | const vnode = <tr>{children}</tr> 16 | return vnode 17 | } 18 | 19 | export default renderTableRow 20 | -------------------------------------------------------------------------------- /packages/table-module/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 | -------------------------------------------------------------------------------- /packages/table-module/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": {}, 3 | "extends": "../../tsconfig.json", 4 | "include": [ 5 | "./src/**/*", 6 | "../custom-types.d.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/upload-image-module/README.md: -------------------------------------------------------------------------------- 1 | # wangEditor upload-image-module 2 | 3 | Upload image module built in [wangEditor](https://www.wangeditor.com/) by default. 4 | -------------------------------------------------------------------------------- /packages/upload-image-module/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config' 2 | import pkg from './package.json' 3 | 4 | const name = 'WangEditorUploadImageModule' 5 | 6 | const configList = [] 7 | 8 | // esm 9 | const esmConf = createRollupConfig({ 10 | output: { 11 | file: pkg.module, 12 | format: 'esm', 13 | name, 14 | }, 15 | }) 16 | configList.push(esmConf) 17 | 18 | // umd 19 | const umdConf = createRollupConfig({ 20 | output: { 21 | file: pkg.main, 22 | format: 'umd', 23 | name, 24 | }, 25 | }) 26 | configList.push(umdConf) 27 | 28 | export default configList 29 | -------------------------------------------------------------------------------- /packages/upload-image-module/src/assets/index.less: -------------------------------------------------------------------------------- 1 | // styles 2 | -------------------------------------------------------------------------------- /packages/upload-image-module/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description upload image 3 | * @author wangfupeng 4 | */ 5 | 6 | import './assets/index.less' 7 | 8 | // 配置多语言 9 | import './locale/index' 10 | 11 | import wangEditorUploadImageModule from './module/index' 12 | export default wangEditorUploadImageModule 13 | -------------------------------------------------------------------------------- /packages/upload-image-module/src/locale/en.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n en 3 | * @author wangfupeng 4 | */ 5 | 6 | export default { 7 | uploadImgModule: { 8 | uploadImage: 'Upload Image', 9 | uploadError: '{{fileName}} upload error', 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /packages/upload-image-module/src/locale/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { i18nAddResources } from '@wangeditor/core' 7 | import enResources from './en' 8 | import zhResources from './zh-CN' 9 | 10 | i18nAddResources('en', enResources) 11 | i18nAddResources('zh-CN', zhResources) 12 | -------------------------------------------------------------------------------- /packages/upload-image-module/src/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n zh-CN 3 | * @author wangfupeng 4 | */ 5 | 6 | export default { 7 | uploadImgModule: { 8 | uploadImage: '上传图片', 9 | uploadError: '{{fileName}} 上传出错', 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /packages/upload-image-module/src/module/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description uploadImage module 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import withUploadImage from './plugin' 8 | import { uploadImageMenuConf } from './menu/index' 9 | 10 | const uploadImage: Partial<IModuleConf> = { 11 | menus: [uploadImageMenuConf], 12 | editorPlugin: withUploadImage, 13 | } 14 | 15 | export default uploadImage 16 | -------------------------------------------------------------------------------- /packages/upload-image-module/src/module/menu/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description upload image menu 3 | * @author wangfupeng 4 | */ 5 | 6 | import UploadImageMenu from './UploadImageMenu' 7 | import { genUploadImageConfig } from './config' 8 | 9 | export const uploadImageMenuConf = { 10 | key: 'uploadImage', 11 | factory() { 12 | return new UploadImageMenu() 13 | }, 14 | 15 | // 默认的菜单菜单配置,将存储在 editorConfig.MENU_CONF[key] 中 16 | // 创建编辑器时,可通过 editorConfig.MENU_CONF[key] = {...} 来修改 17 | config: genUploadImageConfig(), 18 | } 19 | -------------------------------------------------------------------------------- /packages/upload-image-module/src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description DOM 操作 3 | * @author wangfupeng 4 | */ 5 | 6 | import $, { append, on, remove, val, click, hide } from 'dom7' 7 | export { Dom7Array } from 'dom7' 8 | 9 | if (append) $.fn.append = append 10 | if (on) $.fn.on = on 11 | if (remove) $.fn.remove = remove 12 | if (val) $.fn.val = val 13 | if (click) $.fn.click = click 14 | if (hide) $.fn.hide = hide 15 | 16 | export default $ 17 | -------------------------------------------------------------------------------- /packages/upload-image-module/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": {}, 3 | "extends": "../../tsconfig.json", 4 | "include": [ 5 | "./src/**/*", 6 | "../custom-types.d.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/video-module/README.md: -------------------------------------------------------------------------------- 1 | # wangEditor video-module 2 | 3 | Video module built in [wangEditor](https://www.wangeditor.com/) by default. 4 | -------------------------------------------------------------------------------- /packages/video-module/__tests__/render-elem.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description video render elem test 3 | * @author luochao 4 | */ 5 | 6 | import createEditor from '../../../tests/utils/create-editor' 7 | import { renderVideoConf } from '../src/module/render-elem' 8 | 9 | describe('video module - render elem', () => { 10 | const editor = createEditor() 11 | 12 | it('render video elem', () => { 13 | expect(renderVideoConf.type).toBe('video') 14 | 15 | const elem = { type: 'video', src: 'test.mp4', poster: 'xxx.png', children: [] } 16 | const vnode = renderVideoConf.renderElem(elem, null, editor) 17 | expect(vnode.sel).toBe('div') 18 | }) 19 | 20 | it('render video with iframe', () => { 21 | expect(renderVideoConf.type).toBe('video') 22 | 23 | const elem = { type: 'video', src: '<iframe src="test.mp4"></iframe>', children: [] } 24 | const vnode = renderVideoConf.renderElem(elem, null, editor) 25 | expect(vnode.sel).toBe('div') 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /packages/video-module/__tests__/util.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description video menu test 3 | * @author luochao 4 | */ 5 | 6 | import { genRandomStr } from '../src/utils/util' 7 | 8 | describe('videoModule util', () => { 9 | describe('utils util', () => { 10 | test('genRandomStr should generate a random string every time', () => { 11 | const str1 = genRandomStr() 12 | const str2 = genRandomStr() 13 | 14 | expect(str1).not.toBe(str2) 15 | }) 16 | 17 | test('genRandomStr should generate a random string that specify a prefix string', () => { 18 | const str = genRandomStr('wangeditor') 19 | 20 | expect(str.indexOf('wangeditor-')).toEqual(0) 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/video-module/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createRollupConfig, IS_PRD } from '../../build/create-rollup-config' 2 | import pkg from './package.json' 3 | 4 | const name = 'WangEditorVideoModule' 5 | 6 | const configList = [] 7 | 8 | // esm 9 | const esmConf = createRollupConfig({ 10 | output: { 11 | file: pkg.module, 12 | format: 'esm', 13 | name, 14 | }, 15 | }) 16 | configList.push(esmConf) 17 | 18 | // umd 19 | const umdConf = createRollupConfig({ 20 | output: { 21 | file: pkg.main, 22 | format: 'umd', 23 | name, 24 | }, 25 | }) 26 | configList.push(umdConf) 27 | 28 | export default configList 29 | -------------------------------------------------------------------------------- /packages/video-module/src/assets/index.less: -------------------------------------------------------------------------------- 1 | @import "../../../vars.less"; 2 | 3 | .w-e-textarea-video-container { 4 | text-align: center; 5 | border: 1px dashed @textarea-border-color; 6 | padding: 10px 0; 7 | margin: 0 auto; 8 | margin-top: 10px; 9 | border-radius: 5px; 10 | background-position: 0px 0px, 10px 10px; 11 | background-size: 20px 20px; 12 | background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee 100%),linear-gradient(45deg, #eee 25%, white 25%, white 75%, #eee 75%, #eee 100%); 13 | } 14 | -------------------------------------------------------------------------------- /packages/video-module/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description video module 3 | * @author wangfupeng 4 | */ 5 | 6 | import './assets/index.less' 7 | 8 | // 配置多语言 9 | import './locale/index' 10 | 11 | import wangEditorVideoModule from './module/index' 12 | export default wangEditorVideoModule 13 | -------------------------------------------------------------------------------- /packages/video-module/src/locale/en.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n en 3 | * @author wangfupeng 4 | */ 5 | 6 | export default { 7 | videoModule: { 8 | delete: 'Delete', 9 | uploadVideo: 'Upload video', 10 | insertVideo: 'Insert video', 11 | videoSrc: 'Video source', 12 | videoSrcPlaceHolder: 'Video file url, or third-party <iframe>', 13 | videoPoster: 'Video poster', 14 | videoPosterPlaceHolder: 'Poster image url', 15 | ok: 'Ok', 16 | editSize: 'Edit size', 17 | width: 'Width', 18 | height: 'Height', 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /packages/video-module/src/locale/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n entry 3 | * @author wangfupeng 4 | */ 5 | 6 | import { i18nAddResources } from '@wangeditor/core' 7 | import enResources from './en' 8 | import zhResources from './zh-CN' 9 | 10 | i18nAddResources('en', enResources) 11 | i18nAddResources('zh-CN', zhResources) 12 | -------------------------------------------------------------------------------- /packages/video-module/src/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description i18n zh-CN 3 | * @author wangfupeng 4 | */ 5 | 6 | export default { 7 | videoModule: { 8 | delete: '删除视频', 9 | uploadVideo: '上传视频', 10 | insertVideo: '插入视频', 11 | videoSrc: '视频地址', 12 | videoSrcPlaceHolder: '视频文件 url 或第三方 <iframe>', 13 | videoPoster: '视频封面', 14 | videoPosterPlaceHolder: '封面图片 url', 15 | ok: '确定', 16 | editSize: '修改尺寸', 17 | width: '宽度', 18 | height: '高度', 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /packages/video-module/src/module/custom-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description video element 3 | * @author wangfupeng 4 | */ 5 | 6 | //【注意】需要把自定义的 Element 引入到最外层的 custom-types.d.ts 7 | 8 | type EmptyText = { 9 | text: '' 10 | } 11 | 12 | export type VideoElement = { 13 | type: 'video' 14 | src: string 15 | poster?: string 16 | width?: string 17 | height?: string 18 | children: EmptyText[] 19 | } 20 | -------------------------------------------------------------------------------- /packages/video-module/src/module/elem-to-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description to html 3 | * @author wangfupeng 4 | */ 5 | 6 | import { Element } from 'slate' 7 | import { VideoElement } from './custom-types' 8 | import { genSizeStyledIframeHtml } from '../utils/dom' 9 | 10 | function videoToHtml(elemNode: Element, childrenHtml?: string): string { 11 | const { src = '', poster = '', width = 'auto', height = 'auto' } = elemNode as VideoElement 12 | let res = '<div data-w-e-type="video" data-w-e-is-void>\n' 13 | 14 | if (src.trim().indexOf('<iframe ') === 0) { 15 | // iframe 形式 16 | const iframeHtml = genSizeStyledIframeHtml(src, width, height) 17 | res += iframeHtml 18 | } else { 19 | // 其他,mp4 等 url 格式 20 | res += `<video poster="${poster}" controls="true" width="${width}" height="${height}"><source src="${src}" type="video/mp4"/></video>` 21 | } 22 | res += '\n</div>' 23 | 24 | return res 25 | } 26 | 27 | export const videoToHtmlConf = { 28 | type: 'video', 29 | elemToHtml: videoToHtml, 30 | } 31 | -------------------------------------------------------------------------------- /packages/video-module/src/module/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description video module 3 | * @author wangfupeng 4 | */ 5 | 6 | import { IModuleConf } from '@wangeditor/core' 7 | import withVideo from './plugin' 8 | import { renderVideoConf } from './render-elem' 9 | import { videoToHtmlConf } from './elem-to-html' 10 | import { preParseHtmlConf } from './pre-parse-html' 11 | import { parseHtmlConf } from './parse-elem-html' 12 | import { insertVideoMenuConf, uploadVideoMenuConf, editorVideSizeMenuConf } from './menu/index' 13 | 14 | const video: Partial<IModuleConf> = { 15 | renderElems: [renderVideoConf], 16 | elemsToHtml: [videoToHtmlConf], 17 | preParseHtml: [preParseHtmlConf], 18 | parseElemsHtml: [parseHtmlConf], 19 | menus: [insertVideoMenuConf, uploadVideoMenuConf, editorVideSizeMenuConf], 20 | editorPlugin: withVideo, 21 | } 22 | 23 | export default video 24 | -------------------------------------------------------------------------------- /packages/video-module/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, '&lt;').replace(/>/g, '&gt;') 19 | } 20 | -------------------------------------------------------------------------------- /packages/video-module/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": {}, 3 | "extends": "../../tsconfig.json", 4 | "include": [ 5 | "./src/**/*", 6 | "../custom-types.d.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /scripts/release-tag.js: -------------------------------------------------------------------------------- 1 | const util = require('util') 2 | const exec = util.promisify(require('child_process').exec) 3 | const DEFAULT_RELEASE_COMMIT_MESSAGE = 'chore: release tag' 4 | 5 | function command(command) { 6 | return exec(command, { cwd: process.cwd() }) 7 | .then(resp => { 8 | const data = resp.stdout.toString() 9 | return Promise.resolve(data) 10 | }) 11 | .catch(err => { 12 | throw err 13 | }) 14 | } 15 | 16 | async function run(commitMsg = DEFAULT_RELEASE_COMMIT_MESSAGE) { 17 | const timestamp = Date.now() 18 | const tagName = `v${timestamp}` 19 | // 先打触发 publish ci 的标签 20 | await command(`git tag -a ${tagName} -m"${commitMsg}"`) 21 | // 推送标签到远程触发 ci 22 | await command(`git push origin ${tagName}`) 23 | } 24 | 25 | run() 26 | -------------------------------------------------------------------------------- /tests/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 | 11 | // Jest environment not contains DataTransfer object, so mock a DataTransfer class 12 | // @ts-ignore 13 | global.DataTransfer = class DataTransfer { 14 | clearData() {} 15 | getData(type: string) { 16 | if (type === 'text/plain') return '' 17 | return [] 18 | } 19 | setData() {} 20 | get files() { 21 | return [new File(['124'], 'test.jpg')] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/utils/create-editor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description create editor for test 3 | * @author luochao 4 | */ 5 | import { createEditor as create } from '../../packages/editor/src' 6 | 7 | export default function createEditor(options: any = {}) { 8 | const container = document.createElement('div') 9 | document.body.appendChild(container) 10 | 11 | return create({ 12 | selector: container, 13 | ...options, 14 | }) 15 | } 16 | 17 | // 【注意】packages/editor 中的 createEditor 不能用于 packages/core 的单元测试(因为从模块关系上,后者不能依赖于前者)!!! 18 | // 只能用于其他 package 19 | -------------------------------------------------------------------------------- /tests/utils/create-toolbar.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description create toolbar for test 3 | * @author wangfupeng 4 | */ 5 | import { createToolbar as create } from '../../packages/editor/src' 6 | 7 | export default function createToolbar(editor: any, config: any = {}) { 8 | const container = document.createElement('div') 9 | document.body.appendChild(container) 10 | 11 | return create({ 12 | editor, 13 | selector: container, 14 | config, 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /tests/utils/stylesMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "ES2015", 5 | "lib": [ 6 | "es6", 7 | "dom", 8 | "esnext" 9 | ], 10 | "declaration": true, 11 | "sourceMap": true, 12 | "outDir": "dist", 13 | "strict": true, 14 | "noImplicitAny": false, 15 | "resolveJsonModule": true, 16 | "esModuleInterop": true, 17 | "moduleResolution": "node", 18 | "forceConsistentCasingInFileNames": true, 19 | "jsx": "react", 20 | "jsxFactory": "jsx", /* snabbdom jsx */ 21 | "downlevelIteration": true, 22 | "allowJs": true 23 | }, 24 | "exclude": [ 25 | "node_modules", 26 | "build", 27 | "__tests__" 28 | ], 29 | "include": ["./tests/setup/index.ts", "./packages/custom-types.d.ts"] 30 | } --------------------------------------------------------------------------------