├── .all-contributorsrc ├── .babelrc.js ├── .editorconfig ├── .eslintrc.js ├── .github └── badge.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .travis.yml ├── LICENSE ├── README-zh.md ├── README.md ├── build.sh ├── build ├── rollup.config.js └── webpack.config.js ├── docs ├── autosave.md ├── autosize.md ├── basic.md ├── dialog.md ├── disabled.md ├── editor-options.md ├── faq.md ├── height.md ├── placeholder.md └── upload-options.md ├── netlify.sh ├── notify.sh ├── package.json ├── src ├── assets │ ├── attachment.svg │ ├── bold.svg │ ├── bulletedlist.svg │ ├── dropdown-arrow.svg │ ├── eraser.svg │ ├── fontcolor.svg │ ├── fullscreen.vue │ ├── fullscreenexit.vue │ ├── horizontalline.svg │ ├── image.svg │ ├── italic.svg │ ├── link.svg │ ├── maximize.svg │ ├── media.svg │ ├── minimize.svg │ ├── numberedlist.svg │ ├── quotes.svg │ ├── redo.svg │ ├── richtext.less │ ├── strikethrough.svg │ ├── table.svg │ ├── todolist.svg │ ├── underline.svg │ ├── undo.svg │ └── zoom.svg ├── defaultEditorOptions.js ├── index.js ├── plugin │ ├── AttachmentUpload.js │ ├── Autoformat.js │ ├── Blockquote.js │ ├── Bold.js │ ├── DropdownButtonView.js │ ├── Essentials.js │ ├── ExtraFormat.js │ ├── FixComposing.js │ ├── Font │ │ ├── ColorTableView.js │ │ ├── ColorUI.js │ │ ├── FontBackgroundColor.js │ │ ├── FontColor.js │ │ ├── index.js │ │ └── utils.js │ ├── FullScreen.js │ ├── Heading.js │ ├── Horizontalline.js │ ├── ImagePreview.js │ ├── ImageUpload.js │ ├── Italic.js │ ├── Link.js │ ├── List.js │ ├── Mediaembed │ │ ├── AutoMediaEmbed.js │ │ └── index.js │ ├── RemoveFormat │ │ ├── RemoveFormatLinks.js │ │ ├── RemoveFormatUI.js │ │ └── index.js │ ├── Strikethrough.js │ ├── Table.js │ ├── TodoList.js │ ├── TransformMD.js │ ├── Underline.js │ ├── Undo.js │ ├── Uploader.js │ └── generateUIPlugin.js ├── translations │ ├── en.js │ ├── index.js │ └── zh-cn.js ├── utils │ ├── adapter.js │ └── index.js ├── v-editor.d.ts └── v-editor.vue ├── styleguide.config.js ├── styleguide ├── element.js └── upload-to-ali.js ├── test ├── index.test.js └── sum.js └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "v-editor", 3 | "projectOwner": "FEMessage", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": false, 11 | "commitConvention": "angular", 12 | "contributors": [ 13 | { 14 | "login": "kunzhijia", 15 | "name": "kunzhijia", 16 | "avatar_url": "https://avatars2.githubusercontent.com/u/4848041?v=4", 17 | "profile": "https://github.com/kunzhijia", 18 | "contributions": [ 19 | "code", 20 | "bug", 21 | "doc" 22 | ] 23 | }, 24 | { 25 | "login": "listars", 26 | "name": "listars", 27 | "avatar_url": "https://avatars2.githubusercontent.com/u/20613509?v=4", 28 | "profile": "https://github.com/listars", 29 | "contributions": [ 30 | "bug", 31 | "doc" 32 | ] 33 | }, 34 | { 35 | "login": "donaldshen", 36 | "name": "Donald Shen", 37 | "avatar_url": "https://avatars3.githubusercontent.com/u/19591950?v=4", 38 | "profile": "https://donaldshen.github.io/portfolio", 39 | "contributions": [ 40 | "bug", 41 | "doc", 42 | "plugin", 43 | "review" 44 | ] 45 | }, 46 | { 47 | "login": "levy9527", 48 | "name": "levy", 49 | "avatar_url": "https://avatars3.githubusercontent.com/u/9384365?v=4", 50 | "profile": "http://levy.work", 51 | "contributions": [ 52 | "review", 53 | "infra", 54 | "ideas" 55 | ] 56 | }, 57 | { 58 | "login": "colmugx", 59 | "name": "ColMugX", 60 | "avatar_url": "https://avatars1.githubusercontent.com/u/21327913?v=4", 61 | "profile": "https://colmugx.github.io", 62 | "contributions": [ 63 | "code", 64 | "blog", 65 | "design", 66 | "plugin" 67 | ] 68 | }, 69 | { 70 | "login": "snowlocked", 71 | "name": "snowlocked", 72 | "avatar_url": "https://avatars0.githubusercontent.com/u/19562649?v=4", 73 | "profile": "https://github.com/snowlocked", 74 | "contributions": [ 75 | "bug" 76 | ] 77 | } 78 | ], 79 | "contributorsPerLine": 7, 80 | "skipCi": true 81 | } 82 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | return { 3 | presets: [['@babel/env', {modules: api.env('test') ? 'commonjs' : false}]], 4 | plugins: [ 5 | [ 6 | '@babel/transform-runtime', 7 | { 8 | regenerator: true 9 | } 10 | ] 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:jest/recommended', 13 | 'plugin:vue/recommended', 14 | 'plugin:prettier/recommended', 15 | 'prettier/vue' 16 | ], 17 | plugins: ['vue', 'prettier'], 18 | rules: { 19 | 'no-console': [ 20 | 'error', 21 | { 22 | allow: ['warn', 'error'] 23 | } 24 | ], 25 | 'no-debugger': 'error', 26 | 'prettier/prettier': 'error' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/badge.yml: -------------------------------------------------------------------------------- 1 | types: 2 | feat: 'enhancement' 3 | fix: 'bug' 4 | docs: 'documentation' 5 | refactor: 'refactor' 6 | test: 'test' 7 | perf: 'performance' 8 | chore: 9 | deps: 'dependencies' 10 | default: 'chore' 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | dist 7 | docs/build 8 | docs/index.html 9 | docs/*.woff 10 | docs/*.ttf 11 | docs/**/*.js 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | .env 21 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | docs 2 | dist 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | bracketSpacing: false 4 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "no-empty-source": null, 5 | "selector-pseudo-element-no-unknown": [true, { 6 | "ignorePseudoElements": ["v-deep"] 7 | }] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | only: 3 | - master 4 | language: node_js 5 | node_js: 6 | - lts/* 7 | git: 8 | depth: 30 9 | install: 10 | - yarn --frozen-lockfile 11 | - yarn test 12 | script: 13 | - ./build.sh 14 | after_script: 15 | - ./notify.sh 16 | cache: yarn 17 | deploy: 18 | - provider: pages 19 | local-dir: docs 20 | github-token: $GITHUB_TOKEN 21 | skip-cleanup: true 22 | keep-history: true 23 | - provider: npm 24 | email: levy9527@qq.com 25 | api_key: $NPM_TOKEN 26 | skip-cleanup: true 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 FEMessage 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 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # v-editor 2 | 3 | [![Build Status](https://travis-ci.com/FEMessage/v-editor.svg?branch=master)](https://travis-ci.com/FEMessage/v-editor) 4 | [![NPM Download](https://img.shields.io/npm/dm/@femessage/v-editor.svg)](https://www.npmjs.com/package/@femessage/v-editor) 5 | [![NPM Version](https://img.shields.io/npm/v/@femessage/v-editor.svg)](https://www.npmjs.com/package/@femessage/v-editor) 6 | [![NPM License](https://img.shields.io/npm/l/@femessage/v-editor.svg)](https://github.com/FEMessage/v-editor/blob/master/LICENSE) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/FEMessage/v-editor/pulls) 8 | [![Automated Release Notes by gren](https://img.shields.io/badge/%F0%9F%A4%96-release%20notes-00B2EE.svg)](https://github-tools.github.io/github-release-notes/) 9 | 10 | 根据 [ckeditor5](https://github.com/ckeditor/ckeditor5) 以及 [upload-to-ali](https://github.com/femessage/upload-to-ali) 封装的轻量级富文本编辑器。 11 | 12 | ![view.png](https://i.loli.net/2020/02/03/5J8Holf2vqrGSwu.png) 13 | 14 | ## Table of Contents 15 | 16 | - [Feature](#feature) 17 | - [Demo](#demo) 18 | - [Install](#install) 19 | - [Quick start](#quick-start) 20 | - [Links](#Links) 21 | - [License](#license) 22 | - [Contributors](#contributors) 23 | 24 | ## Feature 25 | 26 | - **oss 上传**:整合了上传组件,只需配置 OSS 的基本信息([配置参考](https://github.com/FEMessage/upload-to-ali/blob/dev/README-zh.md#dotenv)),即可将图片或文件上传到 oss,支持截图粘贴上传 27 | - **添加网络图片**:可以使用 markdown 图片语法(`![]()`)快速添加网络图片,也可以直接粘贴添加 28 | - **全屏编辑**:可以让编辑器覆盖整个页面 29 | 30 | [⬆ Back to Top](#table-of-contents) 31 | 32 | ## Demo 33 | 34 | - [doc and online demo](https://femessage.github.io/v-editor/) 35 | 36 | [⬆ Back to Top](#table-of-contents) 37 | 38 | ## Install 39 | 40 | ```sh 41 | # 上传图片功能依赖upload-to-ali组件 42 | yarn add @femessage/upload-to-ali @femessage/v-editor 43 | ``` 44 | 45 | [⬆ Back to Top](#table-of-contents) 46 | 47 | ## Quick start 48 | 49 | ```vue 50 | //step1 确保oss配置 //step2 在需要使用该渲染器的.vue文件中 51 | 54 | 67 | ``` 68 | 69 | [⬆ Back to Top](#table-of-contents) 70 | 71 | ## Links 72 | 73 | - [插件开发指南](https://www.yuque.com/docs/share/d52c00bf-d379-45c6-955f-8eb218a4dabf) 74 | 75 | [⬆ Back to Top](#table-of-contents) 76 | 77 | ## License 78 | 79 | [MIT](./LICENSE) 80 | 81 | [⬆ Back to Top](#table-of-contents) 82 | 83 | ## Contributors 84 | 85 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 86 | 87 | 88 | 89 | 90 |
kunzhijia
kunzhijia

💻 🐛 📖
listars
listars

🐛 📖
Donald Shen
Donald Shen

🐛 📖
levy
levy

👀 🚇 🤔
91 | 92 | 93 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # v-editor 2 | 3 | [![Build Status](https://travis-ci.com/FEMessage/v-editor.svg?branch=master)](https://travis-ci.com/FEMessage/v-editor) 4 | [![NPM Download](https://img.shields.io/npm/dm/@femessage/v-editor.svg)](https://www.npmjs.com/package/@femessage/v-editor) 5 | [![NPM Version](https://img.shields.io/npm/v/@femessage/v-editor.svg)](https://www.npmjs.com/package/@femessage/v-editor) 6 | [![NPM License](https://img.shields.io/npm/l/@femessage/v-editor.svg)](https://github.com/FEMessage/v-editor/blob/master/LICENSE) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/FEMessage/v-editor/pulls) 8 | [![Automated Release Notes by gren](https://img.shields.io/badge/%F0%9F%A4%96-release%20notes-00B2EE.svg)](https://github-tools.github.io/github-release-notes/) 9 | 10 | Lightweight rich text editor based on [ckeditor5](https://github.com/ckeditor/ckeditor5) and [upload-to-ali](https://github.com/FEMessage/upload-to-ali). 11 | 12 | ![view.png](https://i.loli.net/2020/07/17/8geC2fBIv17SLaN.png) 13 | 14 | [中文文档](./README-zh.md) 15 | 16 | ## Table of Contents 17 | 18 | - [Feature](#feature) 19 | - [Demo](#demo) 20 | - [Install](#install) 21 | - [Quick Start](#quick-start) 22 | - [Links](#Links) 23 | - [License](#license) 24 | - [Contributors](#contributors) 25 | 26 | ## Feature 27 | 28 | - **File Upload** : Integrated upload components, just configure the basic information of OSS ([Configuration Reference](https://github.com/FEMessage/upload-to-ali/blob/dev/README.md#dotenv)), you can upload the picture or file to oss, support screenshot paste upload 29 | - **Add Net Image**: Can quickly add a net picture using the markdown picture syntax(`![]()`), or you can paste it directly 30 | - **Fullscreen Editing**: Allows the editor to cover the window 31 | 32 | [⬆Back to Top](#table-of-contents) 33 | 34 | ## Demo 35 | 36 | - [Doc and online demo](https://femessage.github.io/v-editor/) 37 | 38 | [⬆Back to Top](#table-of-contents) 39 | 40 | ## Install 41 | 42 | ```sh 43 | # Upload image function depends on upload-to-ali component 44 | yarn add @femessage/upload-to-ali @femessage/v-editor 45 | ``` 46 | 47 | [⬆ Back to Top](#table-of-contents) 48 | 49 | ## Quick start 50 | 51 | ```html 52 | 53 | 54 | renderer 55 | 58 | 59 | 73 | ``` 74 | 75 | [⬆ Back to Top](#table-of-contents) 76 | 77 | ## Links 78 | 79 | - [how to create a plugin](https://www.yuque.com/docs/share/d52c00bf-d379-45c6-955f-8eb218a4dabf?translate=en) 80 | 81 | [⬆ Back to Top](#table-of-contents) 82 | 83 | ## License 84 | 85 | [MIT](./LICENSE) 86 | 87 | [⬆ Back to Top](#table-of-contents) 88 | 89 | ## Contributors 90 | 91 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |

