├── .eslintignore ├── example ├── globals.d.ts ├── index.scss ├── index.tsx ├── src │ ├── app.module.scss.d.ts │ ├── app.module.scss │ └── app.tsx ├── index.html └── static │ └── help.md ├── .gitignore ├── src ├── lib │ ├── fonts │ │ ├── iconfont.eot │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ ├── iconfont.woff2 │ │ ├── iconfont.css.d.ts │ │ ├── iconfont.css │ │ └── iconfont.svg │ ├── css │ │ ├── index.scss.d.ts │ │ └── index.scss │ ├── lang │ │ ├── zh-CN │ │ │ └── index.json │ │ └── en │ │ │ └── index.json │ ├── helpers │ │ ├── highlight.ts │ │ ├── marked.ts │ │ ├── function.ts │ │ └── keydownListen.ts │ └── index.ts ├── components │ ├── toolbar_right.tsx │ └── toolbar_left.tsx └── index.tsx ├── .eslintrc.js ├── dist ├── static │ └── fonts │ │ ├── iconfont.0f693eba.eot │ │ ├── iconfont.a614fc0f.ttf │ │ ├── iconfont.fe07082d.woff │ │ └── iconfont.35e220a6.svg └── index.js ├── .babelrc ├── tsconfig.json ├── webpack ├── webpack.dist.config.js ├── webpack.dev.config.js ├── webpack.prod.config.js └── webpack.base.config.js ├── .prettierrc.js ├── LICENSE ├── doc └── UPDATELOG.md ├── index.d.ts ├── package.json ├── README.md └── README.EN.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /example/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.md' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | playground/ 3 | .DS_Store -------------------------------------------------------------------------------- /example/index.scss: -------------------------------------------------------------------------------- 1 | #root { 2 | height: 100%; 3 | } -------------------------------------------------------------------------------- /src/lib/fonts/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkfor/for-editor/HEAD/src/lib/fonts/iconfont.eot -------------------------------------------------------------------------------- /src/lib/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkfor/for-editor/HEAD/src/lib/fonts/iconfont.ttf -------------------------------------------------------------------------------- /src/lib/fonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkfor/for-editor/HEAD/src/lib/fonts/iconfont.woff -------------------------------------------------------------------------------- /src/lib/fonts/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkfor/for-editor/HEAD/src/lib/fonts/iconfont.woff2 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint-config-for/react', 'eslint-config-for/typescript'] 3 | } 4 | -------------------------------------------------------------------------------- /dist/static/fonts/iconfont.0f693eba.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkfor/for-editor/HEAD/dist/static/fonts/iconfont.0f693eba.eot -------------------------------------------------------------------------------- /dist/static/fonts/iconfont.a614fc0f.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkfor/for-editor/HEAD/dist/static/fonts/iconfont.a614fc0f.ttf -------------------------------------------------------------------------------- /dist/static/fonts/iconfont.fe07082d.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkfor/for-editor/HEAD/dist/static/fonts/iconfont.fe07082d.woff -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-proposal-class-properties", 7 | "@babel/plugin-transform-modules-commonjs" 8 | ] 9 | } -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './src/app' 4 | import './index.scss' 5 | 6 | ReactDOM.render(, document.getElementById('root')) 7 | -------------------------------------------------------------------------------- /src/lib/css/index.scss.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. 2 | // Please do not change this file! 3 | interface CssExports { 4 | 5 | } 6 | declare var cssExports: CssExports; 7 | export = cssExports; 8 | -------------------------------------------------------------------------------- /src/lib/fonts/iconfont.css.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. 2 | // Please do not change this file! 3 | interface CssExports { 4 | 5 | } 6 | declare var cssExports: CssExports; 7 | export = cssExports; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "resolveJsonModule": true, 5 | "esModuleInterop": true, 6 | "jsx": "react", 7 | "noImplicitAny": true 8 | }, 9 | "exclude": ["node_modules"] 10 | } -------------------------------------------------------------------------------- /example/src/app.module.scss.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. 2 | // Please do not change this file! 3 | interface CssExports { 4 | 'editor': string; 5 | 'main': string; 6 | 'top': string; 7 | } 8 | declare var cssExports: CssExports; 9 | export = cssExports; 10 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | for-editor | 基于react的markdown编辑器 8 | 9 | 10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /src/lib/lang/zh-CN/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "placeholder": "开始编辑...", 3 | "undo": "上一步", 4 | "redo": "下一步", 5 | "h1": "一级标题", 6 | "h2": "二级标题", 7 | "h3": "三级标题", 8 | "h4": "四级标题", 9 | "img": "添加图片链接", 10 | "link": "链接", 11 | "code": "代码块", 12 | "save": "保存", 13 | "preview": "预览", 14 | "singleColumn": "单栏", 15 | "doubleColumn": "双栏", 16 | "fullscreenOn": "全屏编辑", 17 | "fullscreenOff": "退出全屏", 18 | "addImgLink": "添加图片链接", 19 | "addImg": "上传图片" 20 | } 21 | -------------------------------------------------------------------------------- /webpack/webpack.dist.config.js: -------------------------------------------------------------------------------- 1 | const webpackBaseConfig = require('./webpack.base.config') 2 | const merge = require('webpack-merge') 3 | const path = require('path') 4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 5 | 6 | module.exports = merge(webpackBaseConfig, { 7 | mode: 'production', 8 | entry: './src/index.tsx', 9 | output: { 10 | path: path.resolve(__dirname, '../dist'), 11 | filename: 'index.js', 12 | publicPath: '/', 13 | libraryTarget: 'umd' 14 | }, 15 | plugins: [new CleanWebpackPlugin()] 16 | }) 17 | -------------------------------------------------------------------------------- /example/static/help.md: -------------------------------------------------------------------------------- 1 | > `for-editor` is a markdown editor 2 | 3 | # for-editor 4 | 5 | this is a markdown editor 6 | 7 | ## for-editor 8 | 9 | this is a markdown editor 10 | 11 | ### for-editor 12 | 13 | ```js 14 | const editor = 'for-editor' 15 | ``` 16 | 17 | - item1 18 | - subitem1 19 | - subitem2 20 | - subitem3 21 | - item2 22 | - item3 23 | 24 | --- 25 | 26 | 1. item1 27 | 2. item2 28 | 3. item3 29 | 30 | ### table 31 | 32 | | title | description | 33 | | ---------- | --------------- | 34 | | for-editor | markdown editor | 35 | -------------------------------------------------------------------------------- /src/lib/lang/en/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "placeholder": "Begin editing...", 3 | "undo": "Undo", 4 | "redo": "Redo", 5 | "h1": "Header 1", 6 | "h2": "Header 2", 7 | "h3": "Header 3", 8 | "h4": "Header 4", 9 | "img": "Image Link", 10 | "link": "Link", 11 | "code": "Code", 12 | "save": "Save", 13 | "preview": "Preview", 14 | "singleColumn": "Single Column", 15 | "doubleColumn": "Double Columns", 16 | "fullscreenOn": "FullScreen ON", 17 | "fullscreenOff": "FullScreen OFF", 18 | "addImgLink": "Add Image Link", 19 | "addImg": "Upload Image" 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/helpers/highlight.ts: -------------------------------------------------------------------------------- 1 | const Hljs = require('highlight.js/lib/highlight') 2 | 3 | Hljs.registerLanguage('css', require('highlight.js/lib/languages/css')) 4 | Hljs.registerLanguage('json', require('highlight.js/lib/languages/json')) 5 | Hljs.registerLanguage('less', require('highlight.js/lib/languages/less')) 6 | Hljs.registerLanguage('scss', require('highlight.js/lib/languages/scss')) 7 | Hljs.registerLanguage( 8 | 'javascript', 9 | require('highlight.js/lib/languages/javascript') 10 | ) 11 | Hljs.registerLanguage( 12 | 'typescript', 13 | require('highlight.js/lib/languages/typescript') 14 | ) 15 | 16 | export default Hljs 17 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import zhCN from './lang/zh-CN/index.json' 2 | import en from './lang/en/index.json' 3 | import { IToolbar, IWords } from '../index' 4 | export interface ICONFIG { 5 | language: { 6 | 'zh-CN': IWords 7 | en: IWords 8 | [key: string]: IWords 9 | } 10 | langList: string[] 11 | toolbar: IToolbar 12 | } 13 | 14 | export const CONFIG: ICONFIG = { 15 | language: { 16 | 'zh-CN': zhCN, 17 | en, 18 | }, 19 | langList: ['zh-CN', 'en'], 20 | toolbar: { 21 | h1: true, 22 | h2: true, 23 | h3: true, 24 | h4: true, 25 | img: true, 26 | link: true, 27 | code: true, 28 | preview: true, 29 | expand: true, 30 | undo: true, 31 | redo: true, 32 | save: true, 33 | subfield: true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/src/app.module.scss: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | height: 100%; 7 | margin: 0; 8 | } 9 | 10 | .main { 11 | padding-bottom: 40px; 12 | 13 | h1, 14 | ul, 15 | li { 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | ul, 21 | li { 22 | list-style: none; 23 | } 24 | 25 | a { 26 | color: inherit; 27 | } 28 | 29 | .top { 30 | display: flex; 31 | justify-content: space-between; 32 | height: 80px; 33 | line-height: 80px; 34 | padding: 0 20px; 35 | margin-bottom: 30px; 36 | font-size: 18px; 37 | color: #fff; 38 | background: linear-gradient(160deg, #852178, #019845); 39 | } 40 | 41 | .editor { 42 | width: 90%; 43 | max-width: 1400px; 44 | margin: 0 auto; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /webpack/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const webpackBaseConfig = require('./webpack.base.config') 2 | const merge = require('webpack-merge') 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | const HtmlWebpackPlugin = require('html-webpack-plugin') 6 | 7 | module.exports = merge(webpackBaseConfig, { 8 | mode: 'development', 9 | devtool: 'cheap-module-source-map', 10 | entry: ['./example/index.tsx'], 11 | output: { 12 | path: path.resolve(__dirname, '../dist'), 13 | filename: 'index.js' 14 | }, 15 | plugins: [ 16 | new HtmlWebpackPlugin({ 17 | template: './example/index.html' 18 | }), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | devServer: { 22 | historyApiFallback: true, 23 | disableHostCheck: true, 24 | inline: true, 25 | host: '0.0.0.0', 26 | port: 3020, 27 | hot: true 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 一行最多 100 字符 3 | printWidth: 100, 4 | // 使用 2 个空格缩进 5 | tabWidth: 2, 6 | // 不使用缩进符,而使用空格 7 | useTabs: false, 8 | // 行尾需要有分号 9 | semi: false, 10 | // 使用单引号 11 | singleQuote: true, 12 | // jsx 不使用单引号,而使用双引号 13 | jsxSingleQuote: false, 14 | // 末尾不需要逗号 15 | trailingComma: 'none', 16 | // 大括号内的首尾需要空格 17 | bracketSpacing: true, 18 | // jsx 标签的反尖括号需要换行 19 | jsxBracketSameLine: false, 20 | // 箭头函数,只有一个参数的时候,也需要括号 21 | arrowParens: 'always', 22 | // 每个文件格式化的范围是文件的全部内容 23 | rangeStart: 0, 24 | rangeEnd: Infinity, 25 | // 不需要写文件开头的 @prettier 26 | requirePragma: false, 27 | // 不需要自动在文件开头插入 @prettier 28 | insertPragma: false, 29 | // 使用默认的折行标准 30 | proseWrap: 'preserve', 31 | // 根据显示样式决定 html 要不要折行 32 | htmlWhitespaceSensitivity: 'css', 33 | // 换行符使用 lf 34 | endOfLine: 'lf' 35 | }; 36 | -------------------------------------------------------------------------------- /webpack/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const webpackBaseConfig = require('./webpack.base.config') 2 | const merge = require('webpack-merge') 3 | const path = require('path') 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 6 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 7 | const webpack = require('webpack') 8 | 9 | module.exports = merge(webpackBaseConfig, { 10 | mode: 'development', 11 | entry: ['./example/index.tsx'], 12 | output: { 13 | path: path.resolve(__dirname, '../playground'), 14 | filename: 'index.js', 15 | publicPath: '/', 16 | libraryTarget: 'umd' 17 | }, 18 | plugins: [ 19 | new CleanWebpackPlugin(), 20 | new HtmlWebpackPlugin({ 21 | template: './example/index.html' 22 | }), 23 | new webpack.DefinePlugin({ 24 | 'process.env.NODE_ENV': JSON.stringify('production') 25 | }), 26 | new UglifyJsPlugin() 27 | ] 28 | }) 29 | -------------------------------------------------------------------------------- /src/lib/helpers/marked.ts: -------------------------------------------------------------------------------- 1 | import marked from 'marked' 2 | import Hljs from './highlight' 3 | 4 | marked.setOptions({ 5 | renderer: new marked.Renderer(), 6 | gfm: true, 7 | tables: true, 8 | breaks: false, 9 | pedantic: false, 10 | sanitize: false, 11 | smartLists: true, 12 | smartypants: false, 13 | highlight(code: string) { 14 | return Hljs.highlightAuto(code).value 15 | } 16 | }) 17 | 18 | const renderer = new marked.Renderer() 19 | 20 | // 段落解析 21 | const paragraphParse = (text: string) => `

