├── .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 
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 |
2 |
3 |
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 |
2 |
3 |
4 |
5 |
9 |
10 |
14 |
15 |
16 |
17 |
117 |
--------------------------------------------------------------------------------
/demos/views/BubbleMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
61 |
--------------------------------------------------------------------------------
/demos/views/Event.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
15 |
89 |
--------------------------------------------------------------------------------
/demos/views/Output.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
HTML
13 |
{{ output.html }}
14 |
15 |
16 |
17 |
JSON
18 |
{{ output.json }}
19 |
20 |
21 |
22 |
23 |
79 |
80 |
115 |
--------------------------------------------------------------------------------
/demos/views/Placeholder.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
37 |
--------------------------------------------------------------------------------
/demos/views/Readonly.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
45 |
--------------------------------------------------------------------------------
/demos/views/Simple.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
79 |
--------------------------------------------------------------------------------
/demos/views/Title.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
21 |
--------------------------------------------------------------------------------
/src/components/ExtensionViews/TaskItemView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
47 |
--------------------------------------------------------------------------------
/src/components/Icon/Icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
22 |
--------------------------------------------------------------------------------
/src/components/MenuBar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
60 |
--------------------------------------------------------------------------------
/src/components/MenuBubble/ImageBubbleMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
31 |
--------------------------------------------------------------------------------
/src/components/MenuBubble/LinkBubbleMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
41 |
42 |
47 |
--------------------------------------------------------------------------------
/src/components/MenuBubble/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
34 |
35 |
36 |
37 |
182 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/CodeViewCommandButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
34 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/ColorPopover.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
30 |
31 |
32 |
40 |
41 |
48 | OK
49 |
50 |
51 |
52 |
53 |
54 |
60 |
61 |
62 |
63 |
64 |
65 |
135 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/CommandButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
75 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/FontFamilyDropdown.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
26 |
27 |
28 |
29 |
30 |
84 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/FontSizeDropdown.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
39 |
40 |
41 |
42 |
43 |
99 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/FullscreenCommandButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
50 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/HeadingDropdown.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
15 |
16 |
46 |
47 |
48 |
49 |
50 |
98 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/HighlightPopover.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
110 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/IframeCommandButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
59 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/Image/EditImageCommandButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
18 |
19 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
38 |
39 |
40 |
41 |
42 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | {{ t('editor.extensions.Image.control.edit_image.cancel') }}
56 |
57 |
58 |
59 | {{ t('editor.extensions.Image.control.edit_image.confirm') }}
60 |
61 |
62 |
63 |
64 |
65 |
66 |
150 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/Image/ImageDisplayCommandButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
35 |
36 |
37 |
87 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/Image/InsertImageCommandButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
24 |
25 |
26 |
27 |
33 |
34 |
35 |
36 |
37 |
42 |
50 |
51 |
52 |
53 |
54 | {{ t('editor.extensions.Image.control.upload_image.button') }}
55 |
56 |
57 |
58 |
59 |
60 |
169 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/Image/RemoveImageCommandButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
44 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/LineHeightDropdown.vue:
--------------------------------------------------------------------------------
1 |
2 | editor.commands.setLineHeight(lineHeight)"
6 | >
7 |
13 |
14 |
28 |
29 |
30 |
31 |
32 |
80 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/Link/AddLinkCommandButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 | {{ t('editor.extensions.Link.add.control.open_in_new_tab') }}
30 |
31 |
32 |
33 |
34 |
35 |
36 | {{ t('editor.extensions.Link.add.control.cancel') }}
37 |
38 |
39 |
46 | {{ t('editor.extensions.Link.add.control.confirm') }}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
135 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/Link/EditLinkCommandButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
27 | {{ t('editor.extensions.Link.edit.control.open_in_new_tab') }}
28 |
29 |
30 |
31 |
32 |
33 |
34 | {{ t('editor.extensions.Link.edit.control.cancel') }}
35 |
36 |
37 |
44 | {{ t('editor.extensions.Link.edit.control.confirm') }}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
120 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/Link/OpenLinkCommandButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
55 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/Link/UnlinkCommandButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
37 |
--------------------------------------------------------------------------------
/src/components/MenuCommands/TablePopover/CreateTablePopover.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
38 |
39 |
40 |
41 | {{ t('editor.extensions.Table.buttons.insert_table') }}
42 |
43 |
44 |
45 |
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 |
--------------------------------------------------------------------------------