├── .browserslistrc ├── .editorconfig ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .releaserc.json ├── .yarnrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README_ZH.md ├── babel.config.js ├── demos ├── App.vue ├── assets │ ├── logo.svg │ ├── logo_for_github.png │ └── wechat_reward_qrcode.jpg ├── main.js ├── router │ └── index.js └── views │ ├── AllExtensions.vue │ ├── BubbleMenu.vue │ ├── Event.vue │ ├── Index.vue │ ├── Output.vue │ ├── Placeholder.vue │ ├── Readonly.vue │ ├── Simple.vue │ └── Title.vue ├── index.html ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── wechat_reward_qrcode.jpg ├── src ├── components │ ├── ElementTiptap.vue │ ├── ExtensionViews │ │ ├── IframeView.vue │ │ ├── ImageView.vue │ │ └── TaskItemView.vue │ ├── Icon │ │ └── Icon.vue │ ├── MenuBar │ │ └── index.vue │ ├── MenuBubble │ │ ├── ImageBubbleMenu.vue │ │ ├── LinkBubbleMenu.vue │ │ └── index.vue │ └── MenuCommands │ │ ├── CodeViewCommandButton.vue │ │ ├── ColorPopover.vue │ │ ├── CommandButton.vue │ │ ├── FontFamilyDropdown.vue │ │ ├── FontSizeDropdown.vue │ │ ├── FullscreenCommandButton.vue │ │ ├── HeadingDropdown.vue │ │ ├── HighlightPopover.vue │ │ ├── IframeCommandButton.vue │ │ ├── Image │ │ ├── EditImageCommandButton.vue │ │ ├── ImageDisplayCommandButton.vue │ │ ├── InsertImageCommandButton.vue │ │ └── RemoveImageCommandButton.vue │ │ ├── LineHeightDropdown.vue │ │ ├── Link │ │ ├── AddLinkCommandButton.vue │ │ ├── EditLinkCommandButton.vue │ │ ├── OpenLinkCommandButton.vue │ │ └── UnlinkCommandButton.vue │ │ └── TablePopover │ │ ├── CreateTablePopover.vue │ │ └── index.vue ├── constants.ts ├── extensions │ ├── blockquote.ts │ ├── bold.ts │ ├── bullet-list.ts │ ├── code-block.ts │ ├── code-view.ts │ ├── color.ts │ ├── document.ts │ ├── font-family.ts │ ├── font-size.ts │ ├── format-clear.ts │ ├── fullscreen.ts │ ├── heading.ts │ ├── highlight.ts │ ├── history.ts │ ├── horizontal-rule.ts │ ├── iframe.ts │ ├── image.ts │ ├── indent.ts │ ├── index.ts │ ├── italic.ts │ ├── line-height.ts │ ├── link.ts │ ├── list-item.ts │ ├── ordered-list.ts │ ├── print.ts │ ├── select-all.ts │ ├── strike.ts │ ├── table.ts │ ├── task-item.ts │ ├── task-list.ts │ ├── text-align.ts │ ├── title.ts │ └── underline.ts ├── hooks │ ├── index.ts │ ├── useCharacterCount.ts │ ├── useCodeView.ts │ └── useEditorStyle.ts ├── i18n │ ├── index.ts │ └── locales │ │ ├── de │ │ └── index.ts │ │ ├── en │ │ └── index.ts │ │ ├── es │ │ └── index.ts │ │ ├── fr │ │ └── index.ts │ │ ├── he │ │ └── index.ts │ │ ├── ko │ │ └── index.ts │ │ ├── nl │ │ └── index.ts │ │ ├── pl │ │ └── index.ts │ │ ├── pt-br │ │ └── index.ts │ │ ├── ru │ │ └── index.ts │ │ ├── zh-tw │ │ └── index.ts │ │ └── zh │ │ └── index.ts ├── icons │ ├── align-center.svg │ ├── align-justify.svg │ ├── align-left.svg │ ├── align-right.svg │ ├── arrow-left.svg │ ├── bold.svg │ ├── clear-format.svg │ ├── code.svg │ ├── compress.svg │ ├── edit.svg │ ├── ellipsis-h.svg │ ├── expand.svg │ ├── external-link.svg │ ├── file-code.svg │ ├── font-color.svg │ ├── font-family.svg │ ├── font-size.svg │ ├── heading.svg │ ├── highlight.svg │ ├── horizontal-rule.svg │ ├── image-align.svg │ ├── image.svg │ ├── indent.svg │ ├── italic.svg │ ├── link.svg │ ├── list-ol.svg │ ├── list-ul.svg │ ├── outdent.svg │ ├── print.svg │ ├── quote-right.svg │ ├── redo.svg │ ├── select-all.svg │ ├── strikethrough.svg │ ├── table.svg │ ├── tasks.svg │ ├── text-height.svg │ ├── trash-alt.svg │ ├── underline.svg │ ├── undo.svg │ ├── unlink.svg │ └── video.svg ├── index.ts ├── styles │ ├── command-button.scss │ ├── editor.scss │ └── variables.scss └── utils │ ├── code-view.ts │ ├── color.ts │ ├── font-size.ts │ ├── font-type.ts │ ├── image.ts │ ├── indent.ts │ ├── line-height.ts │ ├── logger.ts │ ├── print.ts │ ├── shared.ts │ └── table.ts ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true 5 | }, 6 | "extends": ["plugin:vue/essential", "@vue/standard", "@vue/typescript"], 7 | "rules": { 8 | "comma-dangle": ["error", "only-multiline"], 9 | "semi": ["error", "always"], 10 | "indent": [ 11 | "error", 12 | 2, 13 | { 14 | "SwitchCase": 1, 15 | "VariableDeclarator": { 16 | "var": 2, 17 | "let": 2, 18 | "const": 3 19 | } 20 | } 21 | ], 22 | "linebreak-style": ["error", "unix"], 23 | "quotes": ["error", "single"], 24 | "no-multi-spaces": [ 25 | "error", 26 | { 27 | "exceptions": { 28 | "VariableDeclarator": true 29 | } 30 | } 31 | ], 32 | "space-before-function-paren": ["error", "never"], 33 | "no-unused-vars": "off", 34 | "camelcase": "off", 35 | "@typescript-eslint/no-unused-vars": "error" 36 | }, 37 | "parserOptions": { 38 | "parser": "@typescript-eslint/parser" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - next 8 | - beta 9 | - alpha 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v2 18 | with: 19 | node-version: '16' 20 | 21 | - uses: c-hive/gha-yarn-cache@v2 22 | 23 | - run: yarn install --registry=https://registry.yarnpkg.com 24 | 25 | # - name: Test 26 | # run: yarn lint 27 | 28 | - name: Build 29 | run: yarn build:lib 30 | 31 | - name: release 32 | run: yarn semantic-release 33 | env: 34 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /lib 5 | 6 | auto-imports.d.ts 7 | components.d.ts 8 | 9 | # local env files 10 | .env.local 11 | .env.*.local 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" 2 | message="release: 🔖 %s" 3 | registry="https://registry.npmjs.org/" 4 | access=public 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "bracketSpacing": true, 5 | "useTabs": false, 6 | "tabWidth": 2, 7 | "semi": true 8 | } 9 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "next", 4 | "master", 5 | { "name": "beta", "prerelease": true }, 6 | { "name": "alpha", "prerelease": true } 7 | ], 8 | "tagFormat": "@${version}", 9 | "plugins": [ 10 | [ 11 | "@semantic-release/commit-analyzer", 12 | { 13 | "preset": "angular", 14 | "releaseRules": [ 15 | { "type": "breaking", "release": "major" }, 16 | { "type": "docs", "scope": "README", "release": "patch" }, 17 | { "type": "refactor", "release": "patch" }, 18 | { "type": "style", "release": "patch" }, 19 | { "type": "chore", "release": false }, 20 | { "type": "example", "release": false } 21 | ] 22 | } 23 | ], 24 | "@semantic-release/release-notes-generator", 25 | [ 26 | "@semantic-release/changelog", 27 | { 28 | "changelogFile": "CHANGELOG.md" 29 | } 30 | ], 31 | [ 32 | "@semantic-release/github", 33 | { 34 | "assets": [ 35 | { "path": "lib/element-tiptap.umd.js", "label": "UMD module" }, 36 | { "path": "lib/element-tiptap.es.js", "label": "ECMAScript 6 module" } 37 | ] 38 | } 39 | ], 40 | "@semantic-release/npm", 41 | "@semantic-release/git" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.yarnpkg.com" -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## 🏗 Contributing ![PR or ISSUE](https://img.shields.io/badge/PR%20or%20ISSUE-welcome-brightgreen) 2 | 3 | 1. 🍴Fork it 4 | 2. 🔀Create your branch: `git checkout -b your-branch` 5 | 3. 🎨Make your changes 6 | 4. 📝Commit your changes with [Semantic Commit Messages (recommended)](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) 7 | 5. 🚀Push to the branch: `git push origin your-branch` 8 | 6. 🎉Submit a PR to `develop` branch 9 | 10 | _OR_ 11 | 12 | Just submit an [issue](https://github.com/Leecason/element-tiptap/issues)! - any helpful suggestions are welcomed. 😜 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jiaxun Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /demos/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 28 | -------------------------------------------------------------------------------- /demos/assets/logo_for_github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leecason/element-tiptap/c7b6d2f37a766f24ab10e64a553f60a49971ef3c/demos/assets/logo_for_github.png -------------------------------------------------------------------------------- /demos/assets/wechat_reward_qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leecason/element-tiptap/c7b6d2f37a766f24ab10e64a553f60a49971ef3c/demos/assets/wechat_reward_qrcode.jpg -------------------------------------------------------------------------------- /demos/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { 3 | // layout 4 | ElContainer, 5 | ElHeader, 6 | ElMain, 7 | ElFooter, 8 | ElButton, 9 | } from 'element-plus'; 10 | 11 | import ElementTiptap from 'element-tiptap'; 12 | import App from './App.vue'; 13 | import router from './router'; 14 | 15 | const app = createApp(App); 16 | 17 | app.use(router); 18 | 19 | app.use(ElContainer); 20 | app.use(ElHeader); 21 | app.use(ElMain); 22 | app.use(ElFooter); 23 | app.use(ElButton); 24 | 25 | app.use(ElementTiptap); 26 | 27 | app.mount('#app'); 28 | -------------------------------------------------------------------------------- /demos/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router'; 2 | 3 | const Index = () => import('../views/Index.vue'); 4 | const Simple = () => import('../views/Simple.vue'); 5 | const AllExtensions = () => import('../views/AllExtensions.vue'); 6 | const BubbleMenu = () => import('../views/BubbleMenu.vue'); 7 | const Placeholder = () => import('../views/Placeholder.vue'); 8 | const Readonly = () => import('../views/Readonly.vue'); 9 | const Title = () => import('../views/Title.vue'); 10 | const Event = () => import('../views/Event.vue'); 11 | const Output = () => import('../views/Output.vue'); 12 | 13 | const routes = [ 14 | { 15 | path: '/', 16 | component: Index, 17 | children: [ 18 | { 19 | name: 'Simple', 20 | path: '/', 21 | component: Simple, 22 | }, 23 | { 24 | name: 'AllExtensions', 25 | path: 'all-extensions', 26 | component: AllExtensions, 27 | }, 28 | { 29 | name: 'BubbleMenu', 30 | path: '/bubble-menu', 31 | component: BubbleMenu, 32 | }, 33 | { 34 | name: 'Placeholder', 35 | path: '/placeholder', 36 | component: Placeholder, 37 | }, 38 | { 39 | name: 'Readonly', 40 | path: '/readonly', 41 | component: Readonly, 42 | }, 43 | { 44 | name: 'Title', 45 | path: '/title', 46 | component: Title, 47 | }, 48 | { 49 | name: 'Event', 50 | path: '/event', 51 | component: Event, 52 | }, 53 | { 54 | name: 'Output', 55 | path: '/output', 56 | component: Output, 57 | }, 58 | ], 59 | }, 60 | ]; 61 | 62 | const router = createRouter({ 63 | history: createWebHashHistory(), 64 | routes, 65 | }); 66 | 67 | export default router; 68 | -------------------------------------------------------------------------------- /demos/views/AllExtensions.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 117 | -------------------------------------------------------------------------------- /demos/views/BubbleMenu.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 61 | -------------------------------------------------------------------------------- /demos/views/Event.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 89 | -------------------------------------------------------------------------------- /demos/views/Output.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 79 | 80 | 115 | -------------------------------------------------------------------------------- /demos/views/Placeholder.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 37 | -------------------------------------------------------------------------------- /demos/views/Readonly.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 45 | -------------------------------------------------------------------------------- /demos/views/Simple.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 79 | -------------------------------------------------------------------------------- /demos/views/Title.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 43 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | element-tiptap 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "element-tiptap", 3 | "version": "2.2.1", 4 | "description": "🌸A modern WYSIWYG rich-text editor using tiptap and Element UI for Vue.js", 5 | "author": "leecason", 6 | "scripts": { 7 | "dev": "vite", 8 | "demo:preview": "vite preview", 9 | "build:demo": "VITE_BUILD_TARGET=demo vite build", 10 | "build:lib": "vite build", 11 | "lint": "eslint --ext .ts src", 12 | "lint:fix": "yarn lint --fix", 13 | "semantic-release": "semantic-release" 14 | }, 15 | "main": "./lib/element-tiptap.umd.js", 16 | "module": "./lib/element-tiptap.es.js", 17 | "files": [ 18 | "lib/*" 19 | ], 20 | "types": "./lib/index.d.ts", 21 | "dependencies": { 22 | "@juggle/resize-observer": "^3.1.2", 23 | "@tiptap/core": "^2.0.0-beta.171", 24 | "@tiptap/extension-blockquote": "^2.0.0-beta.26", 25 | "@tiptap/extension-bold": "^2.0.0-beta.25", 26 | "@tiptap/extension-bubble-menu": "^2.0.0-beta.56", 27 | "@tiptap/extension-bullet-list": "^2.0.0-beta.26", 28 | "@tiptap/extension-character-count": "^2.0.0-beta.26", 29 | "@tiptap/extension-code": "^2.0.0-beta.26", 30 | "@tiptap/extension-code-block": "^2.0.0-beta.37", 31 | "@tiptap/extension-color": "^2.0.0-beta.9", 32 | "@tiptap/extension-document": "^2.0.0-beta.15", 33 | "@tiptap/extension-hard-break": "^2.0.0-beta.30", 34 | "@tiptap/extension-heading": "^2.0.0-beta.26", 35 | "@tiptap/extension-highlight": "^2.0.0-beta.33", 36 | "@tiptap/extension-history": "^2.0.0-beta.21", 37 | "@tiptap/extension-horizontal-rule": "^2.0.0-beta.31", 38 | "@tiptap/extension-image": "^2.0.0-beta.25", 39 | "@tiptap/extension-italic": "^2.0.0-beta.25", 40 | "@tiptap/extension-link": "^2.0.0-beta.36", 41 | "@tiptap/extension-list-item": "^2.0.0-beta.20", 42 | "@tiptap/extension-ordered-list": "^2.0.0-beta.27", 43 | "@tiptap/extension-paragraph": "^2.0.0-beta.23", 44 | "@tiptap/extension-placeholder": "^2.0.0-beta.51", 45 | "@tiptap/extension-strike": "^2.0.0-beta.27", 46 | "@tiptap/extension-table": "^2.0.0-beta.49", 47 | "@tiptap/extension-table-cell": "^2.0.0-beta.20", 48 | "@tiptap/extension-table-header": "^2.0.0-beta.22", 49 | "@tiptap/extension-table-row": "^2.0.0-beta.19", 50 | "@tiptap/extension-task-item": "^2.0.0-beta.31", 51 | "@tiptap/extension-task-list": "^2.0.0-beta.26", 52 | "@tiptap/extension-text": "^2.0.0-beta.15", 53 | "@tiptap/extension-text-align": "^2.0.0-beta.29", 54 | "@tiptap/extension-text-style": "^2.0.0-beta.23", 55 | "@tiptap/extension-underline": "^2.0.0-beta.22", 56 | "@tiptap/pm": "^2.1.12", 57 | "@tiptap/vue-3": "^2.1.12", 58 | "core-js": "^3.4.3" 59 | }, 60 | "devDependencies": { 61 | "@babel/core": "^7.7.5", 62 | "@babel/preset-env": "^7.7.6", 63 | "@semantic-release/changelog": "^5.0.0", 64 | "@semantic-release/commit-analyzer": "^8.0.1", 65 | "@semantic-release/git": "^9.0.0", 66 | "@typescript-eslint/eslint-plugin": "^2.18.0", 67 | "@typescript-eslint/parser": "^2.18.0", 68 | "@vitejs/plugin-vue": "^2.1.0", 69 | "@vue/cli-plugin-babel": "~4.4.1", 70 | "@vue/compiler-sfc": "^3.2.37", 71 | "@vue/eslint-config-standard": "^5.1.0", 72 | "@vue/eslint-config-typescript": "^5.0.1", 73 | "autoprefixer": "^10.4.7", 74 | "babel-eslint": "^10.0.3", 75 | "codemirror": "^5.54.0", 76 | "element-plus": "^2.2.8", 77 | "eslint": "^6.7.2", 78 | "eslint-plugin-import": "^2.20.1", 79 | "eslint-plugin-node": "^11.0.0", 80 | "eslint-plugin-promise": "^4.2.1", 81 | "eslint-plugin-standard": "^4.0.0", 82 | "eslint-plugin-vue": "^6.1.2", 83 | "lint-staged": "^10.0.8", 84 | "postcss-nested": "^5.0.6", 85 | "postcss-preset-env": "^6.7.0", 86 | "rollup-plugin-copy": "^3.4.0", 87 | "sass": "^1.53.0", 88 | "semantic-release": "^17.0.4", 89 | "typescript": "^4.5.5", 90 | "unplugin-element-plus": "^0.4.1", 91 | "vite": "^2.9.13", 92 | "vite-plugin-dts": "^1.2.1", 93 | "vite-plugin-vue2": "^1.9.2", 94 | "vite-svg-loader": "^3.1.2", 95 | "vue": "^3.2.29", 96 | "vue-router": "^4.0.12", 97 | "yorkie": "^2.0.0" 98 | }, 99 | "peerDependencies": { 100 | "element-plus": ">= 2.0.0", 101 | "vue": ">= 3.0.0" 102 | }, 103 | "resolutions": {}, 104 | "bugs": { 105 | "url": "https://github.com/Leecason/element-tiptap/issues" 106 | }, 107 | "gitHooks": { 108 | "pre-commit": "lint-staged" 109 | }, 110 | "homepage": "https://github.com/Leecason/element-tiptapr#readme", 111 | "keywords": [ 112 | "editor", 113 | "wysiwyg", 114 | "vue3", 115 | "vue", 116 | "tiptap", 117 | "prosemirror", 118 | "element", 119 | "element-ui", 120 | "element-plus" 121 | ], 122 | "license": "MIT", 123 | "lint-staged": { 124 | "*.{js,vue}": [ 125 | "yarn lint", 126 | "git add" 127 | ] 128 | }, 129 | "repository": { 130 | "type": "git", 131 | "url": "https://github.com/Leecason/element-tiptap.git" 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('autoprefixer'), require('postcss-nested')], 3 | }; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leecason/element-tiptap/c7b6d2f37a766f24ab10e64a553f60a49971ef3c/public/favicon.ico -------------------------------------------------------------------------------- /public/wechat_reward_qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leecason/element-tiptap/c7b6d2f37a766f24ab10e64a553f60a49971ef3c/public/wechat_reward_qrcode.jpg -------------------------------------------------------------------------------- /src/components/ExtensionViews/IframeView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | -------------------------------------------------------------------------------- /src/components/ExtensionViews/TaskItemView.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 47 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /src/components/MenuBar/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 60 | -------------------------------------------------------------------------------- /src/components/MenuBubble/ImageBubbleMenu.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | -------------------------------------------------------------------------------- /src/components/MenuBubble/LinkBubbleMenu.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 41 | 42 | 47 | -------------------------------------------------------------------------------- /src/components/MenuBubble/index.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 182 | -------------------------------------------------------------------------------- /src/components/MenuCommands/CodeViewCommandButton.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 34 | -------------------------------------------------------------------------------- /src/components/MenuCommands/ColorPopover.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 135 | -------------------------------------------------------------------------------- /src/components/MenuCommands/CommandButton.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 75 | -------------------------------------------------------------------------------- /src/components/MenuCommands/FontFamilyDropdown.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 84 | -------------------------------------------------------------------------------- /src/components/MenuCommands/FontSizeDropdown.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 99 | -------------------------------------------------------------------------------- /src/components/MenuCommands/FullscreenCommandButton.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 50 | -------------------------------------------------------------------------------- /src/components/MenuCommands/HeadingDropdown.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 98 | -------------------------------------------------------------------------------- /src/components/MenuCommands/HighlightPopover.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 110 | -------------------------------------------------------------------------------- /src/components/MenuCommands/IframeCommandButton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 59 | -------------------------------------------------------------------------------- /src/components/MenuCommands/Image/EditImageCommandButton.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 150 | -------------------------------------------------------------------------------- /src/components/MenuCommands/Image/ImageDisplayCommandButton.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 87 | -------------------------------------------------------------------------------- /src/components/MenuCommands/Image/InsertImageCommandButton.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 169 | -------------------------------------------------------------------------------- /src/components/MenuCommands/Image/RemoveImageCommandButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 44 | -------------------------------------------------------------------------------- /src/components/MenuCommands/LineHeightDropdown.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 80 | -------------------------------------------------------------------------------- /src/components/MenuCommands/Link/AddLinkCommandButton.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 135 | -------------------------------------------------------------------------------- /src/components/MenuCommands/Link/EditLinkCommandButton.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 120 | -------------------------------------------------------------------------------- /src/components/MenuCommands/Link/OpenLinkCommandButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 55 | -------------------------------------------------------------------------------- /src/components/MenuCommands/Link/UnlinkCommandButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 37 | -------------------------------------------------------------------------------- /src/components/MenuCommands/TablePopover/CreateTablePopover.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 123 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { ImageDisplay } from './utils/image'; 2 | 3 | export const ELEMENT_TIPTAP_TIP = '[Element-Tiptap Tip]'; 4 | 5 | export const enum Alignment { 6 | left = 'left', 7 | center = 'center', 8 | right = 'right', 9 | justify = 'justify' 10 | } 11 | export const ALIGN_PATTERN: RegExp = new RegExp(`(${Alignment.left}|${Alignment.center}|${Alignment.right}|${Alignment.justify})`); 12 | 13 | export const DEFAULT_IMAGE_URL_REGEX = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/; 14 | export const DEFAULT_IMAGE_WIDTH = 200; 15 | export const DEFAULT_IMAGE_DISPLAY = ImageDisplay.INLINE; 16 | 17 | export const LINE_HEIGHT_100 = 1.7; 18 | export const DEFAULT_LINE_HEIGHT = '100%'; 19 | 20 | export const enum EVENTS { 21 | INIT = 'init', 22 | TRANSACTION = 'transaction', 23 | FOCUS = 'focus', 24 | BLUR = 'blur', 25 | PASTE = 'paste', 26 | DROP = 'drop', 27 | UPDATE = 'update', 28 | }; 29 | 30 | export const PREVIEW_WINDOW_WIDTH: string = '80vw'; 31 | -------------------------------------------------------------------------------- /src/extensions/blockquote.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import TiptapBlockquote from '@tiptap/extension-blockquote'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | 5 | const Blockquote = TiptapBlockquote.extend({ 6 | addOptions() { 7 | return { 8 | ...this.parent?.(), 9 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 10 | return { 11 | component: CommandButton, 12 | componentProps: { 13 | command: () => { 14 | editor.commands.toggleBlockquote(); 15 | }, 16 | isActive: editor.isActive('blockquote'), 17 | icon: 'quote-right', 18 | tooltip: t('editor.extensions.Blockquote.tooltip'), 19 | }, 20 | }; 21 | }, 22 | }; 23 | }, 24 | }); 25 | 26 | export default Blockquote; 27 | -------------------------------------------------------------------------------- /src/extensions/bold.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import TiptapBold from '@tiptap/extension-bold'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | 5 | const Bold = TiptapBold.extend({ 6 | addOptions() { 7 | return { 8 | ...this.parent?.(), 9 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 10 | return { 11 | component: CommandButton, 12 | componentProps: { 13 | command: () => { 14 | editor.commands.toggleBold(); 15 | }, 16 | isActive: editor.isActive('bold'), 17 | icon: 'bold', 18 | tooltip: t('editor.extensions.Bold.tooltip'), 19 | }, 20 | }; 21 | }, 22 | }; 23 | }, 24 | }); 25 | 26 | export default Bold; 27 | -------------------------------------------------------------------------------- /src/extensions/bullet-list.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import TiptapBulletList from '@tiptap/extension-bullet-list'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | import ListItem from './list-item'; 5 | 6 | const BulletList = TiptapBulletList.extend({ 7 | addOptions() { 8 | return { 9 | ...this.parent?.(), 10 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 11 | return { 12 | component: CommandButton, 13 | componentProps: { 14 | command: () => { 15 | editor.commands.toggleBulletList(); 16 | }, 17 | isActive: editor.isActive('bulletList'), 18 | icon: 'list-ul', 19 | tooltip: t('editor.extensions.BulletList.tooltip'), 20 | }, 21 | }; 22 | }, 23 | }; 24 | }, 25 | 26 | addExtensions() { 27 | return [ListItem]; 28 | }, 29 | }); 30 | 31 | export default BulletList; 32 | -------------------------------------------------------------------------------- /src/extensions/code-block.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import TiptapCodeBlock from '@tiptap/extension-code-block'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | 5 | const CodeBlock = TiptapCodeBlock.extend({ 6 | addOptions() { 7 | return { 8 | ...this.parent?.(), 9 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 10 | return { 11 | component: CommandButton, 12 | componentProps: { 13 | command: () => { 14 | editor.commands.toggleCodeBlock(); 15 | }, 16 | isActive: editor.isActive('codeBlock'), 17 | icon: 'code', 18 | tooltip: t('editor.extensions.CodeBlock.tooltip'), 19 | }, 20 | }; 21 | }, 22 | }; 23 | }, 24 | }); 25 | 26 | export default CodeBlock; 27 | -------------------------------------------------------------------------------- /src/extensions/code-view.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '@tiptap/core'; 2 | import type { Editor } from '@tiptap/core'; 3 | import { extendCodemirror } from '@/utils/code-view'; 4 | import Logger from '@/utils/logger'; 5 | import CodeViewCommandButton from '@/components/MenuCommands/CodeViewCommandButton.vue'; 6 | 7 | export const DEFAULT_CODEMIRROR_OPTIONS = { 8 | lineNumbers: true, 9 | lineWrapping: true, 10 | tabSize: 2, 11 | tabMode: 'indent', 12 | mode: 'text/html', 13 | }; 14 | 15 | export interface CodeViewOptions { 16 | codemirror: any; 17 | codemirrorOptions: any; 18 | } 19 | 20 | const CodeView = Extension.create({ 21 | name: 'codeView', 22 | 23 | defaultOptions: { 24 | codemirror: null, 25 | codemirrorOptions: { 26 | ...DEFAULT_CODEMIRROR_OPTIONS, 27 | }, 28 | }, 29 | 30 | onBeforeCreate() { 31 | if (!this.options.codemirror) { 32 | Logger.warn('"CodeView" extension requires the CodeMirror library.'); 33 | return; 34 | } 35 | 36 | extendCodemirror(this.options.codemirror); 37 | this.options.codemirrorOptions = { 38 | ...DEFAULT_CODEMIRROR_OPTIONS, 39 | ...this.options.codemirrorOptions, 40 | }; 41 | }, 42 | 43 | addOptions() { 44 | return { 45 | ...this.parent?.(), 46 | button({ editor }: { editor: Editor }) { 47 | return { 48 | component: CodeViewCommandButton, 49 | }; 50 | }, 51 | }; 52 | }, 53 | }); 54 | 55 | export default CodeView; 56 | -------------------------------------------------------------------------------- /src/extensions/color.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from '@tiptap/core'; 2 | import TiptapColor from '@tiptap/extension-color'; 3 | import { COLOR_SET } from '@/utils/color'; 4 | import ColorPopover from '@/components/MenuCommands/ColorPopover.vue'; 5 | import TextStyle from '@tiptap/extension-text-style'; 6 | 7 | const Color = TiptapColor.extend({ 8 | addOptions() { 9 | return { 10 | ...this.parent?.(), 11 | colors: COLOR_SET, 12 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 13 | return { 14 | component: ColorPopover, 15 | componentProps: { 16 | editor, 17 | }, 18 | }; 19 | }, 20 | }; 21 | }, 22 | 23 | addExtensions() { 24 | return [TextStyle]; 25 | }, 26 | }); 27 | 28 | export default Color; 29 | -------------------------------------------------------------------------------- /src/extensions/document.ts: -------------------------------------------------------------------------------- 1 | import TiptapDocument from '@tiptap/extension-document'; 2 | import Title from './title'; 3 | 4 | export interface DocumentOptions { 5 | title: boolean; 6 | } 7 | 8 | const Document = TiptapDocument.extend({ 9 | addOptions() { 10 | return { 11 | title: false, 12 | }; 13 | }, 14 | 15 | content() { 16 | return !!this.options.title ? 'title block+' : 'block+'; 17 | }, 18 | 19 | addExtensions() { 20 | if (this.options.title) { 21 | return [Title]; 22 | } 23 | return []; 24 | }, 25 | }); 26 | 27 | export default Document; 28 | -------------------------------------------------------------------------------- /src/extensions/font-family.ts: -------------------------------------------------------------------------------- 1 | import { Editor, Extension } from '@tiptap/core'; 2 | import { DEFAULT_FONT_FAMILY_MAP } from '@/utils/font-type'; 3 | import FontFamilyDropdown from '@/components/MenuCommands/FontFamilyDropdown.vue'; 4 | import TextStyle from '@tiptap/extension-text-style'; 5 | 6 | export type FontFamilyOptions = { 7 | types: string[]; 8 | }; 9 | 10 | declare module '@tiptap/core' { 11 | interface Commands { 12 | fontFamily: { 13 | /** 14 | * Set the font family 15 | */ 16 | setFontFamily: (fontFamily: string) => ReturnType; 17 | /** 18 | * Unset the font family 19 | */ 20 | unsetFontFamily: () => ReturnType; 21 | }; 22 | } 23 | } 24 | 25 | const FontFamily = Extension.create({ 26 | name: 'fontFamily', 27 | 28 | addOptions() { 29 | return { 30 | types: ['textStyle'], 31 | fontFamilyMap: DEFAULT_FONT_FAMILY_MAP, 32 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 33 | return { 34 | component: FontFamilyDropdown, 35 | componentProps: { 36 | editor, 37 | }, 38 | }; 39 | }, 40 | }; 41 | }, 42 | 43 | addGlobalAttributes() { 44 | return [ 45 | { 46 | types: this.options.types, 47 | attributes: { 48 | fontFamily: { 49 | default: null, 50 | parseHTML: (element) => 51 | element.style.fontFamily.replace(/['"]/g, ''), 52 | renderHTML: (attributes) => { 53 | if (!attributes.fontFamily) { 54 | return {}; 55 | } 56 | 57 | return { 58 | style: `font-family: ${attributes.fontFamily}`, 59 | }; 60 | }, 61 | }, 62 | }, 63 | }, 64 | ]; 65 | }, 66 | 67 | addCommands() { 68 | return { 69 | setFontFamily: 70 | (fontFamily) => 71 | ({ chain }) => { 72 | return chain().setMark('textStyle', { fontFamily }).run(); 73 | }, 74 | 75 | unsetFontFamily: 76 | () => 77 | ({ chain }) => { 78 | return chain() 79 | .setMark('textStyle', { fontFamily: null }) 80 | .removeEmptyTextStyle() 81 | .run(); 82 | }, 83 | }; 84 | }, 85 | 86 | addExtensions() { 87 | return [TextStyle]; 88 | }, 89 | }); 90 | 91 | export default FontFamily; 92 | -------------------------------------------------------------------------------- /src/extensions/font-size.ts: -------------------------------------------------------------------------------- 1 | import { Editor, Extension } from '@tiptap/core'; 2 | import { 3 | DEFAULT_FONT_SIZES, 4 | convertToPX, 5 | DEFAULT_FONT_SIZE, 6 | } from '@/utils/font-size'; 7 | import FontSizeDropdown from '@/components/MenuCommands/FontSizeDropdown.vue'; 8 | import TextStyle from '@tiptap/extension-text-style'; 9 | 10 | export type FontSizeOptions = { 11 | types: string[]; 12 | }; 13 | 14 | declare module '@tiptap/core' { 15 | interface Commands { 16 | fontSize: { 17 | /** 18 | * Set the font size 19 | */ 20 | setFontSize: (fontSize: string) => ReturnType; 21 | /** 22 | * Unset the font size 23 | */ 24 | unsetFontSize: () => ReturnType; 25 | }; 26 | } 27 | } 28 | 29 | const FontSize = Extension.create({ 30 | name: 'fontSize', 31 | 32 | addOptions() { 33 | return { 34 | types: ['textStyle'], 35 | fontSizes: DEFAULT_FONT_SIZES, 36 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 37 | return { 38 | component: FontSizeDropdown, 39 | componentProps: { 40 | editor, 41 | }, 42 | }; 43 | }, 44 | }; 45 | }, 46 | 47 | addGlobalAttributes() { 48 | return [ 49 | { 50 | types: this.options.types, 51 | attributes: { 52 | fontSize: { 53 | default: null, 54 | parseHTML: (element) => convertToPX(element.style.fontSize) || '', 55 | renderHTML: (attributes) => { 56 | if (!attributes.fontSize) { 57 | return {}; 58 | } 59 | 60 | return { 61 | style: `font-size: ${attributes.fontSize}px`, 62 | }; 63 | }, 64 | }, 65 | }, 66 | }, 67 | ]; 68 | }, 69 | 70 | addCommands() { 71 | return { 72 | setFontSize: 73 | (fontSize) => 74 | ({ chain }) => { 75 | return chain().setMark('textStyle', { fontSize }).run(); 76 | }, 77 | unsetFontSize: 78 | () => 79 | ({ chain }) => { 80 | return chain() 81 | .setMark('textStyle', { fontSize: DEFAULT_FONT_SIZE }) 82 | .removeEmptyTextStyle() 83 | .run(); 84 | }, 85 | }; 86 | }, 87 | 88 | addExtensions() { 89 | return [TextStyle]; 90 | }, 91 | }); 92 | 93 | export default FontSize; 94 | -------------------------------------------------------------------------------- /src/extensions/format-clear.ts: -------------------------------------------------------------------------------- 1 | import { ChainedCommands, Extension, UnionCommands } from '@tiptap/core'; 2 | import type { Editor } from '@tiptap/core'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | 5 | declare module '@tiptap/core' { 6 | interface Commands { 7 | formatClear: { 8 | formatClear: () => ReturnType; 9 | }; 10 | } 11 | } 12 | 13 | const FormatClear = Extension.create({ 14 | name: 'formatClear', 15 | 16 | addCommands() { 17 | const commandsMap: Record = { 18 | bold: 'unsetBold', 19 | italic: 'unsetItalic', 20 | underline: 'unsetUnderline', 21 | strike: 'unsetStrike', 22 | link: 'unsetLink', 23 | fontFamily: 'unsetFontFamily', 24 | fontSize: 'unsetFontSize', 25 | color: 'unsetColor', 26 | highlight: 'unsetHighlight', 27 | textAlign: 'unsetTextAlign', 28 | lineHeight: 'unsetLineHeight', 29 | }; 30 | 31 | return { 32 | formatClear: 33 | () => 34 | ({ editor }) => { 35 | const chainedCommand: ChainedCommands = Object.entries( 36 | commandsMap 37 | ).reduce((chain, [name, command]) => { 38 | const extension = editor.extensionManager.extensions.find( 39 | (e) => e.name === name 40 | ); 41 | if (extension) { 42 | return chain[command](); 43 | } 44 | return chain; 45 | }, editor.chain()); 46 | 47 | return chainedCommand.focus().run(); 48 | }, 49 | }; 50 | }, 51 | 52 | addOptions() { 53 | return { 54 | ...this.parent?.(), 55 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 56 | return { 57 | component: CommandButton, 58 | componentProps: { 59 | command: () => { 60 | editor.commands.formatClear(); 61 | }, 62 | icon: 'clear-format', 63 | tooltip: t('editor.extensions.FormatClear.tooltip'), 64 | }, 65 | }; 66 | }, 67 | }; 68 | }, 69 | }); 70 | 71 | export default FormatClear; 72 | -------------------------------------------------------------------------------- /src/extensions/fullscreen.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import { Extension } from '@tiptap/core'; 3 | import FullscreenCommandButton from '@/components/MenuCommands/FullscreenCommandButton.vue'; 4 | 5 | const Fullscreen = Extension.create({ 6 | name: 'fullscreen', 7 | 8 | addOptions() { 9 | return { 10 | ...this.parent?.(), 11 | button({ editor }: { editor: Editor }) { 12 | return { 13 | component: FullscreenCommandButton, 14 | }; 15 | }, 16 | }; 17 | }, 18 | }); 19 | 20 | export default Fullscreen; 21 | -------------------------------------------------------------------------------- /src/extensions/heading.ts: -------------------------------------------------------------------------------- 1 | import type { Editor, Extension } from '@tiptap/core'; 2 | import type { HeadingOptions } from '@tiptap/extension-heading'; 3 | import TiptapHeading from '@tiptap/extension-heading'; 4 | import HeadingDropdown from '@/components/MenuCommands/HeadingDropdown.vue'; 5 | 6 | const Heading = TiptapHeading.extend({ 7 | addOptions() { 8 | return { 9 | ...this.parent?.(), 10 | button({ 11 | editor, 12 | extension, 13 | }: { 14 | editor: Editor; 15 | extension: Extension; 16 | t: (...args: any[]) => string; 17 | }) { 18 | return { 19 | component: HeadingDropdown, 20 | componentProps: { 21 | levels: (extension.options as HeadingOptions).levels, 22 | editor, 23 | }, 24 | }; 25 | }, 26 | }; 27 | }, 28 | }); 29 | 30 | export default Heading; 31 | -------------------------------------------------------------------------------- /src/extensions/highlight.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from '@tiptap/core'; 2 | import TiptapHighlight from '@tiptap/extension-highlight'; 3 | import { COLOR_SET } from '@/utils/color'; 4 | import HighlightPopover from '../components/MenuCommands/HighlightPopover.vue'; 5 | 6 | const Highlight = TiptapHighlight.extend({ 7 | addOptions() { 8 | return { 9 | ...this.parent?.(), 10 | multicolor: true, 11 | colors: COLOR_SET, 12 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 13 | return { 14 | component: HighlightPopover, 15 | componentProps: { 16 | editor, 17 | }, 18 | }; 19 | }, 20 | }; 21 | }, 22 | }); 23 | 24 | export default Highlight; 25 | -------------------------------------------------------------------------------- /src/extensions/history.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import TiptapHistory from '@tiptap/extension-history'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | 5 | const History = TiptapHistory.extend({ 6 | addOptions() { 7 | return { 8 | ...this.parent?.(), 9 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 10 | return [ 11 | { 12 | component: CommandButton, 13 | componentProps: { 14 | command: () => { 15 | editor.commands.undo(); 16 | }, 17 | 18 | icon: 'undo', 19 | tooltip: t('editor.extensions.History.tooltip.undo'), 20 | }, 21 | }, 22 | { 23 | component: CommandButton, 24 | componentProps: { 25 | command: () => { 26 | editor.commands.redo(); 27 | }, 28 | 29 | icon: 'redo', 30 | tooltip: t('editor.extensions.History.tooltip.redo'), 31 | }, 32 | }, 33 | ]; 34 | }, 35 | }; 36 | }, 37 | }); 38 | 39 | export default History; 40 | -------------------------------------------------------------------------------- /src/extensions/horizontal-rule.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import TiptapHorizontalRule from '@tiptap/extension-horizontal-rule'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | 5 | const HorizontalRule = TiptapHorizontalRule.extend({ 6 | addOptions() { 7 | return { 8 | ...this.parent?.(), 9 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 10 | return { 11 | component: CommandButton, 12 | componentProps: { 13 | command: () => { 14 | editor.commands.setHorizontalRule(); 15 | }, 16 | icon: 'horizontal-rule', 17 | tooltip: t('editor.extensions.HorizontalRule.tooltip'), 18 | }, 19 | }; 20 | }, 21 | }; 22 | }, 23 | }); 24 | 25 | export default HorizontalRule; 26 | -------------------------------------------------------------------------------- /src/extensions/iframe.ts: -------------------------------------------------------------------------------- 1 | import { Node, mergeAttributes } from '@tiptap/core'; 2 | import { Editor, VueNodeViewRenderer } from '@tiptap/vue-3'; 3 | import IframeCommandButton from '@/components/MenuCommands/IframeCommandButton.vue'; 4 | import IframeView from '@/components/ExtensionViews/IframeView.vue'; 5 | 6 | declare module '@tiptap/core' { 7 | interface Commands { 8 | iframe: { 9 | setIframe: (options: { src: string }) => ReturnType; 10 | }; 11 | } 12 | } 13 | 14 | const Iframe = Node.create({ 15 | name: 'iframe', 16 | 17 | // schema 18 | group: 'block', 19 | selectable: false, 20 | 21 | addAttributes() { 22 | return { 23 | ...this.parent?.(), 24 | src: { 25 | default: null, 26 | parseHTML: (element) => { 27 | const src = element.getAttribute('src'); 28 | return src; 29 | }, 30 | }, 31 | }; 32 | }, 33 | 34 | parseHTML() { 35 | return [ 36 | { 37 | tag: 'iframe', 38 | }, 39 | ]; 40 | }, 41 | 42 | renderHTML({ HTMLAttributes }) { 43 | return [ 44 | 'iframe', 45 | mergeAttributes(HTMLAttributes, { 46 | frameborder: 0, 47 | allowfullscreen: 'true', 48 | }), 49 | ]; 50 | }, 51 | 52 | addCommands() { 53 | return { 54 | setIframe: 55 | (options) => 56 | ({ commands }) => { 57 | return commands.insertContent({ 58 | type: this.name, 59 | attrs: options, 60 | }); 61 | }, 62 | }; 63 | }, 64 | 65 | addOptions() { 66 | return { 67 | button({ editor }: { editor: Editor }) { 68 | return { 69 | component: IframeCommandButton, 70 | componentProps: { 71 | editor, 72 | }, 73 | }; 74 | }, 75 | }; 76 | }, 77 | 78 | addNodeView() { 79 | return VueNodeViewRenderer(IframeView); 80 | }, 81 | }); 82 | 83 | export default Iframe; 84 | -------------------------------------------------------------------------------- /src/extensions/image.ts: -------------------------------------------------------------------------------- 1 | import { Editor, mergeAttributes } from '@tiptap/core'; 2 | import { VueNodeViewRenderer } from '@tiptap/vue-3'; 3 | import TiptapImage from '@tiptap/extension-image'; 4 | import InsertImageCommandButton from '@/components/MenuCommands/Image/InsertImageCommandButton.vue'; 5 | import ImageView from '@/components/ExtensionViews/ImageView.vue'; 6 | import { ImageDisplay } from '@/utils/image'; 7 | import { 8 | DEFAULT_IMAGE_WIDTH, 9 | DEFAULT_IMAGE_DISPLAY, 10 | DEFAULT_IMAGE_URL_REGEX, 11 | } from '@/constants'; 12 | 13 | const Image = TiptapImage.extend({ 14 | // https://github.com/ueberdosis/tiptap/issues/1206 15 | inline() { 16 | return true; 17 | }, 18 | 19 | group() { 20 | return 'inline'; 21 | }, 22 | 23 | addAttributes() { 24 | return { 25 | ...this.parent?.(), 26 | width: { 27 | default: DEFAULT_IMAGE_WIDTH, 28 | parseHTML: (element) => { 29 | const width = 30 | element.style.width || element.getAttribute('width') || null; 31 | return width == null ? null : parseInt(width, 10); 32 | }, 33 | renderHTML: (attributes) => { 34 | return { 35 | width: attributes.width, 36 | }; 37 | }, 38 | }, 39 | height: { 40 | default: null, 41 | parseHTML: (element) => { 42 | const height = 43 | element.style.height || element.getAttribute('height') || null; 44 | return height == null ? null : parseInt(height, 10); 45 | }, 46 | renderHTML: (attributes) => { 47 | return { 48 | height: attributes.height, 49 | }; 50 | }, 51 | }, 52 | display: { 53 | default: DEFAULT_IMAGE_DISPLAY, 54 | parseHTML: (element) => { 55 | const { cssFloat, display } = element.style; 56 | let dp = 57 | element.getAttribute('data-display') || 58 | element.getAttribute('display'); 59 | if (dp) { 60 | dp = /(inline|block|left|right)/.test(dp) 61 | ? dp 62 | : ImageDisplay.INLINE; 63 | } else if (cssFloat === 'left' && !display) { 64 | dp = ImageDisplay.FLOAT_LEFT; 65 | } else if (cssFloat === 'right' && !display) { 66 | dp = ImageDisplay.FLOAT_RIGHT; 67 | } else if (!cssFloat && display === 'block') { 68 | dp = ImageDisplay.BREAK_TEXT; 69 | } else { 70 | dp = ImageDisplay.INLINE; 71 | } 72 | 73 | return dp; 74 | }, 75 | renderHTML: (attributes) => { 76 | return { 77 | ['data-display']: attributes.display, 78 | }; 79 | }, 80 | }, 81 | }; 82 | }, 83 | 84 | addOptions() { 85 | return { 86 | ...this.parent?.(), 87 | inline: true, 88 | uploadRequest: null, 89 | urlPattern: DEFAULT_IMAGE_URL_REGEX, 90 | button({ editor }: { editor: Editor }) { 91 | return { 92 | component: InsertImageCommandButton, 93 | componentProps: { 94 | editor, 95 | }, 96 | }; 97 | }, 98 | }; 99 | }, 100 | 101 | addNodeView() { 102 | return VueNodeViewRenderer(ImageView); 103 | }, 104 | 105 | parseHTML() { 106 | return [ 107 | { 108 | tag: 'img[src]', 109 | }, 110 | ]; 111 | }, 112 | }); 113 | 114 | export default Image; 115 | -------------------------------------------------------------------------------- /src/extensions/indent.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '@tiptap/core'; 2 | import type { Editor } from '@tiptap/core'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | import { createIndentCommand, IndentProps } from '@/utils/indent'; 5 | 6 | export interface IndentOptions { 7 | types: string[]; 8 | minIndent: number; 9 | maxIndent: number; 10 | } 11 | 12 | declare module '@tiptap/core' { 13 | interface Commands { 14 | indent: { 15 | /** 16 | * Set the indent attribute 17 | */ 18 | indent: () => ReturnType; 19 | /** 20 | * Set the outdent attribute 21 | */ 22 | outdent: () => ReturnType; 23 | }; 24 | } 25 | } 26 | 27 | const Indent = Extension.create({ 28 | name: 'indent', 29 | 30 | addOptions() { 31 | return { 32 | types: ['paragraph', 'heading', 'blockquote'], 33 | minIndent: IndentProps.min, 34 | maxIndent: IndentProps.max, 35 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 36 | return [ 37 | { 38 | component: CommandButton, 39 | componentProps: { 40 | command: () => { 41 | editor.commands.indent(); 42 | }, 43 | icon: 'indent', 44 | tooltip: t('editor.extensions.Indent.buttons.indent.tooltip'), 45 | }, 46 | }, 47 | { 48 | component: CommandButton, 49 | componentProps: { 50 | command: () => { 51 | editor.commands.outdent(); 52 | }, 53 | icon: 'outdent', 54 | tooltip: t('editor.extensions.Indent.buttons.outdent.tooltip'), 55 | }, 56 | }, 57 | ]; 58 | }, 59 | }; 60 | }, 61 | 62 | addGlobalAttributes() { 63 | return [ 64 | { 65 | types: this.options.types, 66 | attributes: { 67 | indent: { 68 | default: 0, 69 | parseHTML: (element) => { 70 | const identAttr = element.getAttribute('data-indent'); 71 | return (identAttr ? parseInt(identAttr, 10) : 0) || 0; 72 | }, 73 | renderHTML: (attributes) => { 74 | if (!attributes.indent) { 75 | return {}; 76 | } 77 | 78 | return { ['data-indent']: attributes.indent }; 79 | }, 80 | }, 81 | }, 82 | }, 83 | ]; 84 | }, 85 | 86 | addCommands() { 87 | return { 88 | indent: () => 89 | createIndentCommand({ 90 | delta: IndentProps.more, 91 | types: this.options.types, 92 | }), 93 | outdent: () => 94 | createIndentCommand({ 95 | delta: IndentProps.less, 96 | types: this.options.types, 97 | }), 98 | }; 99 | }, 100 | 101 | addKeyboardShortcuts() { 102 | return { 103 | Tab: () => this.editor.commands.indent(), 104 | 'Shift-Tab': () => this.editor.commands.outdent(), 105 | }; 106 | }, 107 | }); 108 | 109 | export default Indent; 110 | -------------------------------------------------------------------------------- /src/extensions/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Text } from '@tiptap/extension-text'; 2 | 3 | // nodes 4 | export { default as Document } from './document'; 5 | export { default as Paragraph } from '@tiptap/extension-paragraph'; 6 | export { default as Heading } from './heading'; 7 | export { default as Blockquote } from './blockquote'; 8 | export { default as CodeBlock } from './code-block'; 9 | export { default as BulletList } from './bullet-list'; 10 | export { default as OrderedList } from './ordered-list'; 11 | export { default as Image } from './image'; 12 | export { default as TaskList } from './task-list'; 13 | export { default as Table } from './table'; 14 | export { default as Iframe } from './iframe'; 15 | 16 | // marks 17 | export { default as Bold } from './bold'; 18 | export { default as Underline } from './underline'; 19 | export { default as Italic } from './italic'; 20 | export { default as Strike } from './strike'; 21 | export { default as Link } from './link'; 22 | export { default as Color } from './color'; 23 | export { default as Highlight } from './highlight'; 24 | export { default as FontFamily } from './font-family'; 25 | export { default as FontSize } from './font-size'; 26 | export { default as Code } from '@tiptap/extension-code'; 27 | 28 | // extensions 29 | export { default as HardBreak } from '@tiptap/extension-hard-break'; 30 | export { default as HorizontalRule } from './horizontal-rule'; 31 | export { default as History } from './history'; 32 | export { default as TextAlign } from './text-align'; 33 | export { default as Indent } from './indent'; 34 | export { default as LineHeight } from './line-height'; 35 | export { default as FormatClear } from './format-clear'; 36 | export { default as Fullscreen } from './fullscreen'; 37 | export { default as Print } from './print'; 38 | export { default as SelectAll } from './select-all'; 39 | export { default as CodeView } from './code-view'; 40 | -------------------------------------------------------------------------------- /src/extensions/italic.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import TiptapItalic from '@tiptap/extension-italic'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | 5 | const Italic = TiptapItalic.extend({ 6 | addOptions() { 7 | return { 8 | ...this.parent?.(), 9 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 10 | return { 11 | component: CommandButton, 12 | componentProps: { 13 | command: () => { 14 | editor.commands.toggleItalic(); 15 | }, 16 | isActive: editor.isActive('italic'), 17 | icon: 'italic', 18 | tooltip: t('editor.extensions.Italic.tooltip'), 19 | }, 20 | }; 21 | }, 22 | }; 23 | }, 24 | }); 25 | 26 | export default Italic; 27 | -------------------------------------------------------------------------------- /src/extensions/line-height.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '@tiptap/core'; 2 | import type { Editor } from '@tiptap/core'; 3 | import { 4 | createLineHeightCommand, 5 | transformCSStoLineHeight, 6 | transformLineHeightToCSS, 7 | } from '@/utils/line-height'; 8 | import LineHeightDropdown from '@/components/MenuCommands/LineHeightDropdown.vue'; 9 | 10 | export interface LineHeightOptions { 11 | types: string[]; 12 | lineHeights: string[]; 13 | } 14 | 15 | declare module '@tiptap/core' { 16 | interface Commands { 17 | lineHeight: { 18 | setLineHeight: (lineHeight: string) => ReturnType; 19 | unsetLineHeight: () => ReturnType; 20 | }; 21 | } 22 | } 23 | 24 | const LineHeight = Extension.create({ 25 | name: 'lineHeight', 26 | 27 | addOptions() { 28 | return { 29 | types: ['paragraph', 'heading', 'list_item', 'todo_item'], 30 | lineHeights: ['100%', '115%', '150%', '200%', '250%', '300%'], 31 | button({ editor }: { editor: Editor }) { 32 | return { 33 | component: LineHeightDropdown, 34 | componentProps: { 35 | editor, 36 | }, 37 | }; 38 | }, 39 | }; 40 | }, 41 | 42 | addGlobalAttributes() { 43 | return [ 44 | { 45 | types: this.options.types, 46 | attributes: { 47 | lineHeight: { 48 | default: null, 49 | parseHTML: (element) => { 50 | return transformCSStoLineHeight(element.style.lineHeight) || null; 51 | }, 52 | renderHTML: (attributes) => { 53 | if (!attributes.lineHeight) { 54 | return {}; 55 | } 56 | 57 | const cssLineHeight = transformLineHeightToCSS( 58 | attributes.lineHeight 59 | ); 60 | 61 | return { 62 | style: `line-height: ${cssLineHeight};`, 63 | }; 64 | }, 65 | }, 66 | }, 67 | }, 68 | ]; 69 | }, 70 | 71 | addCommands() { 72 | return { 73 | setLineHeight: (lineHeight) => createLineHeightCommand(lineHeight), 74 | 75 | unsetLineHeight: 76 | () => 77 | ({ commands }) => { 78 | return this.options.types.every((type) => 79 | commands.resetAttributes(type, 'lineHeight') 80 | ); 81 | }, 82 | }; 83 | }, 84 | }); 85 | 86 | export default LineHeight; 87 | -------------------------------------------------------------------------------- /src/extensions/link.ts: -------------------------------------------------------------------------------- 1 | import { getMarkRange } from '@tiptap/core'; 2 | import type { Editor } from '@tiptap/core'; 3 | import TiptapLink from '@tiptap/extension-link'; 4 | import { Plugin, TextSelection } from '@tiptap/pm/state'; 5 | import { EditorView } from '@tiptap/pm/view'; 6 | import AddLinkCommandButton from '@/components/MenuCommands/Link/AddLinkCommandButton.vue'; 7 | 8 | const Link = TiptapLink.extend({ 9 | addOptions() { 10 | return { 11 | ...this.parent?.(), 12 | button({ editor }: { editor: Editor }) { 13 | return { 14 | component: AddLinkCommandButton, 15 | componentProps: { 16 | editor, 17 | }, 18 | }; 19 | }, 20 | }; 21 | }, 22 | 23 | addProseMirrorPlugins() { 24 | return [ 25 | new Plugin({ 26 | props: { 27 | handleClick(view: EditorView, pos: number) { 28 | const { schema, doc, tr } = view.state; 29 | 30 | const range = getMarkRange(doc.resolve(pos), schema.marks.link); 31 | 32 | if (!range) return false; 33 | 34 | const $start = doc.resolve(range.from); 35 | const $end = doc.resolve(range.to); 36 | 37 | const transaction = tr.setSelection( 38 | new TextSelection($start, $end) 39 | ); 40 | 41 | view.dispatch(transaction); 42 | return true; 43 | }, 44 | }, 45 | }), 46 | ]; 47 | }, 48 | }); 49 | 50 | export default Link; 51 | -------------------------------------------------------------------------------- /src/extensions/list-item.ts: -------------------------------------------------------------------------------- 1 | import TiptapListItem from '@tiptap/extension-list-item'; 2 | 3 | export default TiptapListItem; 4 | -------------------------------------------------------------------------------- /src/extensions/ordered-list.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import TiptapOrderedList from '@tiptap/extension-ordered-list'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | import ListItem from './list-item'; 5 | 6 | const OrderedList = TiptapOrderedList.extend({ 7 | addOptions() { 8 | return { 9 | ...this.parent?.(), 10 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 11 | return { 12 | component: CommandButton, 13 | componentProps: { 14 | command: () => { 15 | editor.commands.toggleOrderedList(); 16 | }, 17 | isActive: editor.isActive('orderedList'), 18 | icon: 'list-ol', 19 | tooltip: t('editor.extensions.OrderedList.tooltip'), 20 | }, 21 | }; 22 | }, 23 | }; 24 | }, 25 | 26 | addExtensions() { 27 | return [ListItem]; 28 | }, 29 | }); 30 | 31 | export default OrderedList; 32 | -------------------------------------------------------------------------------- /src/extensions/print.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '@tiptap/core'; 2 | import type { Editor } from '@tiptap/core'; 3 | import { printEditorContent } from '@/utils/print'; 4 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 5 | 6 | declare module '@tiptap/core' { 7 | interface Commands { 8 | print: { 9 | /** 10 | * print the editor content 11 | */ 12 | print: () => ReturnType; 13 | }; 14 | } 15 | } 16 | 17 | const Print = Extension.create({ 18 | name: 'print', 19 | 20 | addOptions() { 21 | return { 22 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 23 | return { 24 | component: CommandButton, 25 | componentProps: { 26 | command: () => { 27 | editor.commands.print(); 28 | }, 29 | icon: 'print', 30 | tooltip: t('editor.extensions.Print.tooltip'), 31 | }, 32 | }; 33 | }, 34 | }; 35 | }, 36 | 37 | addCommands() { 38 | return { 39 | print: 40 | () => 41 | ({ view }) => { 42 | return printEditorContent(view); 43 | }, 44 | }; 45 | }, 46 | 47 | addKeyboardShortcuts() { 48 | return { 49 | 'Mod-p': () => this.editor.commands.print(), 50 | }; 51 | }, 52 | }); 53 | 54 | export default Print; 55 | -------------------------------------------------------------------------------- /src/extensions/select-all.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import { Extension } from '@tiptap/core'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | 5 | const SelectAll = Extension.create({ 6 | name: 'selectAll', 7 | 8 | addOptions() { 9 | return { 10 | ...this.parent?.(), 11 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 12 | return { 13 | component: CommandButton, 14 | componentProps: { 15 | command: () => { 16 | editor.commands.selectAll(); 17 | }, 18 | icon: 'select-all', 19 | tooltip: t('editor.extensions.SelectAll.tooltip'), 20 | }, 21 | }; 22 | }, 23 | }; 24 | }, 25 | }); 26 | 27 | export default SelectAll; 28 | -------------------------------------------------------------------------------- /src/extensions/strike.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import TiptapStrike from '@tiptap/extension-strike'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | 5 | const Strike = TiptapStrike.extend({ 6 | addOptions() { 7 | return { 8 | ...this.parent?.(), 9 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 10 | return { 11 | component: CommandButton, 12 | componentProps: { 13 | command: () => { 14 | editor.commands.toggleStrike(); 15 | }, 16 | isActive: editor.isActive('strike'), 17 | icon: 'strikethrough', 18 | tooltip: t('editor.extensions.Strike.tooltip'), 19 | }, 20 | }; 21 | }, 22 | }; 23 | }, 24 | }); 25 | 26 | export default Strike; 27 | -------------------------------------------------------------------------------- /src/extensions/table.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import { Table as TiptapTable } from '@tiptap/extension-table'; 3 | import TableRow from '@tiptap/extension-table-row'; 4 | import TableHeader from '@tiptap/extension-table-header'; 5 | import TableCell from '@tiptap/extension-table-cell'; 6 | import TablePopover from '@/components/MenuCommands/TablePopover/index.vue'; 7 | 8 | const Table = TiptapTable.extend({ 9 | addOptions() { 10 | return { 11 | ...this.parent?.(), 12 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 13 | return { 14 | component: TablePopover, 15 | componentProps: { 16 | editor, 17 | }, 18 | }; 19 | }, 20 | }; 21 | }, 22 | 23 | addExtensions() { 24 | return [TableRow, TableHeader, TableCell]; 25 | }, 26 | }); 27 | 28 | export default Table; 29 | -------------------------------------------------------------------------------- /src/extensions/task-item.ts: -------------------------------------------------------------------------------- 1 | import { VueNodeViewRenderer } from '@tiptap/vue-3'; 2 | import { mergeAttributes } from '@tiptap/core'; 3 | import TiptapTaskItem from '@tiptap/extension-task-item'; 4 | import TaskItemView from '@/components/ExtensionViews/TaskItemView.vue'; 5 | 6 | const TaskItem = TiptapTaskItem.extend({ 7 | addAttributes() { 8 | return { 9 | ...this.parent?.(), 10 | done: { 11 | default: false, 12 | parseHTML: (element) => element.getAttribute('data-done') === 'true', 13 | }, 14 | }; 15 | }, 16 | 17 | renderHTML({ node, HTMLAttributes }) { 18 | const { done } = node.attrs; 19 | return [ 20 | 'li', 21 | mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { 22 | 'data-type': this.name, 23 | }), 24 | // el-checkbox dom 25 | [ 26 | 'span', 27 | { 28 | contenteditable: 'false', 29 | }, 30 | [ 31 | 'span', 32 | { 33 | class: `el-checkbox ${done ? 'is-checked' : ''}`, 34 | style: 'pointer-events: none;', 35 | }, 36 | [ 37 | 'span', 38 | { class: `el-checkbox__input ${done ? 'is-checked' : ''}` }, 39 | ['span', { class: 'el-checkbox__inner' }], 40 | ], 41 | ], 42 | ], 43 | ['div', { class: 'todo-content' }, 0], 44 | ]; 45 | }, 46 | 47 | addNodeView() { 48 | return VueNodeViewRenderer(TaskItemView); 49 | }, 50 | }); 51 | 52 | export default TaskItem; 53 | -------------------------------------------------------------------------------- /src/extensions/task-list.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import TiptapTaskList from '@tiptap/extension-task-list'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | import TaskItem from './task-item'; 5 | 6 | const TaskList = TiptapTaskList.extend({ 7 | addOptions() { 8 | return { 9 | ...this.parent?.(), 10 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 11 | return { 12 | component: CommandButton, 13 | componentProps: { 14 | command: () => { 15 | editor.commands.toggleTaskList(); 16 | }, 17 | isActive: editor.isActive('taskList'), 18 | icon: 'tasks', 19 | tooltip: t('editor.extensions.TodoList.tooltip'), 20 | }, 21 | }; 22 | }, 23 | }; 24 | }, 25 | 26 | addExtensions() { 27 | return [TaskItem]; 28 | }, 29 | }); 30 | 31 | export default TaskList; 32 | -------------------------------------------------------------------------------- /src/extensions/text-align.ts: -------------------------------------------------------------------------------- 1 | import type { Editor, Extension } from '@tiptap/core'; 2 | import TiptapTextAlign from '@tiptap/extension-text-align'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | 5 | const TextAlign = TiptapTextAlign.extend({ 6 | addOptions() { 7 | return { 8 | ...this.parent?.(), 9 | types: ['heading', 'paragraph', 'list_item', 'title'], 10 | button({ 11 | editor, 12 | extension, 13 | t, 14 | }: { 15 | editor: Editor; 16 | extension: Extension; 17 | t: (...args: any[]) => string; 18 | }) { 19 | return extension.options.alignments.reduce((acc, alignment) => { 20 | return acc.concat({ 21 | component: CommandButton, 22 | componentProps: { 23 | command: () => { 24 | if (editor.isActive({ textAlign: alignment })) { 25 | editor.commands.unsetTextAlign(); 26 | } else { 27 | editor.commands.setTextAlign(alignment); 28 | } 29 | }, 30 | isActive: 31 | alignment === 'left' 32 | ? false 33 | : editor.isActive({ textAlign: alignment }), 34 | icon: `align-${alignment}`, 35 | tooltip: t( 36 | `editor.extensions.TextAlign.buttons.align_${alignment}.tooltip` 37 | ), 38 | }, 39 | }); 40 | }, []); 41 | }, 42 | }; 43 | }, 44 | }); 45 | 46 | export default TextAlign; 47 | -------------------------------------------------------------------------------- /src/extensions/title.ts: -------------------------------------------------------------------------------- 1 | import { Node, mergeAttributes } from '@tiptap/core'; 2 | 3 | const Title = Node.create({ 4 | name: 'title', 5 | 6 | // schema 7 | content: 'inline*', 8 | 9 | addOptions() { 10 | return { 11 | ...this.parent?.(), 12 | placeholder: '', 13 | }; 14 | }, 15 | 16 | parseHTML() { 17 | return [{ tag: 'h1' }]; 18 | }, 19 | 20 | renderHTML({ HTMLAttributes }) { 21 | return ['h1', mergeAttributes(HTMLAttributes), 0]; 22 | }, 23 | }); 24 | 25 | export default Title; 26 | -------------------------------------------------------------------------------- /src/extensions/underline.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import TiptapUnderline from '@tiptap/extension-underline'; 3 | import CommandButton from '@/components/MenuCommands/CommandButton.vue'; 4 | 5 | const Underline = TiptapUnderline.extend({ 6 | addOptions() { 7 | return { 8 | ...this.parent?.(), 9 | button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) { 10 | return { 11 | component: CommandButton, 12 | componentProps: { 13 | command: () => { 14 | editor.commands.toggleUnderline(); 15 | }, 16 | isActive: editor.isActive('underline'), 17 | icon: 'underline', 18 | tooltip: t('editor.extensions.Underline.tooltip'), 19 | }, 20 | }; 21 | }, 22 | }; 23 | }, 24 | }); 25 | 26 | export default Underline; 27 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useCharacterCount } from './useCharacterCount'; 2 | export { default as useCodeView } from './useCodeView'; 3 | export { default as useEditorStyle } from './useEditorStyle'; 4 | -------------------------------------------------------------------------------- /src/hooks/useCharacterCount.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from '@tiptap/core'; 2 | import { computed, ShallowRef } from 'vue'; 3 | 4 | export default function useCharacterCount( 5 | editor: ShallowRef 6 | ) { 7 | const characters = computed(() => { 8 | return editor.value?.storage.characterCount.characters(); 9 | }); 10 | 11 | return { 12 | characters, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/hooks/useCodeView.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from '@tiptap/core'; 2 | import { ref, provide, watch, nextTick, unref, ShallowRef } from 'vue'; 3 | 4 | export default function useCodeView(editor: ShallowRef) { 5 | // Don't use ref or reactive, object proxy will encounter this problem 6 | // https://github.com/codemirror/codemirror5/issues/6805 7 | let cmEditor; 8 | const cmTextAreaRef = ref(); 9 | const isCodeViewMode = ref(false); 10 | const toggleIsCodeViewMode = (val: boolean) => { 11 | isCodeViewMode.value = val; 12 | }; 13 | 14 | const formatCode = (cm: any) => { 15 | cm.execCommand('selectAll'); 16 | const selectedRange = { 17 | from: cm.getCursor(true), 18 | to: cm.getCursor(false), 19 | }; 20 | cm.autoFormatRange(selectedRange.from, selectedRange.to); 21 | cm.setCursor(0); 22 | }; 23 | 24 | const initCodemirror = () => { 25 | const codeViewExtension = unref(editor)!.extensionManager.extensions.find( 26 | (e) => e.name === 'codeView' 27 | ); 28 | if (codeViewExtension) { 29 | const { codemirror, codemirrorOptions } = codeViewExtension.options; 30 | if (codemirror) { 31 | // merge options 32 | const cmOptions = { 33 | ...codemirrorOptions, 34 | readOnly: false, 35 | spellcheck: false, 36 | // readOnly: this.readonly, 37 | // spellcheck: this.spellcheckEnabled, 38 | }; 39 | cmEditor = codemirror.fromTextArea(cmTextAreaRef.value, cmOptions); 40 | cmEditor.setValue(unref(editor)!.getHTML()); // init content 41 | formatCode(cmEditor); 42 | } 43 | } 44 | }; 45 | 46 | const destroyCodemirror = () => { 47 | const element = cmEditor.doc.cm.getWrapperElement(); 48 | element && element.remove && element.remove(); 49 | cmEditor = null; 50 | }; 51 | 52 | watch(isCodeViewMode, (val: boolean) => { 53 | if (val) { 54 | nextTick(() => { 55 | if (!cmEditor) { 56 | initCodemirror(); 57 | } 58 | }); 59 | } else { 60 | // update editor content 61 | if (cmEditor) { 62 | const content = cmEditor.getValue(); 63 | unref(editor)!.commands.setContent(content, true /* emitUpdate */); 64 | destroyCodemirror(); 65 | } 66 | } 67 | }); 68 | 69 | provide('isCodeViewMode', isCodeViewMode); 70 | provide('toggleIsCodeViewMode', toggleIsCodeViewMode); 71 | 72 | return { 73 | cmTextAreaRef, 74 | isCodeViewMode, 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /src/hooks/useEditorStyle.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_EDITOR_SIZE_UNIT = 'px'; 2 | 3 | export default function useEditorStyle({ 4 | width, 5 | height, 6 | }: { 7 | width?: string | number; 8 | height?: string | number; 9 | }) { 10 | const editorSizeStyle = { 11 | width: isNaN(Number(width)) ? width : `${width}${DEFAULT_EDITOR_SIZE_UNIT}`, 12 | height: isNaN(Number(height)) 13 | ? height 14 | : `${height}${DEFAULT_EDITOR_SIZE_UNIT}`, 15 | }; 16 | 17 | return [editorSizeStyle]; 18 | } 19 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import en from './locales/en'; 2 | 3 | export const Trans = { 4 | buildI18nHandler(locale: Object = en): Function { 5 | return function t(path: string): string { 6 | const target = path.split('.').reduce((prev, curr) => { 7 | // @ts-ignore 8 | return prev[curr]; 9 | }, locale); 10 | 11 | return target as string; 12 | }; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/i18n/locales/ko/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | editor: { 3 | extensions: { 4 | Bold: { 5 | tooltip: '굵게', 6 | }, 7 | Underline: { 8 | tooltip: '밑줄', 9 | }, 10 | Italic: { 11 | tooltip: '기울임', 12 | }, 13 | Strike: { 14 | tooltip: '취소선', 15 | }, 16 | Heading: { 17 | tooltip: '문단 형식', 18 | buttons: { 19 | paragraph: '문단', 20 | heading: '제목', 21 | }, 22 | }, 23 | Blockquote: { 24 | tooltip: '인용', 25 | }, 26 | CodeBlock: { 27 | tooltip: '코드', 28 | }, 29 | Link: { 30 | add: { 31 | tooltip: '링크 추가', 32 | control: { 33 | title: '링크 추가', 34 | href: 'URL주소', 35 | open_in_new_tab: '새 탭에서 열기', 36 | confirm: '적용', 37 | cancel: '취소', 38 | }, 39 | }, 40 | edit: { 41 | tooltip: '링크 편집', 42 | control: { 43 | title: '링크 편집', 44 | href: 'URL주소', 45 | open_in_new_tab: '새 탭에서 열기', 46 | confirm: '적용', 47 | cancel: '취소', 48 | }, 49 | }, 50 | unlink: { 51 | tooltip: '링크 제거', 52 | }, 53 | open: { 54 | tooltip: '링크 열기', 55 | }, 56 | }, 57 | Image: { 58 | buttons: { 59 | insert_image: { 60 | tooltip: '이미지 추가', 61 | external: '이미지 URL을 입력하세요.', 62 | upload: '이미지 업로드', 63 | }, 64 | remove_image: { 65 | tooltip: '제거', 66 | }, 67 | image_options: { 68 | tooltip: '이미지 옵션', 69 | }, 70 | display: { 71 | tooltip: '표시', 72 | inline: '인라인', 73 | block: '새줄', 74 | left: '좌측 정렬', 75 | right: '우측 정렬', 76 | }, 77 | }, 78 | control: { 79 | insert_by_url: { 80 | title: '이미지 추가', 81 | placeholder: '이미지 URL', 82 | confirm: '추가', 83 | cancel: '취소', 84 | invalid_url: '정확한 URL을 입력하세요', 85 | }, 86 | upload_image: { 87 | title: '이미지 업로드', 88 | button: '이미지 파일을 선택하거나 끌어넣기 하세요', 89 | }, 90 | edit_image: { 91 | title: '이미지 편집', 92 | confirm: '적용', 93 | cancel: '취소', 94 | form: { 95 | src: '이미지 URL', 96 | alt: '대체 텍스트', 97 | width: '너비', 98 | height: '높이', 99 | }, 100 | }, 101 | }, 102 | }, 103 | Iframe: { 104 | tooltip: '비디오 추가', 105 | control: { 106 | title: '비디오 추가', 107 | placeholder: '비디오 URL', 108 | confirm: '추가', 109 | cancel: '취소', 110 | }, 111 | }, 112 | BulletList: { 113 | tooltip: '비번호 목록', 114 | }, 115 | OrderedList: { 116 | tooltip: '번호 목록', 117 | }, 118 | TodoList: { 119 | tooltip: '할일 목록', 120 | }, 121 | TextAlign: { 122 | buttons: { 123 | align_left: { 124 | tooltip: '좌측 정렬', 125 | }, 126 | align_center: { 127 | tooltip: '중앙 정렬', 128 | }, 129 | align_right: { 130 | tooltip: '우측 정렬', 131 | }, 132 | align_justify: { 133 | tooltip: '좌우 정렬', 134 | }, 135 | }, 136 | }, 137 | FontType: { 138 | tooltip: '폰트', 139 | }, 140 | FontSize: { 141 | tooltip: '글자 크기', 142 | default: '기본', 143 | }, 144 | TextColor: { 145 | tooltip: '글자 색', 146 | }, 147 | TextHighlight: { 148 | tooltip: '글자 강조', 149 | }, 150 | LineHeight: { 151 | tooltip: '줄 높이', 152 | }, 153 | Table: { 154 | tooltip: '테이블', 155 | buttons: { 156 | insert_table: '테이블 추가', 157 | add_column_before: '이전에 열 추가', 158 | add_column_after: '이후에 열 추가', 159 | delete_column: '열 삭제', 160 | add_row_before: '이전에 줄 추가', 161 | add_row_after: '이후에 줄 추가', 162 | delete_row: '줄 삭제', 163 | merge_cells: '셀 병합', 164 | split_cell: '셀 분할', 165 | delete_table: '테이블 삭제', 166 | }, 167 | }, 168 | Indent: { 169 | buttons: { 170 | indent: { 171 | tooltip: '들여 쓰기', 172 | }, 173 | outdent: { 174 | tooltip: '내어 쓰기', 175 | }, 176 | }, 177 | }, 178 | FormatClear: { 179 | tooltip: '형식 지우기', 180 | }, 181 | HorizontalRule: { 182 | tooltip: '가로 줄', 183 | }, 184 | History: { 185 | tooltip: { 186 | undo: '되돌리기', 187 | redo: '다시 실행', 188 | }, 189 | }, 190 | Fullscreen: { 191 | tooltip: { 192 | fullscreen: '전체화면', 193 | exit_fullscreen: '전체화면 나가기', 194 | }, 195 | }, 196 | Print: { 197 | tooltip: '인쇄', 198 | }, 199 | Preview: { 200 | tooltip: '미리보기', 201 | dialog: { 202 | title: '미리보기', 203 | }, 204 | }, 205 | SelectAll: { 206 | tooltip: '전체선택', 207 | }, 208 | CodeView: { 209 | tooltip: '코드 뷰', 210 | }, 211 | }, 212 | characters: '문자수', 213 | }, 214 | }; 215 | -------------------------------------------------------------------------------- /src/i18n/locales/zh-tw/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | editor: { 3 | extensions: { 4 | Bold: { 5 | tooltip: '粗體', 6 | }, 7 | Underline: { 8 | tooltip: '底線', 9 | }, 10 | Italic: { 11 | tooltip: '斜體', 12 | }, 13 | Strike: { 14 | tooltip: '刪除線', 15 | }, 16 | Heading: { 17 | tooltip: '標題', 18 | buttons: { 19 | paragraph: '段落', 20 | heading: '標題', 21 | } 22 | }, 23 | Blockquote: { 24 | tooltip: '引用', 25 | }, 26 | CodeBlock: { 27 | tooltip: '程式碼', 28 | }, 29 | Link: { 30 | add: { 31 | tooltip: '新增超連結', 32 | control: { 33 | title: '新增超連結', 34 | href: '超連結', 35 | open_in_new_tab: '在新分頁開啟', 36 | confirm: '新增', 37 | cancel: '取消', 38 | }, 39 | }, 40 | edit: { 41 | tooltip: '編輯超連結', 42 | control: { 43 | title: '編輯超連結', 44 | href: '超連結', 45 | open_in_new_tab: '在新分頁開啟', 46 | confirm: '更新', 47 | cancel: '取消', 48 | }, 49 | }, 50 | unlink: { 51 | tooltip: '取消超連結', 52 | }, 53 | open: { 54 | tooltip: '打開超連結', 55 | }, 56 | }, 57 | Image: { 58 | buttons: { 59 | insert_image: { 60 | tooltip: '新增圖片', 61 | external: '新增網路圖片', 62 | upload: '上傳本機圖片', 63 | }, 64 | remove_image: { 65 | tooltip: '刪除', 66 | }, 67 | image_options: { 68 | tooltip: '圖片屬性', 69 | }, 70 | display: { 71 | tooltip: '佈局', 72 | inline: '内聯', 73 | block: '塊', 74 | left: '左浮動', 75 | right: '右浮動', 76 | }, 77 | }, 78 | control: { 79 | insert_by_url: { 80 | title: '新增網路圖片', 81 | placeholder: '輸入超連結', 82 | confirm: '新增', 83 | cancel: '取消', 84 | invalid_url: '請輸入正確的圖片連結', 85 | }, 86 | upload_image: { 87 | title: '上傳本機圖片', 88 | button: '將圖片文件拖到此處或者點擊上傳', 89 | }, 90 | edit_image: { 91 | title: '編輯圖片', 92 | confirm: '更新', 93 | cancel: '取消', 94 | form: { 95 | src: '圖片連結', 96 | alt: '替代文字', 97 | width: '寬度', 98 | height: '高度', 99 | }, 100 | }, 101 | }, 102 | }, 103 | Iframe: { 104 | tooltip: '新增影片', 105 | control: { 106 | title: '新增影片', 107 | placeholder: '輸入超連結', 108 | confirm: '確認', 109 | cancel: '取消', 110 | }, 111 | }, 112 | BulletList: { 113 | tooltip: '無序列表', 114 | }, 115 | OrderedList: { 116 | tooltip: '有序列表', 117 | }, 118 | TodoList: { 119 | tooltip: '任務列表', 120 | }, 121 | TextAlign: { 122 | buttons: { 123 | align_left: { 124 | tooltip: '置左', 125 | }, 126 | align_center: { 127 | tooltip: '置中', 128 | }, 129 | align_right: { 130 | tooltip: '置右', 131 | }, 132 | align_justify: { 133 | tooltip: '水平對齊', 134 | }, 135 | }, 136 | }, 137 | FontType: { 138 | tooltip: '字體', 139 | }, 140 | FontSize: { 141 | tooltip: '字體大小', 142 | default: '默認', 143 | }, 144 | TextColor: { 145 | tooltip: '文字顏色', 146 | }, 147 | TextHighlight: { 148 | tooltip: '文字反白', 149 | }, 150 | LineHeight: { 151 | tooltip: '行距', 152 | }, 153 | Table: { 154 | tooltip: '表格', 155 | buttons: { 156 | insert_table: '新增表格', 157 | add_column_before: '向左新增一列', 158 | add_column_after: '向右新增一列', 159 | delete_column: '刪除列', 160 | add_row_before: '向上新增一行', 161 | add_row_after: '向下新增一行', 162 | delete_row: '删除行', 163 | merge_cells: '合併', 164 | split_cell: '分離儲存格', 165 | delete_table: '删除表格', 166 | }, 167 | }, 168 | Indent: { 169 | buttons: { 170 | indent: { 171 | tooltip: '增加縮排', 172 | }, 173 | outdent: { 174 | tooltip: '减少縮排', 175 | }, 176 | }, 177 | }, 178 | FormatClear: { 179 | tooltip: '清除格式', 180 | }, 181 | HorizontalRule: { 182 | tooltip: '分隔線', 183 | }, 184 | History: { 185 | tooltip: { 186 | undo: '復原', 187 | redo: '取消復原', 188 | }, 189 | }, 190 | Fullscreen: { 191 | tooltip: { 192 | fullscreen: '全螢幕', 193 | exit_fullscreen: '退出全螢幕', 194 | }, 195 | }, 196 | Print: { 197 | tooltip: '列印', 198 | }, 199 | Preview: { 200 | tooltip: '預覽', 201 | dialog: { 202 | title: '預覽', 203 | }, 204 | }, 205 | SelectAll: { 206 | tooltip: '全選', 207 | }, 208 | CodeView: { 209 | tooltip: '查看原始碼', 210 | }, 211 | }, 212 | characters: '字數', 213 | }, 214 | }; 215 | -------------------------------------------------------------------------------- /src/i18n/locales/zh/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | editor: { 3 | extensions: { 4 | Bold: { 5 | tooltip: '粗体', 6 | }, 7 | Underline: { 8 | tooltip: '下划线', 9 | }, 10 | Italic: { 11 | tooltip: '斜体', 12 | }, 13 | Strike: { 14 | tooltip: '中划线', 15 | }, 16 | Heading: { 17 | tooltip: '标题', 18 | buttons: { 19 | paragraph: '正文', 20 | heading: '标题', 21 | } 22 | }, 23 | Blockquote: { 24 | tooltip: '引用', 25 | }, 26 | CodeBlock: { 27 | tooltip: '代码块', 28 | }, 29 | Link: { 30 | add: { 31 | tooltip: '添加链接', 32 | control: { 33 | title: '添加链接', 34 | href: '链接', 35 | open_in_new_tab: '在新标签页中打开', 36 | confirm: '添加', 37 | cancel: '取消', 38 | }, 39 | }, 40 | edit: { 41 | tooltip: '编辑链接', 42 | control: { 43 | title: '编辑链接', 44 | href: '链接', 45 | open_in_new_tab: '在新标签页中打开', 46 | confirm: '更新', 47 | cancel: '取消', 48 | }, 49 | }, 50 | unlink: { 51 | tooltip: '取消链接', 52 | }, 53 | open: { 54 | tooltip: '打开链接', 55 | }, 56 | }, 57 | Image: { 58 | buttons: { 59 | insert_image: { 60 | tooltip: '插入图片', 61 | external: '插入网络图片', 62 | upload: '上传本地图片', 63 | }, 64 | remove_image: { 65 | tooltip: '删除', 66 | }, 67 | image_options: { 68 | tooltip: '图片属性', 69 | }, 70 | display: { 71 | tooltip: '布局', 72 | inline: '内联', 73 | block: '断行', 74 | left: '左浮动', 75 | right: '右浮动', 76 | }, 77 | }, 78 | control: { 79 | insert_by_url: { 80 | title: '插入网络图片', 81 | placeholder: '输入链接', 82 | confirm: '插入', 83 | cancel: '取消', 84 | invalid_url: '请输入正确的图片链接', 85 | }, 86 | upload_image: { 87 | title: '上传本地图片', 88 | button: '将图片文件拖到此处或者点击上传', 89 | }, 90 | edit_image: { 91 | title: '编辑图片', 92 | confirm: '更新', 93 | cancel: '取消', 94 | form: { 95 | src: '图片链接', 96 | alt: '备用文本描述', 97 | width: '宽度', 98 | height: '高度', 99 | }, 100 | }, 101 | }, 102 | }, 103 | Iframe: { 104 | tooltip: '插入视频', 105 | control: { 106 | title: '插入视频', 107 | placeholder: '输入链接', 108 | confirm: '插入', 109 | cancel: '取消', 110 | }, 111 | }, 112 | BulletList: { 113 | tooltip: '无序列表', 114 | }, 115 | OrderedList: { 116 | tooltip: '有序列表', 117 | }, 118 | TodoList: { 119 | tooltip: '任务列表', 120 | }, 121 | TextAlign: { 122 | buttons: { 123 | align_left: { 124 | tooltip: '左对齐', 125 | }, 126 | align_center: { 127 | tooltip: '居中对齐', 128 | }, 129 | align_right: { 130 | tooltip: '右对齐', 131 | }, 132 | align_justify: { 133 | tooltip: '两端对齐', 134 | }, 135 | }, 136 | }, 137 | FontType: { 138 | tooltip: '字体', 139 | }, 140 | FontSize: { 141 | tooltip: '字号', 142 | default: '默认', 143 | }, 144 | TextColor: { 145 | tooltip: '文本颜色', 146 | }, 147 | TextHighlight: { 148 | tooltip: '文本高亮', 149 | }, 150 | LineHeight: { 151 | tooltip: '行距', 152 | }, 153 | Table: { 154 | tooltip: '表格', 155 | buttons: { 156 | insert_table: '插入表格', 157 | add_column_before: '向左插入一列', 158 | add_column_after: '向右插入一列', 159 | delete_column: '删除列', 160 | add_row_before: '向上插入一行', 161 | add_row_after: '向下插入一行', 162 | delete_row: '删除行', 163 | merge_cells: '合并单元格', 164 | split_cell: '拆分单元格', 165 | delete_table: '删除表格', 166 | }, 167 | }, 168 | Indent: { 169 | buttons: { 170 | indent: { 171 | tooltip: '增加缩进', 172 | }, 173 | outdent: { 174 | tooltip: '减少缩进', 175 | }, 176 | }, 177 | }, 178 | FormatClear: { 179 | tooltip: '清除格式', 180 | }, 181 | HorizontalRule: { 182 | tooltip: '分隔线', 183 | }, 184 | History: { 185 | tooltip: { 186 | undo: '撤销', 187 | redo: '重做', 188 | }, 189 | }, 190 | Fullscreen: { 191 | tooltip: { 192 | fullscreen: '全屏', 193 | exit_fullscreen: '退出全屏', 194 | }, 195 | }, 196 | Print: { 197 | tooltip: '打印', 198 | }, 199 | Preview: { 200 | tooltip: '预览', 201 | dialog: { 202 | title: '预览', 203 | }, 204 | }, 205 | SelectAll: { 206 | tooltip: '全选', 207 | }, 208 | CodeView: { 209 | tooltip: '查看源代码', 210 | }, 211 | }, 212 | characters: '字数', 213 | }, 214 | }; 215 | -------------------------------------------------------------------------------- /src/icons/align-center.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/align-justify.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/align-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/align-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/bold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/clear-format.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/compress.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/ellipsis-h.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/expand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/external-link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/file-code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/font-color.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/font-family.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/font-size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/heading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/highlight.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/horizontal-rule.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/image-align.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/indent.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/italic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/list-ol.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/list-ul.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/outdent.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/print.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/quote-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/redo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/select-all.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/strikethrough.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/tasks.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/text-height.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/trash-alt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/underline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/undo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/unlink.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/video.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'vue'; 2 | import ElementTiptap from '@/components/ElementTiptap.vue'; 3 | 4 | const ElementTiptapPlugin: Plugin = { 5 | install(app) { 6 | app.component('element-tiptap', ElementTiptap); 7 | app.component('el-tiptap', ElementTiptap); 8 | }, 9 | }; 10 | 11 | export * from '@/extensions'; 12 | 13 | export { ElementTiptapPlugin, ElementTiptap }; 14 | 15 | export default ElementTiptapPlugin; 16 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $black-color: #000; 2 | $white-color: #fff; 3 | $light-background-color: #f5f7fA; 4 | 5 | $primary-color: #409eff; 6 | $light-primary-color: #b3d8ff; 7 | $lighter-primary-color: #d9ecff; 8 | $extra-light-primary-color: #ecf5ff; 9 | 10 | $danger-color: #f56c6c; 11 | 12 | $primary-text-color: #303133; 13 | $regular-text-color: #606266; 14 | $placeholder-text-color: #c0c4cc; 15 | 16 | $border-color: #dcdfe6; 17 | $lighter-border-color: #ebeef5; 18 | -------------------------------------------------------------------------------- /src/utils/code-view.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // https://github.com/artf/codemirror-formatting/blob/master/formatting.js 3 | 4 | const inlineElements = /^(a|abbr|acronym|area|base|bdo|big|br|button|caption|cite|code|col|colgroup|dd|del|dfn|em|frame|hr|iframe|img|input|ins|kbd|label|legend|link|map|object|optgroup|option|param|q|samp|script|select|small|span|strong|sub|sup|textarea|tt|var)$/; 5 | 6 | // for format code 7 | export function extendCodemirror(CodeMirror) { 8 | CodeMirror.extendMode('xml', { 9 | newlineAfterToken: function(type, content, textAfter, state) { 10 | let inline = false; 11 | if (this.configuration === 'html') { 12 | inline = state.context ? inlineElements.test(state.context.tagName) : false; 13 | } 14 | return !inline && ((type === 'tag' && />$/.test(content) && state.context) || 15 | /^ { 17 | obj[type] = type; 18 | return obj; 19 | }, 20 | {} 21 | ); 22 | -------------------------------------------------------------------------------- /src/utils/image.ts: -------------------------------------------------------------------------------- 1 | type ImageResult = { 2 | complete: boolean; 3 | width: number; 4 | height: number; 5 | src: string; 6 | }; 7 | 8 | interface ImageCache { 9 | [key: string]: ImageResult; 10 | } 11 | 12 | const IMAGE_CACHE: ImageCache = {}; 13 | 14 | export function resolveImg(src: string): Promise { 15 | return new Promise((resolve, reject) => { 16 | const result: ImageResult = { 17 | complete: false, 18 | width: 0, 19 | height: 0, 20 | src, 21 | }; 22 | 23 | if (!src) { 24 | reject(result); 25 | return; 26 | } 27 | 28 | if (IMAGE_CACHE[src]) { 29 | resolve({ ...IMAGE_CACHE[src] }); 30 | return; 31 | } 32 | 33 | const img = new Image(); 34 | 35 | img.onload = () => { 36 | result.width = img.width; 37 | result.height = img.height; 38 | result.complete = true; 39 | 40 | IMAGE_CACHE[src] = { ...result }; 41 | resolve(result); 42 | }; 43 | 44 | img.onerror = () => { 45 | reject(result); 46 | }; 47 | 48 | img.src = src; 49 | }); 50 | } 51 | 52 | export const enum ImageDisplay { 53 | INLINE = 'inline', 54 | BREAK_TEXT = 'block', 55 | FLOAT_LEFT = 'left', 56 | FLOAT_RIGHT = 'right', 57 | }; 58 | -------------------------------------------------------------------------------- /src/utils/indent.ts: -------------------------------------------------------------------------------- 1 | import type { Command, Editor } from '@tiptap/core'; 2 | import { isList } from '@tiptap/core'; 3 | import { TextSelection, AllSelection, Transaction } from '@tiptap/pm/state'; 4 | import { clamp } from './shared'; 5 | 6 | export const enum IndentProps { 7 | max = 7, 8 | min = 0, 9 | 10 | more = 1, 11 | less = -1, 12 | } 13 | 14 | function updateIndentLevel( 15 | tr: Transaction, 16 | delta: number, 17 | types: string[], 18 | editor: Editor 19 | ): Transaction { 20 | const { doc, selection } = tr; 21 | 22 | if (!doc || !selection) return tr; 23 | 24 | if ( 25 | !(selection instanceof TextSelection || selection instanceof AllSelection) 26 | ) { 27 | return tr; 28 | } 29 | 30 | const { from, to } = selection; 31 | 32 | doc.nodesBetween(from, to, (node, pos) => { 33 | const nodeType = node.type; 34 | 35 | if (types.includes(nodeType.name)) { 36 | tr = setNodeIndentMarkup(tr, pos, delta); 37 | return false; 38 | } else if (isList(node.type.name, editor.extensionManager.extensions)) { 39 | return false; 40 | } 41 | return true; 42 | }); 43 | 44 | return tr; 45 | } 46 | 47 | function setNodeIndentMarkup( 48 | tr: Transaction, 49 | pos: number, 50 | delta: number 51 | ): Transaction { 52 | if (!tr.doc) return tr; 53 | 54 | const node = tr.doc.nodeAt(pos); 55 | if (!node) return tr; 56 | 57 | const minIndent = IndentProps.min; 58 | const maxIndent = IndentProps.max; 59 | 60 | const indent = clamp((node.attrs.indent || 0) + delta, minIndent, maxIndent); 61 | 62 | if (indent === node.attrs.indent) return tr; 63 | 64 | const nodeAttrs = { 65 | ...node.attrs, 66 | indent, 67 | }; 68 | 69 | return tr.setNodeMarkup(pos, node.type, nodeAttrs, node.marks); 70 | } 71 | 72 | export function createIndentCommand({ 73 | delta, 74 | types, 75 | }: { 76 | delta: number; 77 | types: string[]; 78 | }): Command { 79 | return ({ state, dispatch, editor }) => { 80 | const { selection } = state; 81 | let { tr } = state; 82 | tr = tr.setSelection(selection); 83 | tr = updateIndentLevel(tr, delta, types, editor); 84 | 85 | if (tr.docChanged) { 86 | dispatch && dispatch(tr); 87 | return true; 88 | } 89 | 90 | return false; 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /src/utils/line-height.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TextSelection, 3 | AllSelection, 4 | EditorState, 5 | Transaction, 6 | } from '@tiptap/pm/state'; 7 | import { Node as ProsemirrorNode, NodeType } from '@tiptap/pm/model'; 8 | import { LINE_HEIGHT_100, DEFAULT_LINE_HEIGHT } from '@/constants'; 9 | import type { Command } from '@tiptap/core'; 10 | 11 | export const ALLOWED_NODE_TYPES = [ 12 | 'paragraph', 13 | 'heading', 14 | 'list_item', 15 | 'todo_item', 16 | ]; 17 | 18 | const NUMBER_VALUE_PATTERN = /^\d+(.\d+)?$/; 19 | 20 | export function isLineHeightActive( 21 | state: EditorState, 22 | lineHeight: string 23 | ): boolean { 24 | const { selection, doc } = state; 25 | const { from, to } = selection; 26 | 27 | let keepLooking = true; 28 | let active = false; 29 | 30 | doc.nodesBetween(from, to, (node) => { 31 | const nodeType = node.type; 32 | const lineHeightValue = node.attrs.lineHeight || DEFAULT_LINE_HEIGHT; 33 | 34 | if (ALLOWED_NODE_TYPES.includes(nodeType.name)) { 35 | if (keepLooking && lineHeight === lineHeightValue) { 36 | keepLooking = false; 37 | active = true; 38 | 39 | return false; 40 | } 41 | return nodeType.name !== 'list_item' && nodeType.name !== 'todo_item'; 42 | } 43 | return keepLooking; 44 | }); 45 | 46 | return active; 47 | } 48 | 49 | export function transformLineHeightToCSS(value: string | number): string { 50 | if (!value) return ''; 51 | 52 | let strValue = String(value); 53 | 54 | if (NUMBER_VALUE_PATTERN.test(strValue)) { 55 | const numValue = parseFloat(strValue); 56 | strValue = String(Math.round(numValue * 100)) + '%'; 57 | } 58 | 59 | return parseFloat(strValue) * LINE_HEIGHT_100 + '%'; 60 | } 61 | 62 | export function transformCSStoLineHeight(value: string): string { 63 | if (!value) return ''; 64 | if (value === DEFAULT_LINE_HEIGHT) return ''; 65 | 66 | let strValue = value; 67 | 68 | if (NUMBER_VALUE_PATTERN.test(value)) { 69 | const numValue = parseFloat(value); 70 | strValue = String(Math.round(numValue * 100)) + '%'; 71 | if (strValue === DEFAULT_LINE_HEIGHT) return ''; 72 | } 73 | 74 | return parseFloat(strValue) / LINE_HEIGHT_100 + '%'; 75 | } 76 | 77 | interface SetLineHeightTask { 78 | node: ProsemirrorNode; 79 | nodeType: NodeType; 80 | pos: number; 81 | } 82 | 83 | export function setTextLineHeight( 84 | tr: Transaction, 85 | lineHeight: string | null 86 | ): Transaction { 87 | const { selection, doc } = tr; 88 | 89 | if (!selection || !doc) return tr; 90 | 91 | if ( 92 | !(selection instanceof TextSelection || selection instanceof AllSelection) 93 | ) { 94 | return tr; 95 | } 96 | 97 | const { from, to } = selection; 98 | 99 | const tasks: Array = []; 100 | const lineHeightValue = 101 | lineHeight && lineHeight !== DEFAULT_LINE_HEIGHT ? lineHeight : null; 102 | 103 | doc.nodesBetween(from, to, (node, pos) => { 104 | const nodeType = node.type; 105 | if (ALLOWED_NODE_TYPES.includes(nodeType.name)) { 106 | const lineHeight = node.attrs.lineHeight || null; 107 | if (lineHeight !== lineHeightValue) { 108 | tasks.push({ 109 | node, 110 | pos, 111 | nodeType, 112 | }); 113 | } 114 | return nodeType.name !== 'list_item' && nodeType.name !== 'todo_item'; 115 | } 116 | return true; 117 | }); 118 | 119 | if (!tasks.length) return tr; 120 | 121 | tasks.forEach((task) => { 122 | const { node, pos, nodeType } = task; 123 | let { attrs } = node; 124 | 125 | attrs = { 126 | ...attrs, 127 | lineHeight: lineHeightValue, 128 | }; 129 | 130 | tr = tr.setNodeMarkup(pos, nodeType, attrs, node.marks); 131 | }); 132 | 133 | return tr; 134 | } 135 | 136 | export function createLineHeightCommand(lineHeight: string): Command { 137 | return ({ state, dispatch }) => { 138 | const { selection } = state; 139 | let { tr } = state; 140 | tr = tr.setSelection(selection); 141 | 142 | tr = setTextLineHeight(tr, lineHeight); 143 | 144 | if (tr.docChanged) { 145 | dispatch && dispatch(tr); 146 | return true; 147 | } 148 | 149 | return false; 150 | }; 151 | } 152 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { ELEMENT_TIPTAP_TIP } from '@/constants'; 2 | 3 | export default class Logger { 4 | static warn(msg: string) { 5 | console.warn(`${ELEMENT_TIPTAP_TIP} ${msg}`); 6 | } 7 | 8 | static error(msg: string) { 9 | console.error(`${ELEMENT_TIPTAP_TIP} ${msg}`); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/print.ts: -------------------------------------------------------------------------------- 1 | import { EditorView } from '@tiptap/pm/view'; 2 | import Logger from './logger'; 3 | 4 | function printHtml(dom: Element) { 5 | const style: string = Array.from( 6 | document.querySelectorAll('style, link') 7 | ).reduce((str, style) => str + style.outerHTML, ''); 8 | 9 | const content: string = style + dom.outerHTML; 10 | 11 | const iframe: HTMLIFrameElement = document.createElement('iframe'); 12 | iframe.id = 'el-tiptap-iframe'; 13 | iframe.setAttribute( 14 | 'style', 15 | 'position: absolute; width: 0; height: 0; top: -10px; left: -10px;' 16 | ); 17 | document.body.appendChild(iframe); 18 | 19 | const frameWindow = iframe.contentWindow; 20 | const doc = 21 | iframe.contentDocument || 22 | (iframe.contentWindow && iframe.contentWindow.document); 23 | 24 | if (doc) { 25 | doc.open(); 26 | doc.write(content); 27 | doc.close(); 28 | } 29 | 30 | if (frameWindow) { 31 | iframe.onload = function () { 32 | try { 33 | setTimeout(() => { 34 | frameWindow.focus(); 35 | try { 36 | if (!frameWindow.document.execCommand('print', false)) { 37 | frameWindow.print(); 38 | } 39 | } catch (e) { 40 | frameWindow.print(); 41 | } 42 | frameWindow.close(); 43 | }, 10); 44 | } catch (err) { 45 | Logger.error(err); 46 | } 47 | 48 | setTimeout(function () { 49 | document.body.removeChild(iframe); 50 | }, 100); 51 | }; 52 | } 53 | } 54 | 55 | export function printEditorContent(view: EditorView) { 56 | const editorContent = view.dom.closest('.el-tiptap-editor__content'); 57 | if (editorContent) { 58 | printHtml(editorContent); 59 | return true; 60 | } 61 | return false; 62 | } 63 | -------------------------------------------------------------------------------- /src/utils/shared.ts: -------------------------------------------------------------------------------- 1 | export function noop(): void {} 2 | 3 | /** 4 | * Check whether a value is NaN 5 | */ 6 | export function isNaN(val: any) { 7 | return Number.isNaN(val); 8 | } 9 | 10 | export function clamp(val: number, min: number, max: number): number { 11 | if (val < min) { 12 | return min; 13 | } 14 | if (val > max) { 15 | return max; 16 | } 17 | return val; 18 | } 19 | 20 | export function readFileDataUrl(file: File): Promise { 21 | const reader = new FileReader(); 22 | 23 | return new Promise((resolve, reject) => { 24 | // @ts-ignore 25 | reader.onload = readerEvent => resolve(readerEvent.target.result); 26 | reader.onerror = reject; 27 | 28 | reader.readAsDataURL(file); 29 | }); 30 | } 31 | 32 | /** 33 | * Create a cached version of a pure function. 34 | */ 35 | export function cached(fn: Function): Function { 36 | const cache = Object.create(null); 37 | 38 | return function cachedFn(str: string): string { 39 | const hit = cache[str]; 40 | return hit || (cache[str] = fn(str)); 41 | }; 42 | } 43 | 44 | /** 45 | * Capitalize a string. 46 | */ 47 | export const capitalize = cached((str: string): string => { 48 | return str.charAt(0).toUpperCase() + str.slice(1); 49 | }); 50 | 51 | /** 52 | * Strict object type check. Only returns true 53 | * for plain JavaScript objects. 54 | */ 55 | export function isPlainObject(obj: any): boolean { 56 | return Object.prototype.toString.call(obj) === '[object Object]'; 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/table.ts: -------------------------------------------------------------------------------- 1 | import { mergeCells, splitCell } from '@tiptap/pm/tables'; 2 | import { EditorState } from '@tiptap/pm/state'; 3 | 4 | export function isTableActive(state: EditorState): boolean { 5 | const { selection, doc } = state; 6 | const { from, to } = selection; 7 | 8 | let keepLooking = true; 9 | let active = false; 10 | 11 | doc.nodesBetween(from, to, (node) => { 12 | const name = node.type.name; 13 | if ( 14 | keepLooking && 15 | (name === 'table' || 16 | name === 'table_row' || 17 | name === 'table_column' || 18 | name === 'table_cell') 19 | ) { 20 | keepLooking = false; 21 | active = true; 22 | } 23 | return keepLooking; 24 | }); 25 | 26 | return active; 27 | } 28 | 29 | export function enableMergeCells(state: EditorState): boolean { 30 | return isTableActive(state) && mergeCells(state); 31 | } 32 | 33 | export function enableSplitCell(state: EditorState): boolean { 34 | return isTableActive(state) && splitCell(state); 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "allowJs": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": "./src", 15 | "typeRoots": ["./types", "./node_modules/@types"], 16 | "types": ["element-plus/global"], 17 | "paths": { 18 | "@/*": ["./*"] 19 | }, 20 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"], 21 | "isolatedModules": true 22 | }, 23 | "include": [ 24 | "types/**/*", 25 | "src/**/*.ts", 26 | "src/**/*.tsx", 27 | "src/**/*.vue", 28 | "tests/**/*.ts", 29 | "tests/**/*.tsx" 30 | ], 31 | "exclude": ["node_modules"] 32 | } 33 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig, loadEnv } from 'vite'; 3 | import vue from '@vitejs/plugin-vue'; 4 | import ElementPlus from 'unplugin-element-plus/vite'; 5 | import svgLoader from 'vite-svg-loader'; 6 | import copy from 'rollup-plugin-copy'; 7 | import dts from 'vite-plugin-dts'; 8 | 9 | const libDir = path.resolve(__dirname, 'lib'); 10 | const srcDir = path.resolve(__dirname, 'src'); 11 | 12 | export default ({ mode }) => { 13 | process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }; 14 | 15 | const IS_DEMO = process.env.VITE_BUILD_TARGET === 'demo'; 16 | 17 | return defineConfig({ 18 | plugins: [ 19 | vue({ 20 | style: { 21 | preprocessLang: 'scss', 22 | postcssOptions: 'postcss.config.js', 23 | }, 24 | }), 25 | IS_DEMO 26 | ? null 27 | : dts({ 28 | include: ['src'], 29 | insertTypesEntry: true, 30 | }), 31 | ElementPlus(), 32 | svgLoader(), 33 | ], 34 | css: { 35 | preprocessorOptions: { 36 | scss: { 37 | charset: false, 38 | }, 39 | }, 40 | }, 41 | server: { 42 | port: 8080, 43 | }, 44 | resolve: { 45 | alias: [ 46 | { 47 | find: '@', 48 | replacement: path.resolve(__dirname, 'src'), 49 | }, 50 | { 51 | find: 'demos', 52 | replacement: path.resolve(__dirname, 'demos'), 53 | }, 54 | { 55 | find: 'element-tiptap', 56 | replacement: path.resolve(__dirname, 'src/index.ts'), 57 | }, 58 | ], 59 | }, 60 | build: IS_DEMO 61 | ? undefined 62 | : { 63 | outDir: libDir, 64 | minify: 'esbuild', 65 | lib: { 66 | entry: path.resolve(srcDir, 'index.ts'), 67 | name: 'ElementTiptap', 68 | fileName: 'element-tiptap', 69 | }, 70 | // https://rollupjs.org/guide/en/#big-list-of-options 71 | rollupOptions: { 72 | // 确保外部化处理那些你不想打包进库的依赖 73 | external: [ 74 | 'vue', 75 | 'element-plus/lib/components/button', 76 | 'element-plus/lib/components/checkbox', 77 | 'element-plus/lib/components/tooltip', 78 | 'element-plus/lib/components/dialog', 79 | 'element-plus/lib/components/popover', 80 | 'element-plus/lib/components/upload', 81 | 'element-plus/lib/components/message-box', 82 | 'element-plus/lib/components/dropdown', 83 | 'element-plus/lib/components/dropdown-menu', 84 | 'element-plus/lib/components/dropdown-item', 85 | ], 86 | output: { 87 | exports: 'named', 88 | // https://github.com/henriquehbr/svelte-typewriter/issues/21#issuecomment-968835822 89 | inlineDynamicImports: true, 90 | // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量 91 | globals: { 92 | vue: 'vue', 93 | 'element-plus/lib/components/button': 'ELEMENT.Button', 94 | 'element-plus/lib/components/checkbox': 'ELEMENT.Checkbox', 95 | 'element-plus/lib/components/tooltip': 'ELEMENT.Tooltip', 96 | 'element-plus/lib/components/dialog': 'ELEMENT.Dialog', 97 | 'element-plus/lib/components/popover': 'ELEMENT.Popover', 98 | 'element-plus/lib/components/upload': 'ELEMENT.Upload', 99 | 'element-plus/lib/components/message-box': 'ELEMENT.MessageBox', 100 | 'element-plus/lib/components/dropdown': 'ELEMENT.Dropdown', 101 | 'element-plus/lib/components/dropdown-menu': 102 | 'ELEMENT.DropdownMenu', 103 | 'element-plus/lib/components/dropdown-item': 104 | 'ELEMENT.DropdownItem', 105 | }, 106 | }, 107 | plugins: [ 108 | copy({ 109 | targets: [ 110 | { 111 | src: 'src/i18n/locales/', 112 | dest: 'lib', 113 | }, 114 | ], 115 | // https://github.com/vitejs/vite/issues/1231#issuecomment-753549857 116 | hook: 'writeBundle', // notice here 117 | }), 118 | ], 119 | }, 120 | }, 121 | publicDir: IS_DEMO ? 'public' : false, 122 | }); 123 | }; 124 | --------------------------------------------------------------------------------