${text}

` 22 | 23 | // 链接解析 24 | const linkParse = (href: string, title: string, text: string) => { 25 | return `${text}` 29 | } 30 | 31 | renderer.paragraph = paragraphParse 32 | renderer.link = linkParse 33 | 34 | export default (content: string) => { 35 | if (typeof content !== 'string') return '' 36 | 37 | return marked(content, { renderer }) 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 kkfor 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. -------------------------------------------------------------------------------- /src/lib/helpers/function.ts: -------------------------------------------------------------------------------- 1 | function insertText($vm: HTMLTextAreaElement, params: any): string { 2 | const { prefix, str = '', subfix = '' } = params 3 | const value = $vm.value 4 | if ($vm.selectionStart || $vm.selectionStart === 0) { 5 | const start = $vm.selectionStart 6 | const end = $vm.selectionEnd 7 | 8 | const restoreTop = $vm.scrollTop 9 | 10 | if (start === end) { 11 | $vm.value = 12 | value.substring(0, start) + 13 | prefix + 14 | str + 15 | subfix + 16 | value.substring(end, value.length) 17 | $vm.selectionStart = start + prefix.length 18 | $vm.selectionEnd = end + prefix.length + str.length 19 | } else { 20 | $vm.value = 21 | value.substring(0, start) + 22 | prefix + 23 | value.substring(start, end) + 24 | subfix + 25 | value.substring(end, value.length) 26 | $vm.selectionStart = start + prefix.length 27 | $vm.selectionEnd = end + prefix.length 28 | } 29 | 30 | $vm.focus() 31 | if (restoreTop >= 0) { 32 | $vm.scrollTop = restoreTop 33 | } 34 | } 35 | return $vm.value 36 | } 37 | 38 | export { 39 | insertText 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/helpers/keydownListen.ts: -------------------------------------------------------------------------------- 1 | const KEY_CODE = { 2 | F8: 119, 3 | F9: 120, 4 | F10: 121, 5 | F11: 122, 6 | F12: 123, 7 | B: 66, 8 | I: 73, 9 | H: 72, 10 | U: 85, 11 | D: 68, 12 | M: 77, 13 | Q: 81, 14 | O: 79, 15 | L: 76, 16 | S: 83, 17 | Z: 90, 18 | Y: 89, 19 | C: 67, 20 | T: 84, 21 | R: 82, 22 | DELETE: 8, 23 | TAB: 9, 24 | ENTER: 13, 25 | ONE: 97, 26 | TWO: 98, 27 | THREE: 99, 28 | FOUR: 100, 29 | FIVE: 101, 30 | SIX: 102, 31 | _ONE: 49, 32 | _TWO: 50, 33 | _THREE: 51, 34 | _FOUR: 52, 35 | _FIVE: 53, 36 | _SIX: 54 37 | } 38 | 39 | export default ($vm: HTMLTextAreaElement, func: any) => { 40 | $vm.addEventListener('keydown', e => { 41 | if (!(e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey) { 42 | switch (e.keyCode) { 43 | case KEY_CODE.TAB: { 44 | e.preventDefault() 45 | func('tab') 46 | break 47 | } 48 | } 49 | } else if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey) { 50 | // ctrl + 51 | switch (e.keyCode) { 52 | case KEY_CODE.Z: { 53 | // Z 54 | e.preventDefault() 55 | func('undo') 56 | break 57 | } 58 | case KEY_CODE.Y: { 59 | // Y 60 | e.preventDefault() 61 | func('redo') 62 | break 63 | } 64 | case KEY_CODE.S: { 65 | // S 66 | e.preventDefault() 67 | func('save') 68 | break 69 | } 70 | } 71 | } 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /doc/UPDATELOG.md: -------------------------------------------------------------------------------- 1 | - 2019-12-11 v0.3.5 2 | - 修复双栏,预览,全屏bug [#45](https://github.com/kkfor/for-editor/pull/45) 3 | - 2019-08-20 v0.3.4 4 | - 修复typescrit定义问题 [#36](https://github.com/kkfor/for-editor/issues/35) 5 | - 2019-08-20 v0.3.2 6 | - 修复图片上传bug 7 | - 优化typescript d.ts声明 [#35](https://github.com/kkfor/for-editor/issues/35) 8 | - 2019-08-19 v0.3.1 9 | - 添加图片上传功能 [#32](https://github.com/kkfor/for-editor/issues/32) 10 | - 优化代码 11 | - 2019-07-05 v0.3.0 12 | - 添加d.ts声明文件,修复bug [#27](https://github.com/kkfor/for-editor/issues/27) 13 | - 2019-07-03 v0.2.9 14 | - 修复bug [#26](https://github.com/kkfor/for-editor/issues/26) 15 | - 2019-07-03 v0.2.8 16 | - 新增预览设置,支持中文、英文 17 | - 2019-07-01 v0.2.7 18 | - 修复bug [#24](https://github.com/kkfor/for-editor/issues/24) 19 | - 优化样式 20 | - 2019-06-21 v0.2.6 21 | - 优化一些样式 22 | - 2019-06-20 v0.2.5 23 | - 新增工具栏按钮显示隐藏功能 24 | - 优化预览过渡效果 25 | - 2019-06-19 v0.2.3 26 | - 修复双栏模式bug 27 | - 2019-06-19 v0.2.2 28 | - 增加同步滚动功能 29 | - 增加双栏模式 30 | - 2019-06-17 v0.2.1 31 | - 修复预览样式bug 32 | - 2019-06-17 v0.2.0 33 | - 重构项目,优化页面结构 34 | - 2019-02-02 v0.0.12 35 | - 修复编辑器自定义高度bug 36 | - 2019-01-10 v0.0.11 37 | - 优化代码预览样式 38 | - 2019-01-09 v0.0.10 39 | - 优化代码结构 40 | - 2019-01-07 v0.0.9 41 | - 新增上一步,下一步,tab快捷键功能 42 | - 新增保存功能 43 | - 优化图标状态 44 | - 优化页面样式 45 | - 2018-12-29 v0.0.8 46 | - 添加行号显示功能 47 | - 优化快捷插入标签时,光标选中文本内容 48 | - 修复异步加载数据时编辑框回显问题 49 | - 优化编辑区域行间距 50 | - 2018-12-27 v0.0.6 51 | - 优化图标按钮 52 | - 修改组件UnMount时错误bug 53 | - 新增组件placeholder属性 54 | - 2018-12-26 v0.0.5 55 | - 添加上一步,下一步按钮及功能 56 | - 2018-12-25 v0.0.4 57 | - 修复firefox下显示bug 58 | - 2018-12-24 v0.0.3 59 | - 增加全屏功能 60 | - 修改onChange参数为输入框内容 61 | - 优化编辑框输入字体 62 | - 修复快捷插入标签时,滚动条位置bug 63 | - 修复firefox下显示问题 64 | - 2018-12-23 v0.0.0 65 | - 编辑器基础功能,快捷插入markdown标签,预览功能 -------------------------------------------------------------------------------- /src/components/toolbar_right.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import classNames from 'classnames' 3 | import { IToolbar, IWords } from '../index' 4 | 5 | interface IP { 6 | onClick: (type: string) => void 7 | toolbar: IToolbar 8 | preview: boolean 9 | expand: boolean 10 | subfield: boolean 11 | words: IWords 12 | } 13 | 14 | class Toolbars extends React.Component { 15 | static defaultProps = { 16 | onClick: () => {}, 17 | toolbars: {}, 18 | words: {} 19 | } 20 | 21 | onClick(type: string) { 22 | this.props.onClick(type) 23 | } 24 | 25 | render() { 26 | const { preview, expand, subfield, toolbar, words } = this.props 27 | 28 | const previewActive = classNames({ 29 | 'for-active': preview 30 | }) 31 | const expandActive = classNames({ 32 | 'for-active': expand 33 | }) 34 | const subfieldActive = classNames({ 35 | 'for-active': subfield 36 | }) 37 | return ( 38 | 75 | ) 76 | } 77 | } 78 | 79 | export default Toolbars 80 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | export interface IToolbar { 3 | h1?: boolean 4 | h2?: boolean 5 | h3?: boolean 6 | h4?: boolean 7 | img?: boolean 8 | link?: boolean 9 | code?: boolean 10 | preview?: boolean 11 | expand?: boolean 12 | undo?: boolean 13 | redo?: boolean 14 | save?: boolean 15 | subfield?: boolean 16 | } 17 | export interface IWords { 18 | placeholder?: string 19 | h1?: string 20 | h2?: string 21 | h3?: string 22 | h4?: string 23 | undo?: string 24 | redo?: string 25 | img?: string 26 | link?: string 27 | code?: string 28 | save?: string 29 | preview?: string 30 | singleColumn?: string 31 | doubleColumn?: string 32 | fullscreenOn?: string 33 | fullscreenOff?: string 34 | addImgLink?: string 35 | addImg?: string 36 | } 37 | interface IP { 38 | value?: string 39 | lineNum?: number 40 | onChange?: (value: string) => void 41 | onSave?: (value: string) => void 42 | placeholder?: string 43 | fontSize?: string 44 | disabled?: boolean 45 | style?: object 46 | height?: string 47 | preview?: boolean 48 | expand?: boolean 49 | subfield?: boolean 50 | toolbar?: IToolbar 51 | language?: string 52 | addImg?: (file: File, index: number) => void 53 | } 54 | interface IS { 55 | preview: boolean 56 | expand: boolean 57 | subfield: boolean 58 | history: string[] 59 | historyIndex: number 60 | lineIndex: number 61 | value: string 62 | words: IWords 63 | } 64 | declare class MdEditor extends React.Component { 65 | static defaultProps: { 66 | lineNum: boolean 67 | onChange: () => void 68 | onSave: () => void 69 | addImg: () => void 70 | fontSize: string 71 | disabled: boolean 72 | preview: boolean 73 | expand: boolean 74 | subfield: boolean 75 | style: {} 76 | toolbar: IToolbar 77 | language: string 78 | }; 79 | 80 | $img2Url: (name: string, url: string) => void; 81 | 82 | private $vm; 83 | private $scrollEdit; 84 | private $scrollPreview; 85 | private $blockEdit; 86 | private $blockPreview; 87 | private currentTimeout; 88 | } 89 | export default MdEditor 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "for-editor", 3 | "version": "0.3.5", 4 | "description": "react markdown editor", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "start": "npm run dev", 8 | "dev": "webpack-dev-server --config webpack/webpack.dev.config.js", 9 | "build": "webpack --config webpack/webpack.prod.config.js", 10 | "dist": "webpack --config webpack/webpack.dist.config.js", 11 | "lint": "eslint --ext .js,.jsx --ignore-path .gitignore ." 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/kkfor/for-editor.git" 16 | }, 17 | "keywords": [ 18 | "javascript", 19 | "react", 20 | "markdown", 21 | "editor" 22 | ], 23 | "author": "kkfor", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/kkfor/for-editor/issues" 27 | }, 28 | "files": [ 29 | "dist/", 30 | "index.d.ts", 31 | "LICENSE" 32 | ], 33 | "homepage": "https://github.com/kkfor/for-editor#readme", 34 | "devDependencies": { 35 | "@babel/core": "^7.2.2", 36 | "@babel/plugin-proposal-class-properties": "^7.2.3", 37 | "@babel/preset-env": "^7.2.3", 38 | "@babel/preset-react": "^7.0.0", 39 | "@types/classnames": "^2.2.9", 40 | "@types/marked": "^0.6.5", 41 | "@types/react": "^16.8.16", 42 | "@types/react-dom": "^16.8.4", 43 | "@typescript-eslint/eslint-plugin": "^1.13.0", 44 | "@typescript-eslint/parser": "^1.13.0", 45 | "autoprefixer": "^9.5.1", 46 | "babel-core": "^7.0.0-bridge.0", 47 | "babel-eslint": "^10.0.2", 48 | "babel-loader": "^8.0.4", 49 | "classnames": "^2.2.6", 50 | "clean-webpack-plugin": "^3.0.0", 51 | "css-loader": "^2.0.2", 52 | "css-modules-typescript-loader": "^2.0.4", 53 | "eslint": "^6.1.0", 54 | "eslint-config-for": "^0.0.1", 55 | "eslint-plugin-react": "^7.14.3", 56 | "file-loader": "^4.0.0", 57 | "html-webpack-plugin": "^3.2.0", 58 | "node-sass": "^4.11.0", 59 | "postcss-loader": "^3.0.0", 60 | "prettier": "^1.15.3", 61 | "raw-loader": "^3.0.0", 62 | "react": "^16.7.0", 63 | "react-dom": "^16.7.0", 64 | "sass-loader": "^7.1.0", 65 | "style-loader": "^0.23.1", 66 | "ts-loader": "^6.0.0", 67 | "typescript": "^3.5.3", 68 | "uglifyjs-webpack-plugin": "^2.1.0", 69 | "webpack": "^4.28.1", 70 | "webpack-cli": "^3.1.2", 71 | "webpack-dev-server": "^3.1.10", 72 | "webpack-merge": "^4.1.5" 73 | }, 74 | "dependencies": { 75 | "highlight.js": "^9.13.1", 76 | "marked": "^1.1.1" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /example/src/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Editor from '../../src/index' 3 | // import Editor from '../../dist' 4 | import * as styles from './app.module.scss' 5 | import value from '../static/help.md' 6 | 7 | interface IS { 8 | value: string 9 | mobile: boolean 10 | } 11 | 12 | 13 | class App extends Component<{}, IS> { 14 | 15 | private $vm = React.createRef() 16 | 17 | constructor(props: any) { 18 | super(props) 19 | 20 | this.state = { 21 | value: '', 22 | mobile: false 23 | } 24 | } 25 | 26 | componentDidMount() { 27 | this.resize() 28 | window.addEventListener('resize', () => { 29 | this.resize() 30 | }) 31 | setTimeout(() => { 32 | this.setState({ 33 | value 34 | }) 35 | }, 200) 36 | } 37 | 38 | resize() { 39 | if (window.matchMedia('(min-width: 768px)').matches) { 40 | this.setState({ 41 | mobile: false 42 | }) 43 | } else { 44 | this.setState({ 45 | mobile: true 46 | }) 47 | } 48 | } 49 | 50 | handleChange(value: string) { 51 | this.setState({ 52 | value 53 | }) 54 | } 55 | 56 | handleSave(value: string) { 57 | console.log('触发保存事件', value) 58 | } 59 | 60 | addImg($file: File) { 61 | this.$vm.current.$img2Url($file.name, 'file_url') 62 | console.log($file) 63 | } 64 | 65 | render() { 66 | const { value, mobile } = this.state 67 | 68 | return ( 69 |
70 |
71 |

for-editor

72 | 83 |
84 |
85 | {mobile && ( 86 | this.handleChange(value)} 99 | onSave={value => this.handleSave(value)} 100 | /> 101 | )} 102 | {!mobile && ( 103 | this.addImg($file)} 109 | onChange={value => this.handleChange(value)} 110 | onSave={value => this.handleSave(value)} 111 | /> 112 | )} 113 |
114 |
115 | ) 116 | } 117 | } 118 | 119 | export default App 120 | -------------------------------------------------------------------------------- /webpack/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | // const isProd = process.env.NODE_ENV === 'production' 2 | const isDev = process.env.NODE_ENV === 'development' 3 | 4 | module.exports = { 5 | entry: './example/index.tsx', 6 | devtool: isDev && 'source-map', 7 | resolve: { 8 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'] 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(js|jsx)$/, 14 | exclude: /node_modules/, 15 | use: { 16 | loader: 'babel-loader', 17 | options: { 18 | presets: ['@babel/preset-env', '@babel/preset-react'], 19 | plugins: ['@babel/plugin-proposal-class-properties'] 20 | } 21 | } 22 | }, 23 | { 24 | test: /\.tsx?$/, 25 | exclude: /node_modules/, 26 | use: { 27 | loader: 'ts-loader' 28 | } 29 | }, 30 | { 31 | test: /\.css$/, 32 | use: [ 33 | { 34 | loader: 'style-loader' 35 | }, 36 | { 37 | loader: 'css-loader', 38 | options: { 39 | sourceMap: isDev, 40 | importLoaders: 1 41 | } 42 | }, 43 | { 44 | loader: 'postcss-loader', 45 | options: { 46 | plugins: [require('autoprefixer')], 47 | sourceMap: isDev 48 | } 49 | } 50 | ] 51 | }, 52 | { 53 | test: /\.scss$/, 54 | exclude: /\.module\.(sass|scss)$/, 55 | use: [ 56 | 'style-loader', 57 | { 58 | loader: 'css-loader', 59 | options: { 60 | sourceMap: isDev 61 | } 62 | }, 63 | { 64 | loader: 'postcss-loader', 65 | options: { 66 | plugins: [require('autoprefixer')], 67 | sourceMap: isDev 68 | } 69 | }, 70 | { 71 | loader: 'sass-loader', 72 | options: { 73 | sourceMap: isDev 74 | } 75 | } 76 | ] 77 | }, 78 | { 79 | test: /\.module\.(sass|scss)$/, 80 | use: [ 81 | require.resolve('style-loader'), 82 | require.resolve('css-modules-typescript-loader'), 83 | { 84 | loader: require.resolve('css-loader'), 85 | options: { 86 | importLoaders: 2, 87 | modules: true 88 | } 89 | }, 90 | { 91 | loader: 'postcss-loader', 92 | options: { 93 | plugins: [require('autoprefixer')], 94 | sourceMap: isDev 95 | } 96 | }, 97 | require.resolve('sass-loader') 98 | ] 99 | }, 100 | { 101 | test: /\.(woff2?|eot|ttf|svg)$/, 102 | loader: require.resolve('file-loader'), 103 | options: { 104 | name: 'static/fonts/[name].[hash:8].[ext]' 105 | } 106 | }, 107 | { 108 | test: /\.md$/, 109 | loader: require.resolve('raw-loader') 110 | } 111 | ] 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # for-editor 2 | 3 | > for-editor 是一个基于 react 的 markdown 语法编辑器 4 | 5 | ### [English Documents](./README.EN.md) 6 | 7 | - [demo](https://md.kkfor.com) 8 | - [github](https://github.com/kkfor/for-editor) 9 | 10 | ### 安装 11 | 12 | ```js 13 | npm install for-editor -S 14 | ``` 15 | 16 | ### 使用 17 | 18 | ```js 19 | import React, { Component } from 'react' 20 | import ReactDOM from 'react-dom' 21 | import Editor from 'for-editor' 22 | 23 | class App extends Component { 24 | constructor() { 25 | super() 26 | this.state = { 27 | value: '' 28 | } 29 | } 30 | 31 | handleChange(value) { 32 | this.setState({ 33 | value 34 | }) 35 | } 36 | 37 | render() { 38 | const { value } = this.state 39 | return this.handleChange()} /> 40 | } 41 | } 42 | 43 | ReactDOM.render(, document.getElementById('root')) 44 | ``` 45 | 46 | ### Api 47 | 48 | #### 属性 49 | 50 | | name | type | default | description | 51 | | ----------- | ------- | ----------- | ---------------------------------- | 52 | | value | String | - | 输入框内容 | 53 | | placeholder | String | 开始编辑... | 占位文本 | 54 | | lineNum | Boolean | true | 是否显示行号 | 55 | | style | Object | - | 编辑器样式 | 56 | | height | String | 600px | 编辑器高度 | 57 | | preview | Boolean | false | 预览模式 | 58 | | expand | Boolean | false | 全屏模式 | 59 | | subfield | Boolean | false | 双栏模式(预览模式激活下有效) | 60 | | language | String | zh-CN | 语言(支持 zh-CN:中文简体, en:英文) | 61 | | toolbar | Object | 如下 | 自定义工具栏 | 62 | 63 | ```js 64 | /* 65 | 默认工具栏按钮全部开启, 传入自定义对象 66 | 例如: { 67 | h1: true, // h1 68 | code: true, // 代码块 69 | preview: true, // 预览 70 | } 71 | 此时, 仅仅显示此三个功能键 72 | 注:传入空对象则不显示工具栏 73 | */ 74 | 75 | toolbar: { 76 | h1: true, // h1 77 | h2: true, // h2 78 | h3: true, // h3 79 | h4: true, // h4 80 | img: true, // 图片 81 | link: true, // 链接 82 | code: true, // 代码块 83 | preview: true, // 预览 84 | expand: true, // 全屏 85 | /* v0.0.9 */ 86 | undo: true, // 撤销 87 | redo: true, // 重做 88 | save: true, // 保存 89 | /* v0.2.3 */ 90 | subfield: true, // 单双栏模式 91 | } 92 | ``` 93 | 94 | #### 事件 95 | 96 | | name | params 参数 | default | description | 97 | | -------- | ------------- | ------- | -------------- | 98 | | onChange | String: value | - | 内容改变时回调 | 99 | | onSave | String: value | - | 保存时回调 | 100 | | addImg | File: file | - | 添加图片时回调 | 101 | 102 | ##### 图片上传 103 | 104 | ```js 105 | class App extends Component { 106 | constructor() { 107 | super() 108 | this.state = { 109 | value: '', 110 | } 111 | this.$vm = React.createRef() 112 | } 113 | 114 | handleChange(value) { 115 | this.setState({ 116 | value 117 | }) 118 | } 119 | 120 | addImg($file) { 121 | this.$vm.current.$img2Url($file.name, 'file_url') 122 | console.log($file) 123 | } 124 | 125 | render() { 126 | const { value } = this.state 127 | 128 | return ( 129 | this.addImg($file)} 133 | onChange={value => this.handleChange(value)} 134 | /> 135 | ) 136 | } 137 | } 138 | ``` 139 | 140 | #### 快捷键 141 | 142 | | name | description | 143 | | ------ | ------------ | 144 | | tab | 两个空格缩进 | 145 | | ctrl+s | 保存 | 146 | | ctrl+z | 上一步 | 147 | | ctrl+y | 下一步 | 148 | 149 | ### 更新 150 | 151 | - [Update Log](./doc/UPDATELOG.md) 152 | 153 | ### Licence 154 | 155 | for-editor is [MIT Licence](./LICENSE). 156 | -------------------------------------------------------------------------------- /src/components/toolbar_left.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { IToolbar, IWords } from '../index' 3 | interface IP { 4 | onClick: (type: string) => void 5 | addImg: (file: File, index: number) => void 6 | toolbar: IToolbar 7 | words: IWords 8 | } 9 | 10 | interface IS { 11 | imgHidden: boolean 12 | imgList: File[] 13 | } 14 | 15 | class Toolbars extends React.Component { 16 | static defaultProps = { 17 | onClick: () => {}, 18 | toolbar: {}, 19 | words: {} 20 | } 21 | 22 | private timer: number 23 | 24 | constructor(props: IP) { 25 | super(props) 26 | 27 | this.state = { 28 | imgHidden: true, 29 | imgList: [] 30 | } 31 | } 32 | 33 | onClick(type: string) { 34 | this.props.onClick(type) 35 | } 36 | 37 | imgClick() { 38 | this.setState({ 39 | imgHidden: !this.state.imgHidden 40 | }) 41 | } 42 | 43 | imgMouseOver() { 44 | window.clearTimeout(this.timer) 45 | this.setState({ 46 | imgHidden: false 47 | }) 48 | } 49 | 50 | imgMouseOut() { 51 | this.timer = window.setTimeout(() => { 52 | this.setState({ 53 | imgHidden: true 54 | }) 55 | }, 150) 56 | } 57 | 58 | addImgUrl() { 59 | this.props.onClick('img') 60 | } 61 | 62 | addImgFile(e: any) { 63 | let { imgList } = this.state 64 | const index = imgList.length 65 | imgList.push(e.target.files[0]) 66 | this.setState({ 67 | imgList 68 | }) 69 | this.props.addImg(e.target.files[0], index) 70 | e.target.value = '' 71 | } 72 | 73 | render() { 74 | const { toolbar, words } = this.props 75 | const { imgHidden } = this.state 76 | return ( 77 |
    78 | {toolbar.undo && ( 79 |
  • this.onClick('undo')} title={`${words.undo} (ctrl+z)`}> 80 | 81 |
  • 82 | )} 83 | {toolbar.redo && ( 84 |
  • this.onClick('redo')} title={`${words.redo} (ctrl+y)`}> 85 | 86 |
  • 87 | )} 88 | {toolbar.h1 && ( 89 |
  • this.onClick('h1')} title={words.h1}> 90 | H1 91 |
  • 92 | )} 93 | {toolbar.h2 && ( 94 |
  • this.onClick('h2')} title={words.h2}> 95 | H2 96 |
  • 97 | )} 98 | {toolbar.h3 && ( 99 |
  • this.onClick('h3')} title={words.h3}> 100 | H3 101 |
  • 102 | )} 103 | {toolbar.h4 && ( 104 |
  • this.onClick('h4')} title={words.h4}> 105 | H4 106 |
  • 107 | )} 108 | {toolbar.img && ( 109 |
  • this.imgMouseOver()} onMouseOut={() => this.imgMouseOut()}> 110 | 111 |
      112 |
    • this.addImgUrl()}>{words.addImgLink}
    • 113 |
    • 114 | {words.addImg} 115 | this.addImgFile(e)}/> 116 |
    • 117 |
    118 |
  • 119 | )} 120 | {toolbar.link && ( 121 |
  • this.onClick('link')} title={words.link}> 122 | 123 |
  • 124 | )} 125 | {toolbar.code && ( 126 |
  • this.onClick('code')} title={words.code}> 127 | 128 |
  • 129 | )} 130 | {toolbar.save && ( 131 |
  • this.onClick('save')} title={`${words.save} (ctrl+s)`}> 132 | 133 |
  • 134 | )} 135 |