kunzhijia

💻 🐛 📖

listars

🐛 📖

Donald Shen

🐛 📖 🔌 👀

levy

👀 🚇 🤔

ColMugX

💻 📝 🎨 🔌

snowlocked

🐛
106 | 107 | 108 | 109 | 110 | 111 | 112 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 113 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | yarn stdver 3 | 4 | yarn build 5 | -------------------------------------------------------------------------------- /build/rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import vue from 'rollup-plugin-vue' 3 | import babel from 'rollup-plugin-babel' 4 | import commonjs from 'rollup-plugin-commonjs' 5 | import {terser} from 'rollup-plugin-terser' 6 | import minimist from 'minimist' 7 | 8 | const argv = minimist(process.argv.slice(2)) 9 | 10 | const config = { 11 | input: 'src/index.js', 12 | output: { 13 | name: 'VEditor', 14 | exports: 'named' 15 | }, 16 | plugins: [ 17 | commonjs(), 18 | vue({ 19 | css: true, 20 | compileTemplate: true 21 | }), 22 | babel({ 23 | runtimeHelpers: true, 24 | exclude: 'node_modules/**' 25 | }) 26 | ] 27 | } 28 | 29 | // Only minify browser (iife) version 30 | if (argv.format === 'iife') { 31 | config.plugins.push(terser()) 32 | } 33 | 34 | export default config 35 | -------------------------------------------------------------------------------- /build/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CKEditorWebpackPlugin = require('@ckeditor/ckeditor5-dev-webpack-plugin') 2 | const {bundler, styles} = require('@ckeditor/ckeditor5-dev-utils') 3 | const {VueLoaderPlugin} = require('vue-loader') 4 | const TerserPlugin = require('terser-webpack-plugin') 5 | const webpack = require('webpack') 6 | const path = require('path') 7 | 8 | module.exports = { 9 | entry: [ 10 | require.resolve('regenerator-runtime/runtime.js'), 11 | path.resolve('src', 'index.js') 12 | ], 13 | output: { 14 | library: 'VEditor', 15 | path: path.resolve('dist'), 16 | filename: 'v-editor.umd.js', 17 | libraryTarget: 'umd', 18 | libraryExport: 'default' 19 | }, 20 | optimization: { 21 | usedExports: true, 22 | minimizer: [ 23 | new TerserPlugin({ 24 | terserOptions: { 25 | output: { 26 | comments: /^!/ 27 | } 28 | }, 29 | extractComments: false 30 | }) 31 | ] 32 | }, 33 | plugins: [ 34 | new CKEditorWebpackPlugin({ 35 | language: 'zh-cn' 36 | }), 37 | new webpack.BannerPlugin({ 38 | banner: bundler.getLicenseBanner(), 39 | raw: true 40 | }), 41 | new VueLoaderPlugin() 42 | ], 43 | externals: ['@femessage/upload-to-ali'], 44 | module: { 45 | rules: [ 46 | { 47 | test: /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/, 48 | use: [ 49 | { 50 | loader: 'style-loader', 51 | options: { 52 | injectType: 'singletonStyleTag' 53 | } 54 | }, 55 | { 56 | loader: 'postcss-loader', 57 | options: styles.getPostCssConfig({ 58 | themeImporter: { 59 | themePath: require.resolve('@ckeditor/ckeditor5-theme-lark') 60 | }, 61 | minify: true 62 | }) 63 | } 64 | ] 65 | }, 66 | { 67 | test: /\.svg$/, 68 | use: ['raw-loader'] 69 | }, 70 | { 71 | test: /\.vue$/, 72 | loader: 'vue-loader' 73 | }, 74 | { 75 | test: /\.css$/, 76 | exclude: /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/, 77 | loaders: ['style-loader', 'css-loader'] 78 | }, 79 | { 80 | test: /\.less$/, 81 | loaders: ['vue-style-loader', 'css-loader', 'less-loader'] 82 | } 83 | ] 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /docs/autosave.md: -------------------------------------------------------------------------------- 1 | 如果 8 秒内没有任何操作,会触发自动保存 2 | 3 | It will autosave if no any operation in 8s 4 | 5 | ```vue 6 | 9 | 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/autosize.md: -------------------------------------------------------------------------------- 1 | 设置自适应内容高度 2 | 3 | ```vue 4 | 12 | 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/basic.md: -------------------------------------------------------------------------------- 1 | 基本用法 2 | 3 | ```vue 4 | 10 | 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/dialog.md: -------------------------------------------------------------------------------- 1 | 弹窗例子 2 | 3 | ```vue 4 | 17 | 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/disabled.md: -------------------------------------------------------------------------------- 1 | 禁用 2 | 3 | ```vue 4 | 10 | 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/editor-options.md: -------------------------------------------------------------------------------- 1 | 自定义菜单 2 | 3 | > lodash.merge 的覆盖规则是数组项合并类似 concat; 4 | > 目前似乎无法自定义 toolbar;要么这里改例子要么改代码 5 | 6 | ```vue 7 | 10 | 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | ## 在 TypeScript 中指定组件的类型 2 | 3 | ```html 4 | 13 | ``` -------------------------------------------------------------------------------- /docs/height.md: -------------------------------------------------------------------------------- 1 | 自定义编辑区(不包括 toolbar)高度,height 支持传 css 长度和 Number 类型(默认单位 px) 2 | 3 | ```vue 4 | 10 | 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/placeholder.md: -------------------------------------------------------------------------------- 1 | 基本用法 2 | 3 | ```vue 4 | 9 | 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/upload-options.md: -------------------------------------------------------------------------------- 1 | 覆盖默认的上传行为,可以自定义上传的实现 2 | 3 | ```vue 4 | 7 | 8 | 35 | ``` 36 | 37 | 设置upload-to-ali的大小限制,兼容overSize等事件 38 | 39 | ```vue 40 | 43 | 44 | 73 | ``` 74 | -------------------------------------------------------------------------------- /netlify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "is netlify: $NETLIFY" 3 | echo "in branch: $BRANCH" 4 | echo "head: $HEAD" 5 | 6 | if [ "$NETLIFY" != "true" ] 7 | then 8 | echo "this script only runs in netlify, bye" 9 | exit 1 10 | fi 11 | 12 | if [ "$BRANCH" != "dev" ] && [ "$HEAD" != "dev" ] 13 | then 14 | yarn doc 15 | else 16 | echo "this script only runs in targeting dev's PR deploy preview, bye" 17 | fi 18 | -------------------------------------------------------------------------------- /notify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # https://stackoverflow.com/questions/13872048/bash-script-what-does-bin-bash-mean 3 | echo "1/5: checking TRAVIS_TEST_RESULT" 4 | if [ "$TRAVIS_TEST_RESULT" != "0" ] 5 | then 6 | echo "build not success, bye" 7 | exit 1 8 | fi 9 | 10 | ORG_NAME=$(echo "$TRAVIS_REPO_SLUG" | cut -d '/' -f 1) 11 | REPO_NAME=$(echo "$TRAVIS_REPO_SLUG" | cut -d '/' -f 2) 12 | 13 | echo "2/5: pushing commit and tag to github" 14 | # 该命令很可能报错,但不影响实际进行,因而不能简单地在脚本开头 set -e 15 | git remote add github https://$GITHUB_TOKEN@github.com/$TRAVIS_REPO_SLUG.git > /dev/null 2>&1 16 | git push github HEAD:master --follow-tags 17 | 18 | echo "3/5: generating github release notes" 19 | GREN_GITHUB_TOKEN=$GITHUB_TOKEN yarn release 20 | 21 | # 避免发送错误信息 22 | if [ $? -ne 0 ] 23 | then 24 | echo "gren fails, bye" 25 | exit 1 26 | fi 27 | 28 | echo "4/5: downloading github release info" 29 | url=https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases/latest 30 | resp_tmp_file=resp.tmp 31 | 32 | curl -H "Authorization: token $GITHUB_TOKEN" $url > $resp_tmp_file 33 | 34 | html_url=$(sed -n 5p $resp_tmp_file | sed 's/\"html_url\"://g' | awk -F '"' '{print $2}') 35 | body=$(grep body < $resp_tmp_file | sed 's/\"body\"://g;s/\"//g') 36 | version=$(echo $html_url | awk -F '/' '{print $NF}') 37 | 38 | echo "5/5: notifying with dingtalk bot" 39 | msg='{"msgtype": "markdown", "markdown": {"title": "'$REPO_NAME'更新", "text": "@所有人\n# ['$REPO_NAME'('$version')]('$html_url')\n'$body'"}}' 40 | 41 | curl -X POST https://oapi.dingtalk.com/robot/send\?access_token\=$DINGTALK_ROBOT_TOKEN -H 'Content-Type: application/json' -d "$msg" 42 | 43 | rm $resp_tmp_file 44 | 45 | echo "executing notify.sh successfully" 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@femessage/v-editor", 3 | "version": "1.2.0", 4 | "description": "upload img and write rich text easily", 5 | "author": "https://github.com/FEMessage", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/FEMessage/v-editor.git" 10 | }, 11 | "keywords": [ 12 | "vue", 13 | "sfc", 14 | "component" 15 | ], 16 | "files": [ 17 | "src", 18 | "dist", 19 | "yarn.lock" 20 | ], 21 | "main": "dist/v-editor.umd.js", 22 | "browser": { 23 | "./sfc": "src/v-editor.vue" 24 | }, 25 | "types": "src/v-editor.d.ts", 26 | "scripts": { 27 | "dev": "vue-styleguidist server", 28 | "test": "jest --verbose", 29 | "doc": "vue-styleguidist build", 30 | "build": "npm run build:umd & npm run doc", 31 | "precommit": "pretty-quick --staged", 32 | "stdver": "standard-version -m '[skip ci] chore(release): v%s'", 33 | "release": "gren release --override", 34 | "lint": "eslint \"**/*.@(js|vue)\" --fix", 35 | "build:umd": "webpack --mode production --config build/webpack.config.js" 36 | }, 37 | "dependencies": { 38 | "@ckeditor/ckeditor5-adapter-ckfinder": "^16.0.0", 39 | "@ckeditor/ckeditor5-autoformat": "^16.0.0", 40 | "@ckeditor/ckeditor5-autosave": "^16.0.0", 41 | "@ckeditor/ckeditor5-basic-styles": "^16.0.0", 42 | "@ckeditor/ckeditor5-block-quote": "^16.0.0", 43 | "@ckeditor/ckeditor5-ckfinder": "^16.0.0", 44 | "@ckeditor/ckeditor5-core": "^16.0.0", 45 | "@ckeditor/ckeditor5-dev-utils": "^12.0.0", 46 | "@ckeditor/ckeditor5-dev-webpack-plugin": "^8.0.0", 47 | "@ckeditor/ckeditor5-editor-classic": "^16.0.0", 48 | "@ckeditor/ckeditor5-essentials": "^16.0.0", 49 | "@ckeditor/ckeditor5-font": "^16.0.0", 50 | "@ckeditor/ckeditor5-heading": "^16.0.0", 51 | "@ckeditor/ckeditor5-horizontal-line": "^16.0.0", 52 | "@ckeditor/ckeditor5-image": "^16.0.0", 53 | "@ckeditor/ckeditor5-indent": "^16.0.0", 54 | "@ckeditor/ckeditor5-link": "^16.0.0", 55 | "@ckeditor/ckeditor5-list": "^16.0.0", 56 | "@ckeditor/ckeditor5-media-embed": "^16.0.0", 57 | "@ckeditor/ckeditor5-paragraph": "^16.0.0", 58 | "@ckeditor/ckeditor5-paste-from-office": "^16.0.0", 59 | "@ckeditor/ckeditor5-remove-format": "^16.0.0", 60 | "@ckeditor/ckeditor5-table": "^16.0.0", 61 | "@ckeditor/ckeditor5-theme-lark": "^16.0.0", 62 | "@ckeditor/ckeditor5-upload": "^16.0.0", 63 | "@ckeditor/ckeditor5-utils": "^16.0.0", 64 | "@ckeditor/ckeditor5-vue": "^1.0.1", 65 | "@femessage/img-preview": "^1.4.1", 66 | "github-markdown-css": "^3.0.1", 67 | "lodash-es": "^4.17.15", 68 | "marked": "^1.0.0" 69 | }, 70 | "devDependencies": { 71 | "@babel/core": "^7.8.3", 72 | "@babel/plugin-transform-runtime": "^7.4.3", 73 | "@babel/preset-env": "^7.8.3", 74 | "@ckeditor/ckeditor5-inspector": "^1.5.0", 75 | "@femessage/github-release-notes": "latest", 76 | "@femessage/upload-to-ali": "^2.1.2", 77 | "babel-eslint": "^10.0.3", 78 | "babel-loader": "^8.0.5", 79 | "css-loader": "^3.4.2", 80 | "dotenv": "^7.0.0", 81 | "element-ui": "^2.7.2", 82 | "eslint": "^6.6.0", 83 | "eslint-config-prettier": "^6.5.0", 84 | "eslint-plugin-jest": "^23.0.3", 85 | "eslint-plugin-prettier": "^3.1.1", 86 | "eslint-plugin-vue": "^6.0.0", 87 | "file-loader": "^3.0.1", 88 | "glob": "^7.1.3", 89 | "husky": "1.3.1", 90 | "jest": "^24.8.0", 91 | "less": "^3.10.3", 92 | "less-loader": "^5.0.0", 93 | "lint-staged": "^8.1.0", 94 | "minimist": "^1.2.0", 95 | "postcss-loader": "^3.0.0", 96 | "prettier": "1.18.2", 97 | "raw-loader": "^4.0.0", 98 | "regenerator-runtime": "^0.13.3", 99 | "rollup": "^1.11.0", 100 | "rollup-plugin-babel": "^4.3.2", 101 | "rollup-plugin-commonjs": "^9.3.4", 102 | "rollup-plugin-terser": "^4.0.4", 103 | "rollup-plugin-vue": "^4.7.2", 104 | "standard-version": "^6.0.1", 105 | "style-loader": "^1.1.2", 106 | "stylelint": "^13.0.0", 107 | "stylelint-config-standard": "^19.0.0", 108 | "terser-webpack-plugin": "^2.3.2", 109 | "vue": "^2.5.16", 110 | "vue-loader": "^15.8.3", 111 | "vue-styleguidist": "3.11.4", 112 | "vue-template-compiler": "^2.5.16", 113 | "webpack": "^4.41.5", 114 | "webpack-cli": "^3.3.10" 115 | }, 116 | "publishConfig": { 117 | "access": "public" 118 | }, 119 | "vue-sfc-cli": "1.12.0", 120 | "engines": { 121 | "node": ">= 4.0.0", 122 | "npm": ">= 3.0.0" 123 | }, 124 | "husky": { 125 | "hooks": { 126 | "pre-commit": "lint-staged", 127 | "post-commit": "git update-index --again", 128 | "pre-push": "yarn test" 129 | } 130 | }, 131 | "lint-staged": { 132 | "*.@(md|json)": [ 133 | "prettier --write", 134 | "git add" 135 | ], 136 | "*.js": [ 137 | "eslint --fix", 138 | "prettier --write", 139 | "git add" 140 | ], 141 | "*.vue": [ 142 | "eslint --fix", 143 | "prettier --write", 144 | "stylelint --fix", 145 | "git add" 146 | ] 147 | }, 148 | "gren": "@femessage/grenrc" 149 | } 150 | -------------------------------------------------------------------------------- /src/assets/attachment.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/bold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/bulletedlist.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/dropdown-arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/eraser.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fontcolor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fullscreen.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /src/assets/fullscreenexit.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /src/assets/horizontalline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/italic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/maximize.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/media.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/numberedlist.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/quotes.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/redo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/richtext.less: -------------------------------------------------------------------------------- 1 | @import url('~github-markdown-css/github-markdown.css'); 2 | 3 | .v-editor .markdown-body, 4 | .v-editor.markdown-body { 5 | hr { 6 | background-color: #e8e8e8; 7 | height: 2px; 8 | margin: 12px 0; 9 | } 10 | 11 | p { 12 | margin-bottom: 0; 13 | } 14 | 15 | ol, 16 | ul { 17 | padding-left: 1.5em; 18 | display: block; 19 | margin-block-start: 1em; 20 | margin-block-end: 1em; 21 | margin-inline-start: 0; 22 | margin-inline-end: 0; 23 | padding-inline-start: 40px; 24 | } 25 | 26 | ul:not(.todo-list) { 27 | list-style-type: disc; 28 | } 29 | 30 | ol { 31 | list-style-type: decimal; 32 | } 33 | 34 | li { 35 | display: list-item; 36 | text-align: -webkit-match-parent; 37 | } 38 | 39 | ol ol { 40 | list-style-type: lower-alpha; 41 | 42 | & ol { 43 | list-style-type: lower-roman; 44 | } 45 | } 46 | 47 | i { 48 | font-style: italic; 49 | } 50 | 51 | // 图片、表格默认靠左 52 | figure { 53 | &.image, 54 | &.table { 55 | margin: 1em 0; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/assets/strikethrough.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/todolist.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/underline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/undo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/zoom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/defaultEditorOptions.js: -------------------------------------------------------------------------------- 1 | import Autosave from '@ckeditor/ckeditor5-autosave/src/autosave' 2 | import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder' 3 | 4 | import Image from '@ckeditor/ckeditor5-image/src/image' 5 | import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption' 6 | import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle' 7 | import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar' 8 | import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize' 9 | 10 | import Indent from '@ckeditor/ckeditor5-indent/src/indent' 11 | import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph' 12 | import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice' 13 | import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar' 14 | 15 | // 本地魔改插件 16 | import Essentials from './plugin/Essentials' 17 | import Font from './plugin/Font' 18 | import Bold from './plugin/Bold' 19 | import Italic from './plugin/Italic' 20 | import Strikethrough from './plugin/Strikethrough' 21 | import Underline from './plugin/Underline' 22 | import List from './plugin/List' 23 | import TodoList from './plugin/TodoList' 24 | import Link from './plugin/Link' 25 | import BlockQuote from './plugin/Blockquote' 26 | import HorizontalLine from './plugin/Horizontalline' 27 | import ImageUpload from './plugin/ImageUpload' 28 | import Table from './plugin/Table' 29 | import MediaEmbed from './plugin/Mediaembed' 30 | import Heading from './plugin/Heading' 31 | 32 | // 本地插件 33 | import Autoformat from './plugin/Autoformat' 34 | import ExtraFormat from './plugin/ExtraFormat' 35 | import RemoveFormat from './plugin/RemoveFormat' 36 | import AttachmentUpload from './plugin/AttachmentUpload' 37 | import FixComposing from './plugin/FixComposing' 38 | import TransformMD from './plugin/TransformMD' 39 | 40 | export default { 41 | plugins: [ 42 | Essentials, 43 | Autosave, 44 | Autoformat, 45 | Font, 46 | Bold, 47 | Italic, 48 | Strikethrough, 49 | Underline, 50 | BlockQuote, 51 | HorizontalLine, 52 | CKFinder, 53 | Heading, 54 | Image, 55 | ImageCaption, 56 | ImageStyle, 57 | ImageToolbar, 58 | ImageUpload, 59 | ImageResize, 60 | Indent, 61 | Link, 62 | List, 63 | TodoList, 64 | MediaEmbed, 65 | Paragraph, 66 | PasteFromOffice, 67 | Table, 68 | TableToolbar, 69 | ExtraFormat, 70 | RemoveFormat, 71 | AttachmentUpload, 72 | TransformMD, 73 | FixComposing 74 | ], 75 | toolbar: [ 76 | 'undo', 77 | 'redo', 78 | 'removeFormat', 79 | '|', 80 | 'heading', 81 | '|', 82 | 'bold', 83 | 'italic', 84 | 'strikethrough', 85 | 'underline', 86 | '|', 87 | 'fontColor', 88 | 'fontBackgroundColor', 89 | '|', 90 | 'bulletedList', 91 | 'numberedList', 92 | 'todoList', 93 | '|', 94 | 'link', 95 | 'blockQuote', 96 | 'horizontalLine', 97 | '|', 98 | 'imageUpload', 99 | 'AttachmentUpload', 100 | 'insertTable', 101 | 'mediaEmbed' 102 | ], 103 | heading: { 104 | options: [ 105 | {model: 'paragraph', title: '正文', class: 'ck-heading_paragraph'}, 106 | { 107 | model: 'heading1', 108 | view: 'h1', 109 | title: 'Heading 1', 110 | class: 'ck-heading_heading1' 111 | }, 112 | { 113 | model: 'heading2', 114 | view: 'h2', 115 | title: 'Heading 2', 116 | class: 'ck-heading_heading2' 117 | }, 118 | { 119 | model: 'heading3', 120 | view: 'h3', 121 | title: 'Heading 3', 122 | class: 'ck-heading_heading3' 123 | }, 124 | { 125 | model: 'heading4', 126 | view: 'h4', 127 | title: 'Heading 4' 128 | } 129 | ] 130 | }, 131 | image: { 132 | resizeUnit: 'px', 133 | toolbar: [ 134 | 'imageStyle:full', 135 | 'imageStyle:side', 136 | '|', 137 | 'imageTextAlternative', 138 | 'imagePreview' 139 | ] 140 | }, 141 | table: { 142 | contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells'] 143 | }, 144 | link: { 145 | addTargetToExternalLinks: true 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Import vue component 2 | import Component from './v-editor.vue' 3 | 4 | // `Vue.use` automatically prevents you from using 5 | // the same plugin more than once, 6 | // so calling it multiple times on the same plugin 7 | // will install the plugin only once 8 | Component.install = Vue => { 9 | Vue.component(Component.name, Component) 10 | } 11 | 12 | // To auto-install when vue is found 13 | let GlobalVue = null 14 | if (typeof window !== 'undefined') { 15 | GlobalVue = window.Vue 16 | } else if (typeof global !== 'undefined') { 17 | GlobalVue = global.Vue 18 | } 19 | if (GlobalVue) { 20 | GlobalVue.use(Component) 21 | } 22 | 23 | // To allow use as module (npm/webpack/etc.) export component 24 | export default Component 25 | 26 | // It's possible to expose named exports when writing components that can 27 | // also be used as directives, etc. - eg. import { RollupDemoDirective } from 'rollup-demo'; 28 | // export const RollupDemoDirective = component; 29 | -------------------------------------------------------------------------------- /src/plugin/AttachmentUpload.js: -------------------------------------------------------------------------------- 1 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 2 | import Command from '@ckeditor/ckeditor5-core/src/command' 3 | import FileDialogButtonView from '@ckeditor/ckeditor5-upload/src/ui/filedialogbuttonview' 4 | 5 | import attachmentIcon from '../assets/attachment.svg' 6 | 7 | export default class AttachmentUpload extends Plugin { 8 | /** 9 | * @inheritDoc 10 | */ 11 | static get pluginName() { 12 | return 'AttachmentUpload' 13 | } 14 | 15 | init() { 16 | const {editor} = this 17 | 18 | // 注册命令 19 | editor.commands.add('attachmentUpload', new AttachmentCommand(editor)) 20 | 21 | editor.ui.componentFactory.add('AttachmentUpload', locale => { 22 | // 文件选择器类型按钮 23 | const view = new FileDialogButtonView(locale) 24 | 25 | const command = editor.commands.get('attachmentUpload') 26 | 27 | view.buttonView.set({ 28 | label: '附件上传', 29 | icon: attachmentIcon, 30 | tooltip: true 31 | }) 32 | 33 | view.buttonView.bind('isEnabled').to(command) 34 | 35 | // 文件选择结束回调 36 | view.on('done', (_, file) => { 37 | editor.execute('attachmentUpload', {file}) 38 | }) 39 | 40 | return view 41 | }) 42 | } 43 | } 44 | 45 | class AttachmentCommand extends Command { 46 | execute(options) { 47 | const {editor} = this 48 | const model = editor.model 49 | const fileRepository = editor.plugins.get('FileRepository') 50 | 51 | // 被调用命令时传入文件 52 | const file = options.file[0] 53 | 54 | model.change(writer => { 55 | const loader = fileRepository.createLoader(file) 56 | /** @type {import ('ckeditor__ckeditor5-engine').model.Range} */ 57 | let filenameTxtPlaceholderRange 58 | 59 | // 执行顺序依次:读取,占位,上传 60 | loader 61 | .read() 62 | .then(() => { 63 | const filenameTxtModel = writer.createText(`{{${file.name}}}`) 64 | filenameTxtPlaceholderRange = model.insertContent( 65 | filenameTxtModel, 66 | model.document.selection 67 | ) 68 | }) 69 | .then(() => loader.upload()) 70 | .then(data => { 71 | const url = data.default 72 | 73 | /** 74 | * 没法在 link 里插入 svg 图片 75 | * https://ckeditor.com/docs/ckeditor5/latest/builds/guides/faq.html#where-are-the-editorinserthtml-and-editorinserttext-methods-how-to-insert-some-content 76 | */ 77 | // const viewFragment = editor.data.processor.toView(attachmentIcon) 78 | // const modelFragment = editor.data.toModel(viewFragment) 79 | // console.log(modelFragment) // 空。得写插件支持 80 | /** 81 | * 只能 emoji 了 82 | * 备选:🔗📂📚📦 83 | */ 84 | const linkText = writer.createText(`🔗 ${file.name}`, {linkHref: url}) 85 | 86 | let selection 87 | if (filenameTxtPlaceholderRange) { 88 | selection = writer.createSelection(filenameTxtPlaceholderRange) 89 | } 90 | 91 | model.insertContent(linkText, selection || model.document.selection) 92 | 93 | // 回收 loader 94 | fileRepository.destroyLoader(loader) 95 | }) 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/plugin/Autoformat.js: -------------------------------------------------------------------------------- 1 | import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat' 2 | import BlockAutoformatEditing from '@ckeditor/ckeditor5-autoformat/src/blockautoformatediting' 3 | 4 | export default class TheAutoformat extends Autoformat { 5 | _addListAutoformats() { 6 | const commands = this.editor.commands 7 | 8 | if (commands.get('bulletedList')) { 9 | // eslint-disable-next-line no-new 10 | new BlockAutoformatEditing( 11 | this.editor, 12 | /^[*-]\s/, 13 | this._composeListener('bulletedList') 14 | ) 15 | } 16 | 17 | if (commands.get('numberedList')) { 18 | // eslint-disable-next-line no-new 19 | new BlockAutoformatEditing( 20 | this.editor, 21 | /^1\.\s/, 22 | this._composeListener('numberedList') 23 | ) 24 | } 25 | } 26 | 27 | _addHeadingAutoformats() { 28 | const command = this.editor.commands.get('heading') 29 | 30 | if (command) { 31 | command.modelElements 32 | .filter(name => name.match(/^heading[1-6]$/)) 33 | .forEach(commandValue => { 34 | const level = commandValue[7] 35 | const pattern = new RegExp(`^(#{${level}})\\s`) 36 | 37 | // eslint-disable-next-line no-new 38 | new BlockAutoformatEditing(this.editor, pattern, () => { 39 | if (!command.isEnabled) { 40 | return false 41 | } 42 | 43 | this.editor.execute('heading', {value: commandValue}) 44 | }) 45 | }) 46 | } 47 | } 48 | 49 | _composeListener(command) { 50 | return () => { 51 | const isComposing = this.editor.editing.view.document.isComposing 52 | if (isComposing) return false 53 | return this.editor.execute(command) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/plugin/Blockquote.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from './generateUIPlugin' 2 | import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting' 3 | import BlockQuoteUI from '@ckeditor/ckeditor5-block-quote/src/blockquoteui' 4 | 5 | import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview' 6 | import quoteIcon from '../assets/quotes.svg' 7 | 8 | class BlockQuoteUICustom extends BlockQuoteUI { 9 | /** 10 | * @inheritDoc 11 | */ 12 | init() { 13 | const editor = this.editor 14 | editor.ui.componentFactory.add('blockQuote', locale => { 15 | const command = editor.commands.get('blockQuote') 16 | const buttonView = new ButtonView(locale) 17 | 18 | buttonView.set({ 19 | label: '引用', 20 | icon: quoteIcon, 21 | tooltip: true, 22 | isToggleable: true 23 | }) 24 | 25 | // Bind button model to command. 26 | buttonView.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled') 27 | 28 | // Execute command. 29 | this.listenTo(buttonView, 'execute', () => editor.execute('blockQuote')) 30 | 31 | return buttonView 32 | }) 33 | } 34 | } 35 | 36 | export default generateUIPlugin('BlockQuote', [ 37 | BlockQuoteEditing, 38 | BlockQuoteUICustom 39 | ]) 40 | -------------------------------------------------------------------------------- /src/plugin/Bold.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from './generateUIPlugin' 2 | import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting' 3 | /** 4 | * @module basic-styles/bold/boldui 5 | */ 6 | 7 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 8 | import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview' 9 | 10 | import boldIcon from '../assets/bold.svg' 11 | 12 | const BOLD = 'bold' 13 | 14 | /** 15 | * The bold UI feature. It introduces the Bold button. 16 | * 17 | * @extends module:core/plugin~Plugin 18 | */ 19 | class BoldUI extends Plugin { 20 | /** 21 | * @inheritDoc 22 | */ 23 | init() { 24 | const editor = this.editor 25 | // Add bold button to feature components. 26 | editor.ui.componentFactory.add(BOLD, locale => { 27 | const command = editor.commands.get(BOLD) 28 | const view = new ButtonView(locale) 29 | 30 | view.set({ 31 | label: '加粗', 32 | icon: boldIcon, 33 | keystroke: 'CTRL+B', 34 | tooltip: true, 35 | isToggleable: true 36 | }) 37 | 38 | view.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled') 39 | 40 | // Execute command. 41 | this.listenTo(view, 'execute', () => editor.execute(BOLD)) 42 | 43 | return view 44 | }) 45 | } 46 | } 47 | 48 | export default generateUIPlugin('Bold', [BoldEditing, BoldUI]) 49 | -------------------------------------------------------------------------------- /src/plugin/DropdownButtonView.js: -------------------------------------------------------------------------------- 1 | import DropdownButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/dropdownbuttonview' 2 | import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview' 3 | import dropdownArrowIcon from '../assets/dropdown-arrow.svg' 4 | 5 | export default class DropdownButtonViewCustom extends DropdownButtonView { 6 | _createArrowView() { 7 | const arrowView = new IconView() 8 | 9 | arrowView.content = dropdownArrowIcon 10 | 11 | arrowView.extendTemplate({ 12 | attributes: { 13 | class: 'ck-dropdown__arrow' 14 | } 15 | }) 16 | 17 | return arrowView 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/plugin/Essentials.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from './generateUIPlugin' 2 | import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard' 3 | import Enter from '@ckeditor/ckeditor5-enter/src/enter' 4 | import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter' 5 | import Typing from '@ckeditor/ckeditor5-typing/src/typing' 6 | import Undo from './Undo' 7 | 8 | export default generateUIPlugin('Essentials', [ 9 | Clipboard, 10 | Enter, 11 | ShiftEnter, 12 | Typing, 13 | Undo 14 | ]) 15 | -------------------------------------------------------------------------------- /src/plugin/ExtraFormat.js: -------------------------------------------------------------------------------- 1 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 2 | import BlockAutoformatEditing from '@ckeditor/ckeditor5-autoformat/src/blockautoformatediting' 3 | import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element' 4 | 5 | export default class ExtraFormat extends Plugin { 6 | /** 7 | * @inheritDoc 8 | */ 9 | static get pluginName() { 10 | return 'ExtraFormat' 11 | } 12 | 13 | afterInit() { 14 | this._addListAutoformat() 15 | this._urlImageAutoformat() 16 | this._addHorizontalLineAutoFormat() 17 | } 18 | 19 | /** 20 | * 任务列表支持快捷键: [](Space) 21 | */ 22 | _addListAutoformat() { 23 | const commands = this.editor.commands 24 | 25 | if (commands.get('todoList')) { 26 | new BlockAutoformatEditing( 27 | this.editor, 28 | /^\[\]\s/, 29 | this._composeListener('todoList') 30 | ) 31 | } 32 | } 33 | 34 | /** 35 | * URL 图片 36 | */ 37 | _urlImageAutoformat() { 38 | new BlockAutoformatEditing( 39 | this.editor, 40 | /^!\[(\S*)\]\((.*?)\)\s$/, 41 | writer => { 42 | const [, imageAlt, imageUrl] = writer.match 43 | 44 | if (!imageUrl) { 45 | return false 46 | } 47 | 48 | const doc = this.editor.model.document 49 | 50 | const imageElement = new ModelElement('image', { 51 | src: imageUrl, 52 | alt: imageAlt 53 | }) 54 | 55 | this.editor.model.insertContent(imageElement, doc.selection) 56 | } 57 | ) 58 | } 59 | 60 | /** 61 | * 快捷插入分割线 62 | */ 63 | _addHorizontalLineAutoFormat() { 64 | const {editor} = this 65 | const cmd = 'horizontalLine' 66 | if (!editor.commands.get(cmd)) return 67 | const pat = /^---\s$/ 68 | new BlockAutoformatEditing(editor, pat, () => { 69 | const node = editor.model.document.selection.getFirstPosition().parent 70 | editor.model.enqueueChange(writer => { 71 | writer.remove(node) 72 | // 先 remove 再插入分割线,这样光标才会停在分割线下一行 73 | editor.execute(cmd) 74 | }) 75 | }) 76 | } 77 | 78 | _composeListener(command) { 79 | return () => { 80 | const isComposing = this.editor.editing.view.document.isComposing 81 | if (isComposing) return false 82 | return this.editor.execute(command) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/plugin/FixComposing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @hack 如果哪天崩了要看回 @ckeditor/ckeditor5-engine/src/view/{view|renderer}.js 源码 3 | * 已知负面影响:在线协作功能 4 | * 5 | * 详情看相关 issues & pr: 6 | * - https://github.com/ckeditor/ckeditor5/issues/5877 7 | * - https://github.com/ckeditor/ckeditor5-engine/pull/861 8 | * 9 | * PS:这个可能是更优的方案,如果还有问题的话可以参考 10 | * https://github.com/ckeditor/ckeditor5/issues/5877#issuecomment-566369164 11 | * https://github.com/mycolorway/ckeditor5-engine/commit/7d921207054e327088fe7496a8cde451ddd87423 12 | */ 13 | export default function(editor) { 14 | const {_renderer, document} = editor.editing.view 15 | /** 16 | * 注入 isComposing,在 render 中使用 17 | * @see https://github.com/ckeditor/ckeditor5-engine/pull/861/files#diff-644366cecbdf61171130f59482de38e0R114 18 | */ 19 | _renderer.bind('isComposing').to(document) 20 | const {render} = _renderer 21 | /** 22 | * 输入法进行间停止 render 23 | * https://github.com/ckeditor/ckeditor5-engine/pull/861/files#diff-4c87133ed830fc4ea0646426deba32cfR188 24 | */ 25 | _renderer.render = function() { 26 | // console.log('isComposing', this.isComposing) 27 | if (this.isComposing) return 28 | return render.call(this) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/plugin/Font/ColorTableView.js: -------------------------------------------------------------------------------- 1 | import ColorTableView from '@ckeditor/ckeditor5-font/src/ui/colortableview' 2 | import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview' 3 | 4 | import removeButtonIcon from '../../assets/eraser.svg' 5 | 6 | export default class ColorTableViewCustom extends ColorTableView { 7 | _removeColorButton() { 8 | const buttonView = new ButtonView() 9 | 10 | buttonView.set({ 11 | withText: true, 12 | icon: removeButtonIcon, 13 | tooltip: true, 14 | label: this.removeButtonLabel 15 | }) 16 | 17 | buttonView.class = 'ck-color-table__remove-color' 18 | buttonView.on('execute', () => { 19 | this.fire('execute', {value: null}) 20 | }) 21 | 22 | return buttonView 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/plugin/Font/ColorUI.js: -------------------------------------------------------------------------------- 1 | import ColorUI from '@ckeditor/ckeditor5-font/src/ui/colorui' 2 | import {addColorTableToDropdown} from './utils' 3 | import { 4 | normalizeColorOptions, 5 | getLocalizedColorOptions 6 | } from '@ckeditor/ckeditor5-font/src/utils' 7 | 8 | import {createDropdown} from '@ckeditor/ckeditor5-ui/src/dropdown/utils' 9 | import DropdownButtonView from '../DropdownButtonView' 10 | 11 | export default class ColorUICustom extends ColorUI { 12 | init() { 13 | const editor = this.editor 14 | const command = editor.commands.get(this.commandName) 15 | const colorsConfig = normalizeColorOptions( 16 | editor.config.get(this.componentName).colors 17 | ) 18 | const localizedColors = getLocalizedColorOptions(editor, colorsConfig) 19 | const documentColorsCount = editor.config.get( 20 | `${this.componentName}.documentColors` 21 | ) 22 | 23 | // Register the UI component. 24 | editor.ui.componentFactory.add(this.componentName, locale => { 25 | const dropdownView = createDropdown(locale, DropdownButtonView) 26 | this.colorTableView = addColorTableToDropdown({ 27 | dropdownView, 28 | colors: localizedColors.map(option => ({ 29 | label: option.label, 30 | color: option.model, 31 | options: { 32 | hasBorder: option.hasBorder 33 | } 34 | })), 35 | columns: this.columns, 36 | removeButtonLabel: '移除颜色', 37 | documentColorsLabel: 38 | documentColorsCount !== 0 ? '文档中的颜色' : undefined, 39 | documentColorsCount: 40 | documentColorsCount === undefined ? this.columns : documentColorsCount 41 | }) 42 | 43 | this.colorTableView.bind('selectedColor').to(command, 'value') 44 | 45 | dropdownView.buttonView.set({ 46 | label: this.dropdownLabel, 47 | icon: this.icon, 48 | tooltip: true 49 | }) 50 | 51 | dropdownView.extendTemplate({ 52 | attributes: { 53 | class: 'ck-color-ui-dropdown' 54 | } 55 | }) 56 | 57 | dropdownView.bind('isEnabled').to(command) 58 | 59 | dropdownView.on('execute', (evt, data) => { 60 | editor.execute(this.commandName, data) 61 | editor.editing.view.focus() 62 | }) 63 | 64 | dropdownView.on('change:isOpen', (evt, name, isVisible) => { 65 | if (isVisible) { 66 | if (documentColorsCount !== 0) { 67 | this.colorTableView.updateDocumentColors( 68 | editor.model, 69 | this.componentName 70 | ) 71 | } 72 | this.colorTableView.updateSelectedColors() 73 | } 74 | }) 75 | 76 | return dropdownView 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/plugin/Font/FontBackgroundColor.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from '../generateUIPlugin' 2 | import ColorUI from './ColorUI' 3 | 4 | import {FONT_BACKGROUND_COLOR} from '@ckeditor/ckeditor5-font/src/utils' 5 | import fontBackgroundColorIcon from '@ckeditor/ckeditor5-font/theme/icons/font-background.svg' 6 | 7 | import FontBackgroundColorEditing from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor/fontbackgroundcolorediting' 8 | class FontBackgroundColorUI extends ColorUI { 9 | /** 10 | * @inheritDoc 11 | */ 12 | constructor(editor) { 13 | super(editor, { 14 | commandName: FONT_BACKGROUND_COLOR, 15 | componentName: FONT_BACKGROUND_COLOR, 16 | icon: fontBackgroundColorIcon, 17 | dropdownLabel: '背景颜色' 18 | }) 19 | } 20 | 21 | /** 22 | * @inheritDoc 23 | */ 24 | static get pluginName() { 25 | return 'FontBackgroundColorUI' 26 | } 27 | } 28 | 29 | export default generateUIPlugin('FontBackgroundColor', [ 30 | FontBackgroundColorEditing, 31 | FontBackgroundColorUI 32 | ]) 33 | -------------------------------------------------------------------------------- /src/plugin/Font/FontColor.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from '../generateUIPlugin' 2 | import FontColorEditing from '@ckeditor/ckeditor5-font/src/fontcolor/fontcolorediting' 3 | 4 | import ColorUI from './ColorUI' 5 | import {FONT_COLOR} from '@ckeditor/ckeditor5-font/src/utils' 6 | import fontColorIcon from '@ckeditor/ckeditor5-font/theme/icons/font-color.svg' 7 | 8 | /** 9 | * The font color UI plugin. It introduces the `'fontColor'` dropdown. 10 | * 11 | * @extends module:core/plugin~Plugin 12 | */ 13 | class FontColorUI extends ColorUI { 14 | /** 15 | * @inheritDoc 16 | */ 17 | constructor(editor) { 18 | super(editor, { 19 | commandName: FONT_COLOR, 20 | componentName: FONT_COLOR, 21 | icon: fontColorIcon, 22 | dropdownLabel: '字体颜色' 23 | }) 24 | } 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | static get pluginName() { 30 | return 'FontColorUI' 31 | } 32 | } 33 | 34 | export default generateUIPlugin('FontColor', [FontColorEditing, FontColorUI]) 35 | -------------------------------------------------------------------------------- /src/plugin/Font/index.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from '../generateUIPlugin' 2 | import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily' 3 | import FontColor from './FontColor' 4 | import FontBackgroundColor from './FontBackgroundColor' 5 | 6 | export default generateUIPlugin('Font', [ 7 | FontFamily, 8 | FontColor, 9 | FontBackgroundColor 10 | ]) 11 | -------------------------------------------------------------------------------- /src/plugin/Font/utils.js: -------------------------------------------------------------------------------- 1 | import ColorTableView from './ColorTableView' 2 | export function addColorTableToDropdown({ 3 | dropdownView, 4 | colors, 5 | columns, 6 | removeButtonLabel, 7 | documentColorsLabel, 8 | documentColorsCount 9 | }) { 10 | const locale = dropdownView.locale 11 | const colorTableView = new ColorTableView(locale, { 12 | colors, 13 | columns, 14 | removeButtonLabel, 15 | documentColorsLabel, 16 | documentColorsCount 17 | }) 18 | 19 | dropdownView.colorTableView = colorTableView 20 | dropdownView.panelView.children.add(colorTableView) 21 | 22 | colorTableView.delegate('execute').to(dropdownView, 'execute') 23 | 24 | return colorTableView 25 | } 26 | -------------------------------------------------------------------------------- /src/plugin/FullScreen.js: -------------------------------------------------------------------------------- 1 | // 基础功能组件 2 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 3 | // 按钮视图组件 4 | import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview' 5 | 6 | // 按钮的图标 SVG 7 | import maxIcon from '../assets/maximize.svg' 8 | import minIcon from '../assets/minimize.svg' 9 | 10 | // 自定义组件需要继承基础组件 11 | export default class FullScreen extends Plugin { 12 | // 重写基础组件的初始化函数 13 | init() { 14 | const {editor} = this 15 | // 将自定义按钮添加到当前编辑器的组件工厂 16 | editor.ui.componentFactory.add('fullScreen', locale => { 17 | // 获取当前编辑器的按钮视图 18 | const view = new ButtonView(locale) 19 | 20 | // 在按钮视图中注入自定义按钮的名称、图标 21 | view.set({ 22 | label: '切换全屏', 23 | icon: maxIcon, 24 | // 开启提示,当鼠标悬浮到按钮上,会显示 label 指定的 fullscreen 字样 25 | tooltip: true 26 | }) 27 | 28 | // 点击按钮 29 | view.on('execute', () => { 30 | view.set({ 31 | icon: editor.ui.view.element.classList.contains('full-screen') 32 | ? maxIcon 33 | : minIcon 34 | }) 35 | // ckeditor 是挂载在传入 dom 后面的,有意思 36 | editor.ui.view.element.classList.toggle('full-screen') 37 | }) 38 | 39 | // 返回修改后的视图内容 40 | return view 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/plugin/Heading.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from './generateUIPlugin' 2 | import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting' 3 | 4 | import '@ckeditor/ckeditor5-heading/theme/heading.css' 5 | 6 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 7 | import Model from '@ckeditor/ckeditor5-ui/src/model' 8 | 9 | import { 10 | createDropdown, 11 | addListToDropdown 12 | } from '@ckeditor/ckeditor5-ui/src/dropdown/utils' 13 | import {getLocalizedOptions} from '@ckeditor/ckeditor5-heading/src/utils' 14 | import DropdownButtonView from './DropdownButtonView' 15 | 16 | import Collection from '@ckeditor/ckeditor5-utils/src/collection' 17 | 18 | import '@ckeditor/ckeditor5-heading/theme/heading.css' 19 | 20 | class HeadingUI extends Plugin { 21 | /** 22 | * @inheritDoc 23 | */ 24 | init() { 25 | const editor = this.editor 26 | const options = getLocalizedOptions(editor) 27 | const defaultTitle = '选择标题' 28 | const dropdownTooltip = '标题' 29 | 30 | // Register UI component. 31 | editor.ui.componentFactory.add('heading', locale => { 32 | const titles = {} 33 | const itemDefinitions = new Collection() 34 | 35 | const headingCommand = editor.commands.get('heading') 36 | const paragraphCommand = editor.commands.get('paragraph') 37 | 38 | const commands = [headingCommand] 39 | 40 | for (const option of options) { 41 | const def = { 42 | type: 'button', 43 | model: new Model({ 44 | label: option.title, 45 | class: option.class, 46 | withText: true 47 | }) 48 | } 49 | 50 | if (option.model === 'paragraph') { 51 | def.model.bind('isOn').to(paragraphCommand, 'value') 52 | def.model.set('commandName', 'paragraph') 53 | commands.push(paragraphCommand) 54 | } else { 55 | def.model 56 | .bind('isOn') 57 | .to(headingCommand, 'value', value => value === option.model) 58 | def.model.set({ 59 | commandName: 'heading', 60 | commandValue: option.model 61 | }) 62 | } 63 | 64 | // Add the option to the collection. 65 | itemDefinitions.add(def) 66 | 67 | titles[option.model] = option.title 68 | } 69 | 70 | const dropdownView = createDropdown(locale, DropdownButtonView) 71 | addListToDropdown(dropdownView, itemDefinitions) 72 | 73 | dropdownView.buttonView.set({ 74 | isOn: false, 75 | withText: true, 76 | tooltip: dropdownTooltip 77 | }) 78 | 79 | dropdownView.extendTemplate({ 80 | attributes: { 81 | class: ['ck-heading-dropdown'] 82 | } 83 | }) 84 | 85 | dropdownView 86 | .bind('isEnabled') 87 | .toMany(commands, 'isEnabled', (...areEnabled) => { 88 | return areEnabled.some(isEnabled => isEnabled) 89 | }) 90 | 91 | dropdownView.buttonView 92 | .bind('label') 93 | .to( 94 | headingCommand, 95 | 'value', 96 | paragraphCommand, 97 | 'value', 98 | (value, para) => { 99 | const whichModel = value || (para && 'paragraph') 100 | // If none of the commands is active, display default title. 101 | return titles[whichModel] ? titles[whichModel] : defaultTitle 102 | } 103 | ) 104 | 105 | // Execute command when an item from the dropdown is selected. 106 | this.listenTo(dropdownView, 'execute', evt => { 107 | editor.execute( 108 | evt.source.commandName, 109 | evt.source.commandValue ? {value: evt.source.commandValue} : undefined 110 | ) 111 | editor.editing.view.focus() 112 | }) 113 | 114 | return dropdownView 115 | }) 116 | } 117 | } 118 | 119 | export default generateUIPlugin('Heading', [HeadingEditing, HeadingUI]) 120 | -------------------------------------------------------------------------------- /src/plugin/Horizontalline.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from './generateUIPlugin' 2 | import HorizontalLineEditing from '@ckeditor/ckeditor5-horizontal-line/src/horizontallineediting' 3 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 4 | import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview' 5 | import horizontalLineIcon from '../assets/horizontalline.svg' 6 | 7 | class HorizontalLineUI extends Plugin { 8 | init() { 9 | const editor = this.editor 10 | // Add the `horizontalLine` button to feature components. 11 | editor.ui.componentFactory.add('horizontalLine', locale => { 12 | const command = editor.commands.get('horizontalLine') 13 | const view = new ButtonView(locale) 14 | 15 | view.set({ 16 | label: '分割线', 17 | icon: horizontalLineIcon, 18 | tooltip: true 19 | }) 20 | 21 | view.bind('isEnabled').to(command, 'isEnabled') 22 | 23 | // Execute the command. 24 | this.listenTo(view, 'execute', () => editor.execute('horizontalLine')) 25 | 26 | return view 27 | }) 28 | } 29 | } 30 | 31 | export default generateUIPlugin('HorizontalLine', [ 32 | HorizontalLineEditing, 33 | HorizontalLineUI 34 | ]) 35 | -------------------------------------------------------------------------------- /src/plugin/ImagePreview.js: -------------------------------------------------------------------------------- 1 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 2 | import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview' 3 | 4 | import imageIcon from '../assets/zoom.svg' 5 | 6 | export default previewFunc => 7 | class ImagePreview extends Plugin { 8 | static get pluginName() { 9 | return 'imagePreview' 10 | } 11 | init() { 12 | const editor = this.editor 13 | 14 | editor.ui.componentFactory.add('imagePreview', locale => { 15 | const view = new ButtonView(locale) 16 | view.set({ 17 | label: editor.t('Preview'), 18 | icon: imageIcon, 19 | tooltip: true 20 | }) 21 | 22 | // Callback executed once the image preview button is clicked. 23 | view.on('execute', () => { 24 | const el = this.editor.model.document.selection.getSelectedElement() 25 | const picUrl = el && el.getAttribute('src') 26 | previewFunc(picUrl) 27 | }) 28 | 29 | return view 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/plugin/ImageUpload.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from './generateUIPlugin' 2 | import ImageUploadProgress from '@ckeditor/ckeditor5-image/src/imageupload/imageuploadprogress' 3 | import ImageUploadEditing from '@ckeditor/ckeditor5-image/src/imageupload/imageuploadediting' 4 | 5 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 6 | import FileDialogButtonView from '@ckeditor/ckeditor5-upload/src/ui/filedialogbuttonview' 7 | import {createImageTypeRegExp} from '@ckeditor/ckeditor5-image/src/imageupload/utils' 8 | import imageIcon from '../assets/image.svg' 9 | 10 | class ImageUploadUI extends Plugin { 11 | /** 12 | * @inheritDoc 13 | */ 14 | init() { 15 | const editor = this.editor 16 | // Setup `imageUpload` button. 17 | editor.ui.componentFactory.add('imageUpload', locale => { 18 | const view = new FileDialogButtonView(locale) 19 | const command = editor.commands.get('imageUpload') 20 | const imageTypes = editor.config.get('image.upload.types') 21 | const imageTypesRegExp = createImageTypeRegExp(imageTypes) 22 | 23 | view.set({ 24 | acceptedType: imageTypes.map(type => `image/${type}`).join(','), 25 | allowMultipleFiles: true 26 | }) 27 | 28 | view.buttonView.set({ 29 | label: '插入图像', 30 | icon: imageIcon, 31 | tooltip: true 32 | }) 33 | 34 | view.buttonView.bind('isEnabled').to(command) 35 | 36 | view.on('done', (evt, files) => { 37 | const imagesToUpload = Array.from(files).filter(file => 38 | imageTypesRegExp.test(file.type) 39 | ) 40 | 41 | if (imagesToUpload.length) { 42 | editor.execute('imageUpload', {file: imagesToUpload}) 43 | } 44 | }) 45 | 46 | return view 47 | }) 48 | } 49 | } 50 | 51 | export default generateUIPlugin('ImageUpload', [ 52 | ImageUploadEditing, 53 | ImageUploadUI, 54 | ImageUploadProgress 55 | ]) 56 | -------------------------------------------------------------------------------- /src/plugin/Italic.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from './generateUIPlugin' 2 | import ItalicEditing from '@ckeditor/ckeditor5-basic-styles/src/italic/italicediting' 3 | 4 | /** 5 | * @module basic-styles/italic/italicui 6 | */ 7 | 8 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 9 | import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview' 10 | 11 | import italicIcon from '../assets/italic.svg' 12 | 13 | const ITALIC = 'italic' 14 | 15 | /** 16 | * The italic UI feature. It introduces the Italic button. 17 | * 18 | * @extends module:core/plugin~Plugin 19 | */ 20 | class ItalicUI extends Plugin { 21 | /** 22 | * @inheritDoc 23 | */ 24 | init() { 25 | const editor = this.editor 26 | 27 | // Add bold button to feature components. 28 | editor.ui.componentFactory.add(ITALIC, locale => { 29 | const command = editor.commands.get(ITALIC) 30 | const view = new ButtonView(locale) 31 | 32 | view.set({ 33 | label: '斜体', 34 | icon: italicIcon, 35 | keystroke: 'CTRL+I', 36 | tooltip: true, 37 | isToggleable: true 38 | }) 39 | 40 | view.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled') 41 | 42 | // Execute command. 43 | this.listenTo(view, 'execute', () => editor.execute(ITALIC)) 44 | 45 | return view 46 | }) 47 | } 48 | } 49 | 50 | export default generateUIPlugin('Italic', [ItalicEditing, ItalicUI]) 51 | -------------------------------------------------------------------------------- /src/plugin/Link.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from './generateUIPlugin' 2 | import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting' 3 | import LinkUI from '@ckeditor/ckeditor5-link/src/linkui' 4 | import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview' 5 | 6 | import linkIcon from '../assets/link.svg' 7 | const linkKeystroke = 'Ctrl+K' 8 | class LinkUICustom extends LinkUI { 9 | _createToolbarLinkButton() { 10 | const editor = this.editor 11 | const linkCommand = editor.commands.get('link') 12 | // Handle the `Ctrl+K` keystroke and show the panel. 13 | editor.keystrokes.set(linkKeystroke, (keyEvtData, cancel) => { 14 | // Prevent focusing the search bar in FF and opening new tab in Edge. #153, #154. 15 | cancel() 16 | 17 | if (linkCommand.isEnabled) { 18 | this._showUI(true) 19 | } 20 | }) 21 | 22 | editor.ui.componentFactory.add('link', locale => { 23 | const button = new ButtonView(locale) 24 | 25 | button.isEnabled = true 26 | button.label = '超链接' 27 | button.icon = linkIcon 28 | button.keystroke = linkKeystroke 29 | button.tooltip = true 30 | button.isToggleable = true 31 | 32 | // Bind button to the command. 33 | button.bind('isEnabled').to(linkCommand, 'isEnabled') 34 | button.bind('isOn').to(linkCommand, 'value', value => !!value) 35 | 36 | // Show the panel on button click. 37 | this.listenTo(button, 'execute', () => this._showUI(true)) 38 | 39 | return button 40 | }) 41 | } 42 | } 43 | 44 | export default generateUIPlugin('Link', [LinkEditing, LinkUICustom]) 45 | -------------------------------------------------------------------------------- /src/plugin/List.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from './generateUIPlugin' 2 | import ListEditing from '@ckeditor/ckeditor5-list/src/listediting' 3 | 4 | /** 5 | * @module list/listui 6 | */ 7 | 8 | import {createUIComponent} from '@ckeditor/ckeditor5-list/src/utils' 9 | 10 | import numberedListIcon from '../assets/numberedlist.svg' 11 | import bulletedListIcon from '../assets/bulletedlist.svg' 12 | 13 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 14 | 15 | /** 16 | * The list UI feature. It introduces the `'numberedList'` and `'bulletedList'` buttons that 17 | * allow to convert paragraphs to and from list items and indent or outdent them. 18 | * 19 | * @extends module:core/plugin~Plugin 20 | */ 21 | class ListUI extends Plugin { 22 | /** 23 | * @inheritDoc 24 | */ 25 | init() { 26 | // Create two buttons and link them with numberedList and bulletedList commands. 27 | createUIComponent(this.editor, 'numberedList', '有序列表', numberedListIcon) 28 | createUIComponent(this.editor, 'bulletedList', '无序列表', bulletedListIcon) 29 | } 30 | } 31 | 32 | export default generateUIPlugin('List', [ListEditing, ListUI]) 33 | -------------------------------------------------------------------------------- /src/plugin/Mediaembed/AutoMediaEmbed.js: -------------------------------------------------------------------------------- 1 | import AutoMediaEmbed from '@ckeditor/ckeditor5-media-embed/src/automediaembed' 2 | import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard' 3 | 4 | import Undo from '../Undo' 5 | 6 | export default class AutoMediaEmbedCustom extends AutoMediaEmbed { 7 | static get requires() { 8 | return [Clipboard, Undo] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/plugin/Mediaembed/index.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from '../generateUIPlugin' 2 | 3 | import MediaEmbedEditing from '@ckeditor/ckeditor5-media-embed/src/mediaembedediting' 4 | import AutoMediaEmbed from './AutoMediaEmbed' 5 | import MediaEmbedUI from '@ckeditor/ckeditor5-media-embed/src/mediaembedui' 6 | import MediaFormView from '@ckeditor/ckeditor5-media-embed/src/ui/mediaformview' 7 | import Widget from '@ckeditor/ckeditor5-widget/src/widget' 8 | 9 | import {createDropdown} from '@ckeditor/ckeditor5-ui/src/dropdown/utils' 10 | import DropdownButtonView from '../DropdownButtonView' 11 | 12 | import '@ckeditor/ckeditor5-media-embed/theme/mediaembed.css' 13 | 14 | import mediaIcon from '../../assets/media.svg' 15 | 16 | function getFormValidators(registry) { 17 | return [ 18 | form => { 19 | if (!form.url.length) { 20 | return 'URL不能为空' 21 | } 22 | }, 23 | form => { 24 | if (!registry.hasMedia(form.url)) { 25 | return '这个媒体链接不支持' 26 | } 27 | } 28 | ] 29 | } 30 | class MediaEmbedUICustom extends MediaEmbedUI { 31 | init() { 32 | const editor = this.editor 33 | const command = editor.commands.get('mediaEmbed') 34 | const registry = editor.plugins.get(MediaEmbedEditing).registry 35 | 36 | /** 37 | * The form view displayed inside the drop-down. 38 | * 39 | * @member {module:media-embed/ui/mediaformview~MediaFormView} 40 | */ 41 | this.form = new MediaFormView(getFormValidators(registry), editor.locale) 42 | 43 | // Setup `imageUpload` button. 44 | editor.ui.componentFactory.add('mediaEmbed', locale => { 45 | const dropdown = createDropdown(locale, DropdownButtonView) 46 | 47 | this._setUpDropdown(dropdown, this.form, command, editor) 48 | this._setUpForm(this.form, dropdown, command) 49 | 50 | return dropdown 51 | }) 52 | } 53 | _setUpDropdown(dropdown, form, command) { 54 | const editor = this.editor 55 | const button = dropdown.buttonView 56 | 57 | dropdown.bind('isEnabled').to(command) 58 | dropdown.panelView.children.add(form) 59 | 60 | button.set({ 61 | label: '插入媒体', 62 | icon: mediaIcon, 63 | tooltip: true 64 | }) 65 | 66 | button.on( 67 | 'open', 68 | () => { 69 | form.url = command.value || '' 70 | form.urlInputView.select() 71 | form.focus() 72 | }, 73 | {priority: 'low'} 74 | ) 75 | 76 | dropdown.on('submit', () => { 77 | if (form.isValid()) { 78 | editor.execute('mediaEmbed', form.url) 79 | closeUI() 80 | } 81 | }) 82 | 83 | dropdown.on('change:isOpen', () => form.resetFormStatus()) 84 | dropdown.on('cancel', () => closeUI()) 85 | 86 | function closeUI() { 87 | editor.editing.view.focus() 88 | dropdown.isOpen = false 89 | } 90 | } 91 | } 92 | 93 | export default generateUIPlugin('MediaEmbed', [ 94 | MediaEmbedEditing, 95 | MediaEmbedUICustom, 96 | AutoMediaEmbed, 97 | Widget 98 | ]) 99 | -------------------------------------------------------------------------------- /src/plugin/RemoveFormat/RemoveFormatLinks.js: -------------------------------------------------------------------------------- 1 | // https://ckeditor.com/docs/ckeditor5/latest/features/remove-format.html#integrating-with-editor-features 2 | export default function RemoveFormatLinks(editor) { 3 | // Extend the editor schema and mark the "linkHref" model attribute as formatting. 4 | editor.model.schema.setAttributeProperties('linkHref', { 5 | isFormatting: true 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /src/plugin/RemoveFormat/RemoveFormatUI.js: -------------------------------------------------------------------------------- 1 | import RemoveFormatUI from '@ckeditor/ckeditor5-remove-format/src/removeformatui' 2 | import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview' 3 | // 只有图标和原本不一样 4 | import removeButtonIcon from '../../assets/eraser.svg' 5 | 6 | const REMOVE_FORMAT = 'removeFormat' 7 | 8 | export default class extends RemoveFormatUI { 9 | init() { 10 | const editor = this.editor 11 | 12 | editor.ui.componentFactory.add(REMOVE_FORMAT, locale => { 13 | const command = editor.commands.get(REMOVE_FORMAT) 14 | const view = new ButtonView(locale) 15 | 16 | view.set({ 17 | label: '清除格式', 18 | icon: removeButtonIcon, 19 | tooltip: true 20 | }) 21 | 22 | view.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled') 23 | 24 | // Execute the command. 25 | this.listenTo(view, 'execute', () => { 26 | editor.execute(REMOVE_FORMAT) 27 | editor.editing.view.focus() 28 | }) 29 | 30 | return view 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/plugin/RemoveFormat/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/ckeditor/ckeditor5-remove-format/tree/master 3 | * 在官方 remove-format 基础上: 4 | * - links 也会被清除 5 | * - 图标改成橡皮擦,和 fontColor 的清除图标一样 6 | */ 7 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 8 | 9 | import RemoveFormatUI from './RemoveFormatUI' 10 | import RemoveFormatLinks from './RemoveFormatLinks' 11 | import RemoveFormatEditing from '@ckeditor/ckeditor5-remove-format/src/removeformatediting' 12 | 13 | export default class RemoveFormat extends Plugin { 14 | static get requires() { 15 | return [RemoveFormatEditing, RemoveFormatUI, RemoveFormatLinks] 16 | } 17 | static get pluginName() { 18 | return 'RemoveFormat' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/plugin/Strikethrough.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from './generateUIPlugin' 2 | import StrikethroughEditing from '@ckeditor/ckeditor5-basic-styles/src/strikethrough/strikethroughediting' 3 | /** 4 | * @module basic-styles/strikethrough/strikethroughui 5 | */ 6 | 7 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 8 | import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview' 9 | 10 | import strikethroughIcon from '../assets/strikethrough.svg' 11 | 12 | const STRIKETHROUGH = 'strikethrough' 13 | 14 | /** 15 | * The strikethrough UI feature. It introduces the Strikethrough button. 16 | * 17 | * @extends module:core/plugin~Plugin 18 | */ 19 | class StrikethroughUI extends Plugin { 20 | /** 21 | * @inheritDoc 22 | */ 23 | init() { 24 | const editor = this.editor 25 | 26 | // Add strikethrough button to feature components. 27 | editor.ui.componentFactory.add(STRIKETHROUGH, locale => { 28 | const command = editor.commands.get(STRIKETHROUGH) 29 | const view = new ButtonView(locale) 30 | 31 | view.set({ 32 | label: '删除线', 33 | icon: strikethroughIcon, 34 | keystroke: 'CTRL+SHIFT+X', 35 | tooltip: true, 36 | isToggleable: true 37 | }) 38 | 39 | view.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled') 40 | 41 | // Execute command. 42 | this.listenTo(view, 'execute', () => editor.execute(STRIKETHROUGH)) 43 | 44 | return view 45 | }) 46 | } 47 | } 48 | 49 | export default generateUIPlugin('Strikethrough', [ 50 | StrikethroughEditing, 51 | StrikethroughUI 52 | ]) 53 | -------------------------------------------------------------------------------- /src/plugin/Table.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from './generateUIPlugin' 2 | 3 | import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting' 4 | import TableUI from '@ckeditor/ckeditor5-table/src/tableui' 5 | import Widget from '@ckeditor/ckeditor5-widget/src/widget' 6 | 7 | import '@ckeditor/ckeditor5-table/theme/table.css' 8 | 9 | import {createDropdown} from '@ckeditor/ckeditor5-ui/src/dropdown/utils' 10 | import DropdownButtonView from './DropdownButtonView' 11 | import InsertTableView from '@ckeditor/ckeditor5-table/src/ui/inserttableview' 12 | 13 | import tableIcon from '../assets/table.svg' 14 | import tableColumnIcon from '@ckeditor/ckeditor5-table/theme/icons/table-column.svg' 15 | import tableRowIcon from '@ckeditor/ckeditor5-table/theme/icons/table-row.svg' 16 | import tableMergeCellIcon from '@ckeditor/ckeditor5-table/theme/icons/table-merge-cell.svg' 17 | 18 | class TableUICustom extends TableUI { 19 | init() { 20 | this.addInsertTable() 21 | this.addTableColumn() 22 | this.addTableRow() 23 | this.addMergeTableCells() 24 | } 25 | addInsertTable() { 26 | const editor = this.editor 27 | editor.ui.componentFactory.add('insertTable', locale => { 28 | const command = editor.commands.get('insertTable') 29 | const dropdownView = createDropdown(locale, DropdownButtonView) 30 | 31 | dropdownView.bind('isEnabled').to(command) 32 | 33 | // Decorate dropdown's button. 34 | dropdownView.buttonView.set({ 35 | icon: tableIcon, 36 | label: '插入表格', 37 | tooltip: true 38 | }) 39 | 40 | // Prepare custom view for dropdown's panel. 41 | const insertTableView = new InsertTableView(locale) 42 | dropdownView.panelView.children.add(insertTableView) 43 | 44 | insertTableView.delegate('execute').to(dropdownView) 45 | 46 | dropdownView.buttonView.on('open', () => { 47 | // Reset the chooser before showing it to the user. 48 | insertTableView.rows = 0 49 | insertTableView.columns = 0 50 | }) 51 | 52 | dropdownView.on('execute', () => { 53 | editor.execute('insertTable', { 54 | rows: insertTableView.rows, 55 | columns: insertTableView.columns 56 | }) 57 | editor.editing.view.focus() 58 | }) 59 | 60 | return dropdownView 61 | }) 62 | } 63 | addTableColumn() { 64 | const editor = this.editor 65 | const contentLanguageDirection = editor.locale.contentLanguageDirection 66 | const isContentLtr = contentLanguageDirection === 'ltr' 67 | editor.ui.componentFactory.add('tableColumn', locale => { 68 | const options = [ 69 | { 70 | type: 'switchbutton', 71 | model: { 72 | commandName: 'setTableColumnHeader', 73 | label: '标题列', 74 | bindIsOn: true 75 | } 76 | }, 77 | {type: 'separator'}, 78 | { 79 | type: 'button', 80 | model: { 81 | commandName: isContentLtr 82 | ? 'insertTableColumnLeft' 83 | : 'insertTableColumnRight', 84 | label: '向左插入列' 85 | } 86 | }, 87 | { 88 | type: 'button', 89 | model: { 90 | commandName: isContentLtr 91 | ? 'insertTableColumnRight' 92 | : 'insertTableColumnLeft', 93 | label: '向右插入列' 94 | } 95 | }, 96 | { 97 | type: 'button', 98 | model: { 99 | commandName: 'removeTableColumn', 100 | label: '删除列' 101 | } 102 | } 103 | ] 104 | 105 | return this._prepareDropdown('列', tableColumnIcon, options, locale) 106 | }) 107 | } 108 | addTableRow() { 109 | const editor = this.editor 110 | editor.ui.componentFactory.add('tableRow', locale => { 111 | const options = [ 112 | { 113 | type: 'switchbutton', 114 | model: { 115 | commandName: 'setTableRowHeader', 116 | label: '标题行', 117 | bindIsOn: true 118 | } 119 | }, 120 | {type: 'separator'}, 121 | { 122 | type: 'button', 123 | model: { 124 | commandName: 'insertTableRowBelow', 125 | label: '向下插入一行' 126 | } 127 | }, 128 | { 129 | type: 'button', 130 | model: { 131 | commandName: 'insertTableRowAbove', 132 | label: '向上插入一行' 133 | } 134 | }, 135 | { 136 | type: 'button', 137 | model: { 138 | commandName: 'removeTableRow', 139 | label: '删除本行' 140 | } 141 | } 142 | ] 143 | 144 | return this._prepareDropdown('行', tableRowIcon, options, locale) 145 | }) 146 | } 147 | addMergeTableCells() { 148 | const editor = this.editor 149 | const contentLanguageDirection = editor.locale.contentLanguageDirection 150 | const isContentLtr = contentLanguageDirection === 'ltr' 151 | editor.ui.componentFactory.add('mergeTableCells', locale => { 152 | const options = [ 153 | { 154 | type: 'button', 155 | model: { 156 | commandName: 'mergeTableCellUp', 157 | label: '向上合并单元格' 158 | } 159 | }, 160 | { 161 | type: 'button', 162 | model: { 163 | commandName: isContentLtr 164 | ? 'mergeTableCellRight' 165 | : 'mergeTableCellLeft', 166 | label: '向右合并单元格' 167 | } 168 | }, 169 | { 170 | type: 'button', 171 | model: { 172 | commandName: 'mergeTableCellDown', 173 | label: '向下合并单元格' 174 | } 175 | }, 176 | { 177 | type: 'button', 178 | model: { 179 | commandName: isContentLtr 180 | ? 'mergeTableCellLeft' 181 | : 'mergeTableCellRight', 182 | label: '向左合并单元格' 183 | } 184 | }, 185 | {type: 'separator'}, 186 | { 187 | type: 'button', 188 | model: { 189 | commandName: 'splitTableCellVertically', 190 | label: '垂直拆分单元格' 191 | } 192 | }, 193 | { 194 | type: 'button', 195 | model: { 196 | commandName: 'splitTableCellHorizontally', 197 | label: '水平拆分单元格' 198 | } 199 | } 200 | ] 201 | 202 | return this._prepareDropdown( 203 | '合并单元格', 204 | tableMergeCellIcon, 205 | options, 206 | locale 207 | ) 208 | }) 209 | } 210 | } 211 | 212 | export default generateUIPlugin('Table', [TableEditing, TableUICustom, Widget]) 213 | -------------------------------------------------------------------------------- /src/plugin/TodoList.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from './generateUIPlugin' 2 | import TodoListEditing from '@ckeditor/ckeditor5-list/src/todolistediting' 3 | /** 4 | * @module list/todolistui 5 | */ 6 | 7 | import {createUIComponent} from '@ckeditor/ckeditor5-list/src/utils' 8 | import todoListIcon from '../assets/todolist.svg' 9 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 10 | import '@ckeditor/ckeditor5-list/theme/todolist.css' 11 | 12 | /** 13 | * The to-do list UI feature. It introduces the `'todoList'` button that 14 | * allows to convert elements to and from to-do list items and to indent or outdent them. 15 | * 16 | * @extends module:core/plugin~Plugin 17 | */ 18 | class TodoListUI extends Plugin { 19 | /** 20 | * @inheritDoc 21 | */ 22 | init() { 23 | createUIComponent(this.editor, 'todoList', '待办列表', todoListIcon) 24 | } 25 | } 26 | 27 | export default generateUIPlugin('todoList', [TodoListEditing, TodoListUI]) 28 | -------------------------------------------------------------------------------- /src/plugin/TransformMD.js: -------------------------------------------------------------------------------- 1 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 2 | import marked from 'marked' 3 | import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor' 4 | 5 | export default class TransformMD extends Plugin { 6 | afterInit() { 7 | const editor = this.editor 8 | const model = editor.model 9 | const view = editor.editing.view 10 | const viewDocument = view.document 11 | // 把富文本转成编辑器看得懂的内容 12 | const dataProcessor = new HtmlDataProcessor(viewDocument) 13 | 14 | //等同 addEventListener 15 | this.listenTo(viewDocument, 'clipboardInput', (event, data) => { 16 | const dataTransfer = data.dataTransfer 17 | // 1 | - | * | # | ![] | []() 18 | const regex = /[-*1#]{1,6}\s+|!?\[\S*\][\s*|(\S*)].*?[-*1#]?/g 19 | let text = dataTransfer.getData('text/plain') 20 | 21 | // 确认是 markdown 应该八九不离十 22 | if (regex.test(text)) { 23 | let content = marked(text, { 24 | breaks: true 25 | }) 26 | content = dataProcessor.toView(content) 27 | 28 | if (!content.isEmpty) { 29 | const dataController = this.editor.data 30 | const modelFragment = dataController.toModel( 31 | content, 32 | '$clipboardHolder' 33 | ) 34 | 35 | model.insertContent(modelFragment) 36 | } 37 | view.scrollToTheSelection() 38 | // 终止事件,不会继续往下传; 39 | // 如果不满足,也就是 event 放行,即会接下去的监听事件继续工作 40 | event.stop() 41 | } 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/plugin/Underline.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from './generateUIPlugin' 2 | import UnderlineEditing from '@ckeditor/ckeditor5-basic-styles/src/underline/underlineediting' 3 | 4 | /** 5 | * @module basic-styles/underline/underlineui 6 | */ 7 | 8 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 9 | import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview' 10 | 11 | import underlineIcon from '../assets/underline.svg' 12 | 13 | const UNDERLINE = 'underline' 14 | 15 | /** 16 | * The underline UI feature. It introduces the Underline button. 17 | * 18 | * @extends module:core/plugin~Plugin 19 | */ 20 | class UnderlineUI extends Plugin { 21 | /** 22 | * @inheritDoc 23 | */ 24 | init() { 25 | const editor = this.editor 26 | 27 | // Add bold button to feature components. 28 | editor.ui.componentFactory.add(UNDERLINE, locale => { 29 | const command = editor.commands.get(UNDERLINE) 30 | const view = new ButtonView(locale) 31 | 32 | view.set({ 33 | label: '下划线', 34 | icon: underlineIcon, 35 | keystroke: 'CTRL+U', 36 | tooltip: true, 37 | isToggleable: true 38 | }) 39 | 40 | view.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled') 41 | 42 | // Execute command. 43 | this.listenTo(view, 'execute', () => editor.execute(UNDERLINE)) 44 | 45 | return view 46 | }) 47 | } 48 | } 49 | 50 | export default generateUIPlugin('Underline', [UnderlineEditing, UnderlineUI]) 51 | -------------------------------------------------------------------------------- /src/plugin/Undo.js: -------------------------------------------------------------------------------- 1 | import generateUIPlugin from './generateUIPlugin' 2 | import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting' 3 | import UndoUI from '@ckeditor/ckeditor5-undo/src/undoui' 4 | 5 | import undoIcon from '../assets/undo.svg' 6 | import redoIcon from '../assets/redo.svg' 7 | 8 | /** 9 | * The undo UI feature. It introduces the `'undo'` and `'redo'` buttons to the editor. 10 | * 11 | * @extends module:core/plugin~Plugin 12 | */ 13 | class UndoUICustom extends UndoUI { 14 | /** 15 | * @inheritDoc 16 | */ 17 | init() { 18 | const editor = this.editor 19 | const locale = editor.locale 20 | 21 | const localizedUndoIcon = 22 | locale.uiLanguageDirection == 'ltr' ? undoIcon : redoIcon 23 | const localizedRedoIcon = 24 | locale.uiLanguageDirection == 'ltr' ? redoIcon : undoIcon 25 | 26 | this._addButton('undo', '撤销', 'CTRL+Z', localizedUndoIcon) 27 | this._addButton('redo', '重做', 'CTRL+Y', localizedRedoIcon) 28 | } 29 | } 30 | export default generateUIPlugin('Undo', [UndoEditing, UndoUICustom]) 31 | -------------------------------------------------------------------------------- /src/plugin/Uploader.js: -------------------------------------------------------------------------------- 1 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 2 | import {UploadAdapter} from '../utils/adapter' 3 | 4 | /** 5 | * @param {function} uploadFunc upload function for upload image and attachment 6 | */ 7 | export default uploadFunc => 8 | class Uploader extends Plugin { 9 | /** 10 | * @inheritDoc 11 | */ 12 | static get pluginName() { 13 | return 'Uploader' 14 | } 15 | 16 | /** 17 | * @inheritDoc 18 | */ 19 | init() { 20 | const {editor} = this 21 | editor.plugins.get('FileRepository').createUploadAdapter = loader => { 22 | return new UploadAdapter(loader, uploadFunc) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/plugin/generateUIPlugin.js: -------------------------------------------------------------------------------- 1 | import Plugin from '@ckeditor/ckeditor5-core/src/plugin' 2 | 3 | export default (name, plugins) => 4 | class VEditorPlugin extends Plugin { 5 | /** 6 | * @inheritDoc 7 | */ 8 | static get requires() { 9 | return plugins 10 | } 11 | 12 | /** 13 | * @inheritDoc 14 | */ 15 | static get pluginName() { 16 | return name 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/translations/en.js: -------------------------------------------------------------------------------- 1 | import {add} from '@ckeditor/ckeditor5-utils/src/translation-service.js' 2 | 3 | add('en', { 4 | Preview: 'Preview' 5 | }) 6 | -------------------------------------------------------------------------------- /src/translations/index.js: -------------------------------------------------------------------------------- 1 | import './en.js' 2 | import './zh-cn.js' 3 | -------------------------------------------------------------------------------- /src/translations/zh-cn.js: -------------------------------------------------------------------------------- 1 | import {add} from '@ckeditor/ckeditor5-utils/src/translation-service.js' 2 | 3 | add('zh-cn', { 4 | Preview: '预览' 5 | }) 6 | -------------------------------------------------------------------------------- /src/utils/adapter.js: -------------------------------------------------------------------------------- 1 | export class UploadAdapter { 2 | constructor(loader, uploadFunc) { 3 | this.loader = loader 4 | this.uploadFunc = uploadFunc 5 | } 6 | 7 | /** 8 | * 9 | * @param {string} fileMIMEType mime-type 10 | * see: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types 11 | */ 12 | async upload() { 13 | try { 14 | const file = await this.loader.file 15 | // 图片多选时会逐个调用此方法 16 | const url = await this.uploadFunc(file) 17 | // 没有url意味着上传没有执行,需要reject 18 | if (url) { 19 | return {default: url} 20 | } else { 21 | return Promise.reject(url) 22 | } 23 | } catch (error) { 24 | return Promise.reject(error) 25 | } 26 | } 27 | 28 | abort() {} 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export function replaceNewlineWithBr(str) { 2 | return str.replace(/\r?\n/g, '
') 3 | } 4 | -------------------------------------------------------------------------------- /src/v-editor.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, {VueConstructor} from 'vue' 2 | 3 | declare module '@femessage/v-editor' { 4 | class FemessageComponent extends Vue { 5 | static install(vue: typeof Vue): void 6 | } 7 | 8 | type CombinedVueInstance< 9 | Instance extends Vue, 10 | Data, 11 | Methods, 12 | Computed, 13 | Props 14 | > = Data & Methods & Computed & Props & Instance 15 | 16 | type ExtendedVue< 17 | Instance extends Vue, 18 | Data, 19 | Methods, 20 | Computed, 21 | Props 22 | > = VueConstructor< 23 | CombinedVueInstance & Vue 24 | > 25 | 26 | type Combined = Data & 27 | Methods & 28 | Computed & 29 | Props 30 | 31 | type VEditorData = { 32 | editor: any 33 | ClassicEditor: any 34 | isFullScreen: boolean 35 | uploaderAccept: string 36 | previewImageUrl: string 37 | } 38 | 39 | type VEditorMethods = {} 40 | 41 | type VEditorComputed = {} 42 | 43 | type VEditorProps = { 44 | placeholder: string 45 | height: number | string 46 | uploadOptions: object 47 | value: string 48 | editorOptions: object 49 | disabled: boolean 50 | onUploadFail: (status: boolean, error?: any) => void 51 | autosize: object 52 | } 53 | 54 | type VEditor = Combined< 55 | VEditorData, 56 | VEditorMethods, 57 | VEditorComputed, 58 | VEditorProps 59 | > 60 | 61 | export interface VEditorType extends FemessageComponent, VEditor {} 62 | 63 | const VEditorConstruction: ExtendedVue< 64 | Vue, 65 | VEditorData, 66 | VEditorMethods, 67 | VEditorComputed, 68 | VEditorProps 69 | > 70 | 71 | export default VEditorConstruction 72 | } 73 | -------------------------------------------------------------------------------- /src/v-editor.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 269 | 270 | 382 | 383 | 474 | -------------------------------------------------------------------------------- /styleguide.config.js: -------------------------------------------------------------------------------- 1 | const {VueLoaderPlugin} = require('vue-loader') 2 | const {styles} = require('@ckeditor/ckeditor5-dev-utils') 3 | const CKEditorWebpackPlugin = require('@ckeditor/ckeditor5-dev-webpack-plugin') 4 | const path = require('path') 5 | const glob = require('glob') 6 | const env = Object.assign({}, require('dotenv').config().parsed, { 7 | UPLOAD_ACTION: process.env.UPLOAD_ACTION, 8 | OSS_BUCKET: process.env.OSS_BUCKET, 9 | OSS_REGION: process.env.OSS_REGION 10 | }) 11 | 12 | const demos = glob.sync('docs/!(basic).md') 13 | const demoSections = [ 14 | { 15 | name: 'basic', 16 | content: 'docs/basic.md' 17 | } 18 | ].concat( 19 | demos.map(filePath => ({ 20 | name: path.basename(filePath, '.md'), 21 | content: filePath 22 | })) 23 | ) 24 | 25 | module.exports = { 26 | styleguideDir: 'docs', 27 | pagePerSection: true, 28 | ribbon: { 29 | url: 'https://github.com/FEMessage/v-editor' 30 | }, 31 | require: ['./styleguide/element.js', './styleguide/upload-to-ali.js'], 32 | sections: [ 33 | { 34 | name: 'Components', 35 | components: 'src/*.vue', 36 | usageMode: 'expand' 37 | }, 38 | { 39 | name: 'Demo', 40 | sections: demoSections, 41 | sectionDepth: 2 42 | }, 43 | { 44 | name: 'FAQ', 45 | content: 'docs/faq.md' 46 | } 47 | ], 48 | webpackConfig: { 49 | module: { 50 | rules: [ 51 | { 52 | test: /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/, 53 | use: [ 54 | { 55 | loader: 'style-loader', 56 | options: { 57 | injectType: 'singletonStyleTag' 58 | } 59 | }, 60 | { 61 | loader: 'postcss-loader', 62 | options: styles.getPostCssConfig({ 63 | themeImporter: { 64 | themePath: require.resolve('@ckeditor/ckeditor5-theme-lark') 65 | }, 66 | minify: true 67 | }) 68 | } 69 | ] 70 | }, 71 | { 72 | test: /\.svg$/, 73 | use: ['raw-loader'] 74 | }, 75 | { 76 | test: /\.vue$/, 77 | loader: 'vue-loader' 78 | }, 79 | { 80 | test: /\.css$/, 81 | exclude: /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/, 82 | loaders: ['style-loader', 'css-loader'] 83 | }, 84 | { 85 | test: /\.less$/, 86 | loaders: ['vue-style-loader', 'css-loader', 'less-loader'] 87 | }, 88 | { 89 | test: /\.(woff2?|eot|[ot]tf)(\?.*)?$/, 90 | loader: 'file-loader' 91 | } 92 | ] 93 | }, 94 | plugins: [ 95 | new VueLoaderPlugin(), 96 | new CKEditorWebpackPlugin({ 97 | language: 'zh-cn', 98 | additionalLanguages: 'all' 99 | }), 100 | new (require('webpack')).DefinePlugin({ 101 | 'process.env': JSON.stringify(env) 102 | }) 103 | ] 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /styleguide/element.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Elm from 'element-ui' 3 | import 'element-ui/lib/theme-chalk/index.css' 4 | import CKEditorInspector from '@ckeditor/ckeditor5-inspector' 5 | 6 | Vue.use(Elm) 7 | window.CKEditorInspector = CKEditorInspector 8 | -------------------------------------------------------------------------------- /styleguide/upload-to-ali.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Upload from '@femessage/upload-to-ali' 3 | 4 | Vue.component('UploadToAli', Upload) 5 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import {replaceNewlineWithBr} from '../src/utils' 2 | 3 | test('replaceNewlineWithBr', () => { 4 | const strInWindows = '1.登录平台,选择需求池tab\r\n2.需求列表有需求' 5 | const strInUnix = '1.登录平台,选择需求池tab\n2.需求列表有需求' 6 | const normalStr = `1.登录平台,选择需求池tab
2.需求列表有需求` 7 | expect(replaceNewlineWithBr(strInWindows)).toBe(normalStr) 8 | expect(replaceNewlineWithBr(strInUnix)).toBe(normalStr) 9 | }) 10 | -------------------------------------------------------------------------------- /test/sum.js: -------------------------------------------------------------------------------- 1 | export default function sum(a, b) { 2 | return a + b 3 | } 4 | --------------------------------------------------------------------------------