136 | ) 137 | } 138 | } 139 | 140 | export default Toolbars 141 | -------------------------------------------------------------------------------- /src/lib/fonts/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face {font-family: "foricon"; 2 | src: url('iconfont.eot?t=1560850902819'); /* IE9 */ 3 | src: url('iconfont.eot?t=1560850902819#iefix') format('embedded-opentype'), /* IE6-IE8 */ 4 | url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'), 5 | url('iconfont.woff?t=1560850902819') format('woff'), 6 | url('iconfont.ttf?t=1560850902819') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ 7 | url('iconfont.svg?t=1560850902819#foricon') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .foricon { 11 | font-family: "foricon" !important; 12 | font-size: inherit; 13 | font-style: normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .for-code:before { 19 | content: "\e620"; 20 | } 21 | 22 | .for-image:before { 23 | content: "\e621"; 24 | } 25 | 26 | .for-eye:before { 27 | content: "\e622"; 28 | } 29 | 30 | .for-expand:before { 31 | content: "\e623"; 32 | } 33 | 34 | .for-redo:before { 35 | content: "\e624"; 36 | } 37 | 38 | .for-undo:before { 39 | content: "\e625"; 40 | } 41 | 42 | .for-quote:before { 43 | content: "\e626"; 44 | } 45 | 46 | .for-link:before { 47 | content: "\e627"; 48 | } 49 | 50 | .for-save:before { 51 | content: "\e628"; 52 | } 53 | 54 | .for-contract:before { 55 | content: "\e629"; 56 | } 57 | 58 | .for-eye-off:before { 59 | content: "\e62a"; 60 | } 61 | 62 | .for-subfield:before { 63 | content: "\e62b"; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /README.EN.md: -------------------------------------------------------------------------------- 1 | # for-editor 2 | 3 | > for-editor-A markdown editor based on React 4 | 5 | - [demo](https://md.kkfor.com) 6 | - [github](https://github.com/kkfor/for-editor) 7 | 8 | ### Install 9 | 10 | ```js 11 | npm install for-editor -S 12 | ``` 13 | 14 | ### Use 15 | 16 | ```js 17 | import React, { Component } from 'react' 18 | import ReactDOM from 'react-dom' 19 | import Editor from 'for-editor' 20 | 21 | class App extends Component { 22 | constructor() { 23 | super() 24 | this.state = { 25 | value: '' 26 | } 27 | } 28 | 29 | handleChange(value) { 30 | this.setState({ 31 | value 32 | }) 33 | } 34 | 35 | render() { 36 | const { value } = this.state 37 | return this.handleChange()} /> 38 | } 39 | } 40 | 41 | ReactDOM.render(, document.getElementById('root')) 42 | ``` 43 | 44 | ### Api 45 | 46 | #### props 47 | 48 | | name | type | default | description | 49 | | ----------- | ------- | --------------------------- | ------------------------------------------------------------------------------------------------------ | 50 | | value | String | - | value | 51 | | language | String | zh-CN | Language switch, zh-CN: Simplified Chinese, en: English | 52 | | placeholder | String | Begin editing... | The default prompt text when the textarea is empty | 53 | | lineNum | Boolean | true | Show lineNum | 54 | | style | Object | - | editor styles | 55 | | height | String | 600px | editor height | 56 | | preview | Boolean | false | preview switch | 57 | | expand | Boolean | false | fullscreen switch | 58 | | subfield | Boolean | false | true: Double columns - Edit preview same screen(notice: preview: true), Single Columns - otherwise not | 59 | | toolbar | Object | As in the following example | toolbars | 60 | 61 | ```js 62 | /* 63 | The default toolbar properties are all true, 64 | You can customize the object to cover them. 65 | eg: { 66 | h1: true, 67 | code: true, 68 | preview: true, 69 | } 70 | At this point, the toolbar only displays the three function keys. 71 | notice: Toolbar will be hidden when empty object. 72 | */ 73 | 74 | toolbar: { 75 | h1: true, 76 | h2: true, 77 | h3: true, 78 | h4: true, 79 | img: true, 80 | link: true, 81 | code: true, 82 | preview: true, 83 | expand: true, 84 | /* v0.0.9 */ 85 | undo: true, 86 | redo: true, 87 | save: true, 88 | /* v0.2.3 */ 89 | subfield: true 90 | } 91 | ``` 92 | 93 | #### events 94 | 95 | | name | params | default | description | 96 | | -------- | ------------- | ------- | ------------------------------------------- | 97 | | onChange | String: value | - | Edit area change callback event | 98 | | onSave | String: value | - | Ctrl+s and click save button callback event | 99 | | addImg | File: file | - | upload image callback event | 100 | 101 | ##### upload image 102 | 103 | ```js 104 | class App extends Component { 105 | constructor() { 106 | super() 107 | this.state = { 108 | value: '' 109 | } 110 | this.$vm = React.createRef() 111 | } 112 | 113 | handleChange(value) { 114 | this.setState({ 115 | value 116 | }) 117 | } 118 | 119 | addImg($file) { 120 | this.$vm.current.$img2Url($file.name, 'file_url') 121 | console.log($file) 122 | } 123 | 124 | render() { 125 | const { value } = this.state 126 | 127 | return ( 128 | this.addImg($file)} 132 | onChange={(value) => this.handleChange(value)} 133 | /> 134 | ) 135 | } 136 | } 137 | ``` 138 | 139 | #### hot key 140 | 141 | | name | description | 142 | | ------ | ----------- | 143 | | tab | two space | 144 | | ctrl+s | save | 145 | | ctrl+z | undo | 146 | | ctrl+y | redo | 147 | 148 | ### Update 149 | 150 | - [Update Log](./doc/UPDATELOG.md) 151 | 152 | # Licence 153 | 154 | for-editor is [MIT Licence](./LICENSE). 155 | -------------------------------------------------------------------------------- /src/lib/css/index.scss: -------------------------------------------------------------------------------- 1 | .for-container { 2 | font-family: 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif; 3 | display: flex; 4 | flex-direction: column; 5 | height: 600px; 6 | border: 1px solid #ddd; 7 | border-radius: 8px; 8 | box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 12px; 9 | background: #fff; 10 | font-size: 14px; 11 | 12 | ul, 13 | ol, 14 | li { 15 | margin: 0; 16 | padding: 0; 17 | } 18 | 19 | &.for-fullscreen { 20 | position: fixed; 21 | z-index: 99999; 22 | top: 0; 23 | right: 0; 24 | bottom: 0; 25 | left: 0; 26 | height: 100% !important; 27 | } 28 | 29 | > div { 30 | &:first-child { 31 | border-top-left-radius: 8px; 32 | border-top-right-radius: 8px; 33 | } 34 | } 35 | 36 | .for-hidden { 37 | display: none; 38 | } 39 | 40 | .for-toolbar { 41 | font-family: 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif; 42 | display: flex; 43 | justify-content: space-between; 44 | padding: 0 6px; 45 | border-bottom: 1px solid #ddd; 46 | color: #555; 47 | user-select: none; 48 | 49 | > ul { 50 | display: flex; 51 | 52 | > li { 53 | display: flex; 54 | align-items: center; 55 | padding: 4px 6px; 56 | margin: 8px 4px; 57 | border-radius: 4px; 58 | line-height: normal; 59 | 60 | &.for-toolbar-img { 61 | position: relative; 62 | 63 | > ul { 64 | position: absolute; 65 | top: 100%; 66 | left: -50px; 67 | width: 140px; 68 | margin-top: 4px; 69 | background: #fff; 70 | border-radius: 4px; 71 | box-shadow: rgba(0, 0, 0, 0.1) 0 2px 8px 0; 72 | z-index: 99; 73 | line-height: 2.8; 74 | text-align: center; 75 | 76 | li { 77 | position: relative; 78 | 79 | &:hover { 80 | background: #e9e9e9; 81 | } 82 | 83 | &:first-child { 84 | border-radius: 4px 4px 0 0; 85 | } 86 | 87 | &:last-child { 88 | border-radius: 0 0 4px 4px; 89 | } 90 | 91 | input { 92 | position: absolute; 93 | width: 100%; 94 | opacity: 0; 95 | left: 0; 96 | top: 0; 97 | bottom: 0; 98 | cursor: pointer; 99 | } 100 | } 101 | } 102 | } 103 | 104 | &.for-active { 105 | background: #ddd; 106 | } 107 | 108 | &:hover { 109 | cursor: pointer; 110 | background: #e9e9e9; 111 | } 112 | 113 | i { 114 | font-size: 1.2em; 115 | } 116 | } 117 | } 118 | } 119 | 120 | .for-editor { 121 | display: flex; 122 | justify-content: space-between; 123 | height: 100%; 124 | color: #2c3e50; 125 | border-radius: 0 0 8px 8px; 126 | overflow: hidden; 127 | 128 | .for-panel { 129 | height: 100%; 130 | flex: 0 0 100%; 131 | overflow: auto; 132 | transition: all 0.2s linear 0s; 133 | 134 | &.for-active { 135 | flex: 0 0 50%; 136 | } 137 | 138 | .for-preview { 139 | min-height: 100%; 140 | box-sizing: border-box; 141 | padding: 10px 14px; 142 | background: #fcfcfc; 143 | } 144 | } 145 | 146 | // 编辑区域 147 | .for-editor-edit { 148 | line-height: 1.6; 149 | height: 100%; 150 | 151 | &.for-edit-preview { 152 | width: 0; 153 | flex: 0 0 0; 154 | } 155 | .for-editor-block { 156 | display: flex; 157 | min-height: 100%; 158 | } 159 | 160 | .for-line-num { 161 | list-style: none; 162 | background: #eee; 163 | padding: 8px 0 120px; 164 | min-width: 30px; 165 | text-align: center; 166 | &.hidden { 167 | display: none; 168 | } 169 | li { 170 | list-style: none; 171 | } 172 | } 173 | 174 | .for-editor-content { 175 | flex: 1; 176 | position: relative; 177 | height: 100%; 178 | margin-left: 10px; 179 | 180 | pre { 181 | padding: 8px 0; 182 | display: block; 183 | white-space: pre-wrap; 184 | word-wrap: break-word; 185 | visibility: hidden; 186 | margin: 0; 187 | font-family: inherit; 188 | } 189 | } 190 | } 191 | } 192 | 193 | textarea { 194 | font-family: 'Consolas', 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif; 195 | box-sizing: border-box; 196 | position: absolute; 197 | top: 0; 198 | bottom: 0; 199 | padding: 8px 0; 200 | display: block; 201 | height: 100%; 202 | width: 100%; 203 | overflow: hidden; 204 | resize: none; 205 | border: none; 206 | outline: none; 207 | font-size: inherit; 208 | color: inherit; 209 | background: none; 210 | line-height: inherit; 211 | } 212 | 213 | .for-markdown-preview { 214 | line-height: 2; 215 | 216 | p, 217 | blockquote, 218 | ul, 219 | ol, 220 | dl, 221 | pre { 222 | margin-top: 0; 223 | margin-bottom: 0.6em; 224 | } 225 | 226 | h1, 227 | h2 { 228 | border-bottom: 1px solid #e2e2e2; 229 | } 230 | 231 | h1, 232 | h2, 233 | h3, 234 | h4, 235 | h5, 236 | h6 { 237 | padding: 0; 238 | margin: 0 0 0.6em; 239 | font-weight: 600; 240 | 241 | text-indent: 0; 242 | 243 | &:target { 244 | padding-top: 4.5rem; 245 | } 246 | } 247 | 248 | a { 249 | color: #0366d6; 250 | text-decoration: none; 251 | 252 | &:hover { 253 | text-decoration: underline; 254 | } 255 | } 256 | 257 | ul, 258 | ol { 259 | padding: 0.2em 0.8em; 260 | 261 | > li { 262 | line-height: 2; 263 | padding-left: 0.2em; 264 | margin-left: 0.2em; 265 | list-style-type: disc; 266 | 267 | > p { 268 | text-indent: 0; 269 | } 270 | 271 | > ul { 272 | &:last-child { 273 | margin-bottom: 0; 274 | } 275 | 276 | li { 277 | list-style-type: circle; 278 | 279 | > ul li { 280 | list-style-type: square; 281 | } 282 | } 283 | } 284 | } 285 | } 286 | 287 | > ul, 288 | ol { 289 | padding: 0 20px; 290 | } 291 | 292 | ol > li { 293 | list-style-type: decimal; 294 | } 295 | 296 | blockquote { 297 | margin: 0; 298 | margin-bottom: 0.6em; 299 | padding: 0 1em; 300 | color: #6a737d; 301 | border-left: 0.25em solid #dfe2e5; 302 | 303 | p { 304 | text-indent: 0; 305 | 306 | &:first-child { 307 | margin-top: 0; 308 | } 309 | 310 | &:last-child { 311 | margin-bottom: 0; 312 | } 313 | } 314 | } 315 | 316 | pre { 317 | padding: 0.6em; 318 | overflow: auto; 319 | line-height: 1.6; 320 | background-color: #f0f0f0; 321 | border-radius: 3px; 322 | 323 | code { 324 | padding: 0; 325 | margin: 0; 326 | font-size: 100%; 327 | background: transparent; 328 | } 329 | } 330 | 331 | code { 332 | padding: 0.2em 0.4em; 333 | margin: 0; 334 | background-color: #f0f0f0; 335 | border-radius: 3px; 336 | } 337 | 338 | hr { 339 | margin-bottom: 0.6em; 340 | height: 1px; 341 | background: #dadada; 342 | border: none; 343 | } 344 | 345 | table { 346 | width: 100%; 347 | border: 1px solid #ddd; 348 | margin-bottom: 0.6em; 349 | border-collapse: collapse; 350 | text-align: left; 351 | 352 | thead { 353 | background: #eee; 354 | } 355 | 356 | th, 357 | td { 358 | padding: 0.1em 0.4em; 359 | border: 1px solid #ddd; 360 | } 361 | } 362 | 363 | img { 364 | display: block; 365 | margin: 0 auto; 366 | max-width: 100%; 367 | margin-bottom: 0.6em; 368 | } 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/lib/fonts/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /dist/static/fonts/iconfont.35e220a6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import classNames from 'classnames' 3 | import marked from './lib/helpers/marked' 4 | import keydownListen from './lib/helpers/keydownListen' 5 | import ToolbarLeft from './components/toolbar_left' 6 | import ToolbarRight from './components/toolbar_right' 7 | import { insertText } from './lib/helpers/function' 8 | import 'highlight.js/styles/tomorrow.css' 9 | import './lib/fonts/iconfont.css' 10 | import './lib/css/index.scss' 11 | import { CONFIG } from './lib' 12 | 13 | export interface IToolbar { 14 | h1?: boolean 15 | h2?: boolean 16 | h3?: boolean 17 | h4?: boolean 18 | img?: boolean 19 | link?: boolean 20 | code?: boolean 21 | preview?: boolean 22 | expand?: boolean 23 | undo?: boolean 24 | redo?: boolean 25 | save?: boolean 26 | subfield?: boolean 27 | } 28 | 29 | export interface IWords { 30 | placeholder?: string 31 | h1?: string 32 | h2?: string 33 | h3?: string 34 | h4?: string 35 | undo?: string 36 | redo?: string 37 | img?: string 38 | link?: string 39 | code?: string 40 | save?: string 41 | preview?: string 42 | singleColumn?: string 43 | doubleColumn?: string 44 | fullscreenOn?: string 45 | fullscreenOff?: string 46 | addImgLink?: string 47 | addImg?: string 48 | } 49 | 50 | interface ILeft { 51 | prefix: string 52 | subfix: string 53 | str: string 54 | } 55 | interface IP { 56 | value?: string 57 | lineNum?: number 58 | onChange?: (value: string) => void 59 | onSave?: (value: string) => void 60 | placeholder?: string 61 | fontSize?: string 62 | disabled?: boolean 63 | style?: object 64 | height?: string 65 | preview?: boolean 66 | expand?: boolean 67 | subfield?: boolean 68 | toolbar?: IToolbar 69 | language?: string 70 | addImg?: (file: File, index: number) => void 71 | } 72 | 73 | interface IS { 74 | preview: boolean 75 | expand: boolean 76 | subfield: boolean 77 | history: string[] 78 | historyIndex: number 79 | lineIndex: number 80 | value: string 81 | words: IWords 82 | } 83 | 84 | class MdEditor extends React.Component { 85 | static defaultProps = { 86 | lineNum: true, 87 | onChange: () => { }, 88 | onSave: () => { }, 89 | addImg: () => { }, 90 | fontSize: '14px', 91 | disabled: false, 92 | preview: false, 93 | expand: false, 94 | subfield: false, 95 | style: {}, 96 | toolbar: CONFIG.toolbar, 97 | language: 'zh-CN' 98 | } 99 | private $vm = React.createRef() 100 | private $scrollEdit = React.createRef() 101 | private $scrollPreview = React.createRef() 102 | private $blockEdit = React.createRef() 103 | private $blockPreview = React.createRef() 104 | private currentTimeout: number 105 | constructor(props: IP) { 106 | super(props) 107 | 108 | this.state = { 109 | preview: props.preview, 110 | expand: props.expand, 111 | subfield: props.subfield, 112 | history: [], 113 | historyIndex: 0, 114 | lineIndex: 1, 115 | value: props.value, 116 | words: {} 117 | } 118 | } 119 | 120 | componentDidMount() { 121 | const { value } = this.props 122 | keydownListen(this.$vm.current, (type: string) => { 123 | this.toolBarLeftClick(type) 124 | }) 125 | this.reLineNum(value) 126 | this.initLanguage() 127 | } 128 | 129 | componentDidUpdate(preProps: IP) { 130 | const { value, preview, expand, subfield } = this.props 131 | const { history, historyIndex } = this.state 132 | if (preProps.value !== value) { 133 | this.reLineNum(value) 134 | } 135 | if (value !== history[historyIndex]) { 136 | window.clearTimeout(this.currentTimeout) 137 | this.currentTimeout = window.setTimeout(() => { 138 | this.saveHistory(value) 139 | }, 500) 140 | } 141 | if (subfield !== preProps.subfield && this.state.subfield !== subfield) { 142 | this.setState({ subfield }); 143 | } 144 | if (preview !== preProps.preview && this.state.preview !== preview) { 145 | this.setState({ preview }); 146 | } 147 | if (expand !== preProps.expand && this.state.expand !== expand) { 148 | this.setState({ expand }); 149 | } 150 | } 151 | 152 | initLanguage = (): void => { 153 | const { language } = this.props 154 | const lang = CONFIG.langList.indexOf(language) >= 0 ? language : 'zh-CN' 155 | this.setState({ 156 | words: CONFIG.language[lang] 157 | }) 158 | } 159 | 160 | // 输入框改变 161 | handleChange = (e: React.ChangeEvent): void => { 162 | const value = e.target.value 163 | this.props.onChange(value) 164 | } 165 | 166 | // 保存记录 167 | saveHistory = (value: string): void => { 168 | let { history, historyIndex } = this.state 169 | // 撤销后修改,删除当前以后记录 170 | history.splice(historyIndex + 1, history.length) 171 | if (history.length >= 20) { 172 | history.shift() 173 | } 174 | // 记录当前位置 175 | historyIndex = history.length 176 | history.push(value) 177 | this.setState({ 178 | history, 179 | historyIndex 180 | }) 181 | } 182 | 183 | // 重新计算行号 184 | reLineNum(value: string) { 185 | const lineIndex = value ? value.split('\n').length : 1 186 | this.setState({ 187 | lineIndex 188 | }) 189 | } 190 | 191 | // 保存 192 | save = (): void => { 193 | this.props.onSave(this.$vm.current!.value) 194 | } 195 | 196 | // 撤销 197 | undo = (): void => { 198 | let { history, historyIndex } = this.state 199 | historyIndex = historyIndex - 1 200 | if (historyIndex < 0) return 201 | this.props.onChange(history[historyIndex]) 202 | this.setState({ 203 | historyIndex 204 | }) 205 | } 206 | 207 | // 重做 208 | redo = (): void => { 209 | let { history, historyIndex } = this.state 210 | historyIndex = historyIndex + 1 211 | if (historyIndex >= history.length) return 212 | this.props.onChange(history[historyIndex]) 213 | this.setState({ 214 | historyIndex 215 | }) 216 | } 217 | 218 | // 菜单点击 219 | toolBarLeftClick = (type: string): void => { 220 | const { words } = this.state 221 | const insertTextObj: any = { 222 | h1: { 223 | prefix: '# ', 224 | subfix: '', 225 | str: words.h1 226 | }, 227 | h2: { 228 | prefix: '## ', 229 | subfix: '', 230 | str: words.h2 231 | }, 232 | h3: { 233 | prefix: '### ', 234 | subfix: '', 235 | str: words.h3 236 | }, 237 | h4: { 238 | prefix: '#### ', 239 | subfix: '', 240 | str: words.h4 241 | }, 242 | img: { 243 | prefix: '![alt](', 244 | subfix: ')', 245 | str: 'url' 246 | }, 247 | link: { 248 | prefix: '[title](', 249 | subfix: ')', 250 | str: 'url' 251 | }, 252 | code: { 253 | prefix: '```', 254 | subfix: '\n\n```', 255 | str: 'language' 256 | }, 257 | tab: { 258 | prefix: ' ', 259 | subfix: '', 260 | str: '' 261 | } 262 | } 263 | 264 | if (insertTextObj.hasOwnProperty(type)) { 265 | if (this.$vm.current) { 266 | const value = insertText(this.$vm.current, insertTextObj[type]) 267 | this.props.onChange(value) 268 | } 269 | } 270 | 271 | const otherLeftClick: any = { 272 | undo: this.undo, 273 | redo: this.redo, 274 | save: this.save 275 | } 276 | if (otherLeftClick.hasOwnProperty(type)) { 277 | otherLeftClick[type]() 278 | } 279 | } 280 | 281 | // 添加图片 282 | addImg = (file: File, index: number) => { 283 | this.props.addImg(file, index) 284 | } 285 | 286 | $img2Url = (name: string, url: string) => { 287 | const value = insertText(this.$vm.current, { 288 | prefix: `![${name}](${url})`, 289 | subfix: '', 290 | str: '' 291 | }) 292 | this.props.onChange(value) 293 | } 294 | 295 | // 右侧菜单 296 | toolBarRightClick = (type: string): void => { 297 | const toolbarRightPreviewClick = () => { 298 | this.setState({ 299 | preview: !this.state.preview 300 | }) 301 | } 302 | const toolbarRightExpandClick = () => { 303 | this.setState({ 304 | expand: !this.state.expand 305 | }) 306 | } 307 | 308 | const toolbarRightSubfieldClick = () => { 309 | const { preview, subfield } = this.state 310 | if (preview) { 311 | if (subfield) { 312 | this.setState({ 313 | subfield: false, 314 | preview: false 315 | }) 316 | } else { 317 | this.setState({ 318 | subfield: true 319 | }) 320 | } 321 | } else { 322 | if (subfield) { 323 | this.setState({ 324 | subfield: false 325 | }) 326 | } else { 327 | this.setState({ 328 | preview: true, 329 | subfield: true 330 | }) 331 | } 332 | } 333 | } 334 | 335 | const rightClick: any = { 336 | preview: toolbarRightPreviewClick, 337 | expand: toolbarRightExpandClick, 338 | subfield: toolbarRightSubfieldClick 339 | } 340 | if (rightClick.hasOwnProperty(type)) { 341 | rightClick[type]() 342 | } 343 | } 344 | 345 | focusText = (): void => { 346 | this.$vm.current!.focus() 347 | } 348 | 349 | handleScoll = (e: React.UIEvent): void => { 350 | const radio = 351 | this.$blockEdit.current!.scrollTop / 352 | (this.$scrollEdit.current!.scrollHeight - e.currentTarget.offsetHeight) 353 | this.$blockPreview.current!.scrollTop = 354 | (this.$scrollPreview.current!.scrollHeight - this.$blockPreview.current!.offsetHeight) * radio 355 | } 356 | 357 | render() { 358 | const { preview, expand, subfield, lineIndex, words } = this.state 359 | const { value, placeholder, fontSize, disabled, height, style, toolbar } = this.props 360 | const editorClass = classNames({ 361 | 'for-editor-edit': true, 362 | 'for-panel': true, 363 | 'for-active': preview && subfield, 364 | 'for-edit-preview': preview && !subfield 365 | }) 366 | const previewClass = classNames({ 367 | 'for-panel': true, 368 | 'for-editor-preview': true, 369 | 'for-active': preview && subfield 370 | }) 371 | const fullscreen = classNames({ 372 | 'for-container': true, 373 | 'for-fullscreen': expand 374 | }) 375 | const lineNumStyles = classNames({ 376 | 'for-line-num': true, 377 | hidden: !this.props.lineNum 378 | }) 379 | 380 | // 行号 381 | function lineNum() { 382 | const list = [] 383 | for (let i = 0; i < lineIndex; i++) { 384 | list.push(
  • {i + 1}
  • ) 385 | } 386 | return
      {list}
    387 | } 388 | 389 | return ( 390 |
    391 | {/* 菜单栏 */} 392 | {Boolean(Object.keys(toolbar).length) && ( 393 |
    394 | 401 | 409 |
    410 | )} 411 | {/* 内容区 */} 412 |
    413 | {/* 编辑区 */} 414 |
    420 |
    421 | {lineNum()} 422 |
    423 |
    {value} 
    424 |