├── .babelrc ├── .browserslistrc ├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ ├── demo.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .mocha-multi-reporters.json ├── .mocharc.yml ├── .npmignore ├── .nycrc ├── .prettierrc ├── LICENSE ├── README.md ├── demo ├── client │ ├── components │ │ └── DemoEditor │ │ │ └── index.js │ ├── index.js │ └── plugins │ │ └── prism.js ├── publicTemplate │ ├── .circleci │ │ └── config.yml │ ├── css │ │ ├── CheckableListItem.css │ │ ├── Draft.css │ │ ├── base.css │ │ ├── normalize.css │ │ └── prism.css │ └── index.html ├── webpack.config.dev.js └── webpack.config.prod.js ├── package.json ├── postcss.config.js ├── screen.gif ├── src ├── __test__ │ ├── plugin-test.js │ └── utils-test.js ├── components │ ├── Image │ │ ├── __test__ │ │ │ └── Image-test.js │ │ └── index.js │ └── Link │ │ ├── __test__ │ │ └── Link-test.js │ │ └── index.js ├── decorators │ ├── image │ │ ├── __test__ │ │ │ └── imageStrategy-test.js │ │ ├── imageStrategy.js │ │ └── index.js │ └── link │ │ ├── __test__ │ │ └── linkStrategy-test.js │ │ ├── index.js │ │ └── linkStrategy.js ├── index.js ├── modifiers │ ├── __test__ │ │ ├── adjustBlockDepth-test.js │ │ ├── changeCurrentBlockType-test.js │ │ ├── changeCurrentInlineStyle-test.js │ │ ├── handleBlockType-test.js │ │ ├── handleImage-test.js │ │ ├── handleInlineStyle-test.js │ │ ├── handleLink-test.js │ │ ├── handleNewCodeBlock-test.js │ │ ├── insertEmptyBlock-test.js │ │ ├── insertImage-test.js │ │ ├── insertLink-test.js │ │ ├── insertText-test.js │ │ └── leaveList-test.js │ ├── adjustBlockDepth.js │ ├── changeCurrentBlockType.js │ ├── changeCurrentInlineStyle.js │ ├── handleBlockType.js │ ├── handleImage.js │ ├── handleInlineStyle.js │ ├── handleLink.js │ ├── handleNewCodeBlock.js │ ├── insertEmptyBlock.js │ ├── insertImage.js │ ├── insertLink.js │ ├── insertText.js │ └── leaveList.js └── utils.js └── testHelper.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"], 4 | "env": { 5 | "development": { 6 | "presets": [] 7 | }, 8 | "test": { 9 | "plugins": ["rewire"] 10 | } 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "draft-js-markdown-shortcuts-plugin", 3 | "image": "node:12.18.2", 4 | "extensions": ["esbenp.prettier-vscode"], 5 | "postCreateCommand": "yarn install" 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | lib/** 3 | scripts/** 4 | coverage/** 5 | postcss.config.js 6 | public 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ "mocha" ], 4 | "env": { 5 | "browser": true, 6 | "mocha": true, 7 | "node": true 8 | }, 9 | "extends": ["airbnb", "prettier"], 10 | "rules": { 11 | "arrow-parens": ["error", "as-needed"], 12 | "max-len": 0, 13 | "comma-dangle": 0, 14 | "new-cap": 0, 15 | "react/prop-types": 0, 16 | "react/forbid-prop-types": 0, 17 | "react/prefer-stateless-function": 0, 18 | "react/jsx-filename-extension": 0, 19 | "import/no-extraneous-dependencies": 0, 20 | "import/prefer-default-export": 0, 21 | "jsx-a11y/no-static-element-interactions": 0, 22 | "class-methods-use-this": 0 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ngs 2 | issuehunt: ngs/ci2go 3 | custom: https://www.paypal.me/atsnngs 4 | -------------------------------------------------------------------------------- /.github/workflows/demo.yml: -------------------------------------------------------------------------------- 1 | name: Publish demo 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2-beta 11 | with: 12 | node-version: 12 13 | - run: npm install 14 | - run: npm run build:demo 15 | - name: Deploy 16 | uses: peaceiris/actions-gh-pages@v3 17 | with: 18 | github_token: ${{ secrets.GITHUB_TOKEN }} 19 | publish_dir: ./demo/public 20 | publish_branch: gh-pages 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2-beta 11 | with: 12 | node-version: 12 13 | - name: Check tag version 14 | run: | 15 | TAG_NAME=${{ github.event.release.tag_name }} 16 | VERSION=$(node -e 'console.info(require("./package.json").version)') 17 | if [ "v${VERSION}" != $TAG_NAME ]; then 18 | echo "Ref ${TAG_NAME} not match with v${VERSION}" 19 | exit 1 20 | fi 21 | - run: npm install 22 | - name: publish 23 | run: | 24 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc 25 | npm publish 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | pull_request: 7 | branches: 8 | - '*' 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v2-beta 15 | with: 16 | node-version: 12 17 | - run: npm install 18 | - uses: a-b-r-o-w-n/eslint-action@v1 19 | with: 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | - run: npm test 22 | - uses: coverallsapp/github-action@master 23 | with: 24 | github-token: ${{ secrets.GITHUB_TOKEN }} 25 | - uses: actions/upload-artifact@v2 26 | with: 27 | path: lib 28 | - uses: actions/upload-artifact@v2 29 | with: 30 | path: coverage 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | lib 40 | demo/public 41 | .deploy 42 | test-results.xml 43 | .node-version 44 | yarn.lock 45 | package-lock.json 46 | -------------------------------------------------------------------------------- /.mocha-multi-reporters.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "spec, mocha-junit-reporter" 3 | } 4 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | reporter: mocha-multi-reporters 2 | reporter-option: 3 | - 'configFile=.mocha-multi-reporters.json' 4 | recursive: true 5 | spec: 'src/**/*-test.js' 6 | 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/**/*.js 3 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "src/**/__test__/*.js", 4 | "testHelper.js" 5 | ], 6 | "reporter": [ 7 | "lcov", 8 | "text-summary", 9 | "html" 10 | ], 11 | "require": [ 12 | "./testHelper.js" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "arrowParens": "avoid" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Atsushi NAGASE 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.md: -------------------------------------------------------------------------------- 1 | # draft-js-markdown-shortcuts-plugin 2 | 3 | ![Run tests](https://github.com/ngs/draft-js-markdown-shortcuts-plugin/workflows/Run%20tests/badge.svg) 4 | [![Backers on Open Collective](https://opencollective.com/draft-js-markdown-shortcuts-plugin/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/draft-js-markdown-shortcuts-plugin/sponsors/badge.svg)](#sponsors) [![npm](https://img.shields.io/npm/v/draft-js-markdown-shortcuts-plugin.svg)][npm] 5 | [![Coverage Status](https://coveralls.io/repos/github/ngs/draft-js-markdown-shortcuts-plugin/badge.svg?branch=master)](https://coveralls.io/github/ngs/draft-js-markdown-shortcuts-plugin?branch=master) 6 | 7 | A [DraftJS] plugin for supporting Markdown syntax shortcuts 8 | 9 | This plugin works with [DraftJS Plugins] wrapper component. 10 | 11 | ![screen](screen.gif) 12 | 13 | [View Demo][demo] 14 | 15 | ## Usage 16 | 17 | ```sh 18 | npm i --save draft-js-markdown-shortcuts-plugin 19 | ``` 20 | 21 | then import from your editor component 22 | 23 | ```js 24 | import createMarkdownShortcutsPlugin from 'draft-js-markdown-shortcuts-plugin'; 25 | ``` 26 | 27 | ## Example 28 | 29 | ```js 30 | import React, { Component } from 'react'; 31 | import Editor from 'draft-js-plugins-editor'; 32 | import createMarkdownShortcutsPlugin from 'draft-js-markdown-shortcuts-plugin'; 33 | import { EditorState } from 'draft-js'; 34 | 35 | const plugins = [createMarkdownShortcutsPlugin()]; 36 | 37 | export default class DemoEditor extends Component { 38 | constructor(props) { 39 | super(props); 40 | this.state = { 41 | editorState: EditorState.createEmpty(), 42 | }; 43 | } 44 | 45 | onChange = editorState => { 46 | this.setState({ 47 | editorState, 48 | }); 49 | }; 50 | 51 | render() { 52 | return ; 53 | } 54 | } 55 | ``` 56 | 57 | ## License 58 | 59 | MIT. See [LICENSE] 60 | 61 | [demo]: https://ngs.github.io/draft-js-markdown-shortcuts-plugin 62 | [draftjs]: https://facebook.github.io/draft-js/ 63 | [draftjs plugins]: https://github.com/draft-js-plugins/draft-js-plugins 64 | [license]: ./LICENSE 65 | [npm]: https://www.npmjs.com/package/draft-js-markdown-shortcuts-plugin 66 | -------------------------------------------------------------------------------- /demo/client/components/DemoEditor/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Editor from 'draft-js-plugins-editor'; 3 | 4 | import Draft, { 5 | convertToRaw, 6 | // convertFromRaw, 7 | ContentState, 8 | EditorState, 9 | } from 'draft-js'; 10 | import Prism from 'prismjs'; 11 | import 'prismjs/components/prism-java'; 12 | import 'prismjs/components/prism-scala'; 13 | import 'prismjs/components/prism-go'; 14 | import 'prismjs/components/prism-sql'; 15 | import 'prismjs/components/prism-bash'; 16 | import 'prismjs/components/prism-c'; 17 | import 'prismjs/components/prism-cpp'; 18 | import 'prismjs/components/prism-kotlin'; 19 | import 'prismjs/components/prism-perl'; 20 | import 'prismjs/components/prism-ruby'; 21 | import 'prismjs/components/prism-swift'; 22 | import createPrismPlugin from 'draft-js-prism-plugin'; 23 | import styled from 'styled-components'; 24 | import createMarkdownShortcutsPlugin from '../../../..'; // eslint-disable-line 25 | 26 | const prismPlugin = createPrismPlugin({ 27 | prism: Prism, 28 | }); 29 | 30 | window.Draft = Draft; 31 | 32 | const plugins = [prismPlugin, createMarkdownShortcutsPlugin()]; 33 | 34 | export default class DemoEditor extends Component { 35 | constructor(props) { 36 | super(props); 37 | const contentState = ContentState.createFromText(''); 38 | const editorState = EditorState.createWithContent(contentState); 39 | this.state = { editorState }; 40 | } 41 | 42 | componentDidMount = () => { 43 | const { editor } = this; 44 | if (editor) { 45 | setTimeout(editor.focus.bind(editor), 1000); 46 | } 47 | }; 48 | 49 | onChange = editorState => { 50 | window.editorState = editorState; 51 | window.rawContent = convertToRaw(editorState.getCurrentContent()); 52 | 53 | this.setState({ 54 | editorState, 55 | }); 56 | }; 57 | 58 | render() { 59 | const { editorState } = this.state; 60 | const placeholder = editorState.getCurrentContent().hasText() ? null : ( 61 | Write something here... 62 | ); 63 | return ( 64 | 65 | {placeholder} 66 | 67 | { 73 | this.editor = element; 74 | }} 75 | /> 76 | 77 | 78 | ); 79 | } 80 | } 81 | 82 | const Root = styled.div` 83 | background: #fff; 84 | height: 100%; 85 | position: relative; 86 | `; 87 | 88 | const Placeholder = styled.div` 89 | color: #ccc; 90 | font-size: 1em; 91 | position: absolute; 92 | width: 95%; 93 | top: 0; 94 | left: 2.5%; 95 | `; 96 | 97 | const EditorContainer = styled.div` 98 | margin: 2.5% auto 0 auto; 99 | height: 95%; 100 | width: 95%; 101 | `; 102 | -------------------------------------------------------------------------------- /demo/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import Ribbon from 'react-github-fork-ribbon'; 4 | import DemoEditor from './components/DemoEditor'; 5 | 6 | // Import your routes so that you can pass them to the component 7 | // eslint-disable-next-line import/no-named-as-default 8 | 9 | // Only render in the browser 10 | if (typeof document !== 'undefined') { 11 | render( 12 |
13 | 19 | Fork me on GitHub 20 | 21 | 22 |
, 23 | document.getElementById('root'), 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /demo/client/plugins/prism.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Prism from 'prismjs'; 3 | import PrismDecorator from 'draft-js-prism'; 4 | import 'prismjs/components/prism-java'; 5 | import 'prismjs/components/prism-scala'; 6 | import 'prismjs/components/prism-go'; 7 | import 'prismjs/components/prism-sql'; 8 | import 'prismjs/components/prism-bash'; 9 | import 'prismjs/components/prism-c'; 10 | import 'prismjs/components/prism-cpp'; 11 | import 'prismjs/components/prism-kotlin'; 12 | import 'prismjs/components/prism-perl'; 13 | import 'prismjs/components/prism-ruby'; 14 | import 'prismjs/components/prism-swift'; 15 | 16 | const prismPlugin = { 17 | decorators: [ 18 | new PrismDecorator({ 19 | prism: Prism, 20 | getSyntax(block) { 21 | const language = block.getData().get('language'); 22 | if (typeof Prism.languages[language] === 'object') { 23 | return language; 24 | } 25 | return null; 26 | }, 27 | render({ type, children }) { 28 | return {children}; 29 | }, 30 | }), 31 | ], 32 | }; 33 | 34 | export default prismPlugin; 35 | -------------------------------------------------------------------------------- /demo/publicTemplate/.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | workflows: 4 | version: 2 5 | -------------------------------------------------------------------------------- /demo/publicTemplate/css/CheckableListItem.css: -------------------------------------------------------------------------------- 1 | .checkable-list-item{list-style:none;transform:translateX(-1.5em)}.checkable-list-item-block__checkbox{position:absolute;z-index:1;cursor:default}.checkable-list-item-block__text{padding-left:1.5em} -------------------------------------------------------------------------------- /demo/publicTemplate/css/Draft.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @providesModule DraftEditor 3 | * @permanent 4 | */ 5 | 6 | /** 7 | * We inherit the height of the container by default 8 | */ 9 | 10 | .DraftEditor-root, 11 | .DraftEditor-editorContainer, 12 | .public-DraftEditor-content { 13 | height: inherit; 14 | text-align: initial; 15 | } 16 | 17 | .DraftEditor-root { 18 | position: relative; 19 | } 20 | 21 | /** 22 | * Zero-opacity background used to allow focus in IE. Otherwise, clicks 23 | * fall through to the placeholder. 24 | */ 25 | 26 | .DraftEditor-editorContainer { 27 | background-color: rgba(255, 255, 255, 0); 28 | /* Repair mysterious missing Safari cursor */ 29 | border-left: 0.1px solid transparent; 30 | position: relative; 31 | z-index: 1; 32 | } 33 | 34 | .public-DraftEditor-content { 35 | outline: none; 36 | white-space: pre-wrap; 37 | } 38 | 39 | .public-DraftEditor-block { 40 | position: relative; 41 | } 42 | 43 | .DraftEditor-alignLeft .public-DraftEditor-block { 44 | text-align: left; 45 | } 46 | 47 | .DraftEditor-alignLeft .public-DraftEditorPlaceholder/root { 48 | left: 0; 49 | text-align: left; 50 | } 51 | 52 | .DraftEditor-alignCenter .public-DraftEditor-block { 53 | text-align: center; 54 | } 55 | 56 | .DraftEditor-alignCenter .public-DraftEditorPlaceholder/root { 57 | margin: 0 auto; 58 | text-align: center; 59 | width: 100%; 60 | } 61 | 62 | .DraftEditor-alignRight .public-DraftEditor-block { 63 | text-align: right; 64 | } 65 | 66 | .DraftEditor-alignRight .public-DraftEditorPlaceholder/root { 67 | right: 0; 68 | text-align: right; 69 | } 70 | /** 71 | * @providesModule DraftEditorPlaceholder 72 | */ 73 | 74 | .public-DraftEditorPlaceholder-root { 75 | color: #9197a3; 76 | position: absolute; 77 | z-index: 0; 78 | } 79 | 80 | .public-DraftEditorPlaceholder-hasFocus { 81 | color: #bdc1c9; 82 | } 83 | 84 | .DraftEditorPlaceholder-hidden { 85 | display: none; 86 | } 87 | /** 88 | * @providesModule DraftStyleDefault 89 | */ 90 | 91 | .public-DraftStyleDefault-block { 92 | position: relative; 93 | white-space: pre-wrap; 94 | } 95 | 96 | /* @noflip */ 97 | 98 | .public-DraftStyleDefault-ltr { 99 | direction: ltr; 100 | text-align: left; 101 | } 102 | 103 | /* @noflip */ 104 | 105 | .public-DraftStyleDefault-rtl { 106 | direction: rtl; 107 | text-align: right; 108 | } 109 | 110 | /** 111 | * These rules provide appropriate text direction for counter pseudo-elements. 112 | */ 113 | 114 | /* @noflip */ 115 | 116 | .public-DraftStyleDefault-listLTR { 117 | direction: ltr; 118 | } 119 | 120 | /* @noflip */ 121 | 122 | .public-DraftStyleDefault-listRTL { 123 | direction: rtl; 124 | } 125 | 126 | /** 127 | * Default spacing for list container elements. Override with CSS as needed. 128 | */ 129 | 130 | .public-DraftStyleDefault-ul, 131 | .public-DraftStyleDefault-ol { 132 | margin: 16px 0; 133 | padding: 0; 134 | } 135 | 136 | /** 137 | * Default counters and styles are provided for five levels of nesting. 138 | * If you require nesting beyond that level, you should use your own CSS 139 | * classes to do so. If you care about handling RTL languages, the rules you 140 | * create should look a lot like these. 141 | */ 142 | 143 | /* @noflip */ 144 | 145 | .public-DraftStyleDefault-depth0.public-DraftStyleDefault-listLTR { 146 | margin-left: 1.5em; 147 | } 148 | 149 | /* @noflip */ 150 | 151 | .public-DraftStyleDefault-depth0.public-DraftStyleDefault-listRTL { 152 | margin-right: 1.5em; 153 | } 154 | 155 | /* @noflip */ 156 | 157 | .public-DraftStyleDefault-depth1.public-DraftStyleDefault-listLTR { 158 | margin-left: 3em; 159 | } 160 | 161 | /* @noflip */ 162 | 163 | .public-DraftStyleDefault-depth1.public-DraftStyleDefault-listRTL { 164 | margin-right: 3em; 165 | } 166 | 167 | /* @noflip */ 168 | 169 | .public-DraftStyleDefault-depth2.public-DraftStyleDefault-listLTR { 170 | margin-left: 4.5em; 171 | } 172 | 173 | /* @noflip */ 174 | 175 | .public-DraftStyleDefault-depth2.public-DraftStyleDefault-listRTL { 176 | margin-right: 4.5em; 177 | } 178 | 179 | /* @noflip */ 180 | 181 | .public-DraftStyleDefault-depth3.public-DraftStyleDefault-listLTR { 182 | margin-left: 6em; 183 | } 184 | 185 | /* @noflip */ 186 | 187 | .public-DraftStyleDefault-depth3.public-DraftStyleDefault-listRTL { 188 | margin-right: 6em; 189 | } 190 | 191 | /* @noflip */ 192 | 193 | .public-DraftStyleDefault-depth4.public-DraftStyleDefault-listLTR { 194 | margin-left: 7.5em; 195 | } 196 | 197 | /* @noflip */ 198 | 199 | .public-DraftStyleDefault-depth4.public-DraftStyleDefault-listRTL { 200 | margin-right: 7.5em; 201 | } 202 | 203 | /** 204 | * Only use `square` list-style after the first two levels. 205 | */ 206 | 207 | .public-DraftStyleDefault-unorderedListItem { 208 | list-style-type: square; 209 | position: relative; 210 | } 211 | 212 | .public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth0 { 213 | list-style-type: disc; 214 | } 215 | 216 | .public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth1 { 217 | list-style-type: circle; 218 | } 219 | 220 | /** 221 | * Ordered list item counters are managed with CSS, since all list nesting is 222 | * purely visual. 223 | */ 224 | 225 | .public-DraftStyleDefault-orderedListItem { 226 | list-style-type: none; 227 | position: relative; 228 | } 229 | 230 | /* @noflip */ 231 | 232 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listLTR:before { 233 | left: -36px; 234 | position: absolute; 235 | text-align: right; 236 | width: 30px; 237 | } 238 | 239 | /* @noflip */ 240 | 241 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listRTL:before { 242 | position: absolute; 243 | right: -36px; 244 | text-align: left; 245 | width: 30px; 246 | } 247 | 248 | /** 249 | * Counters are reset in JavaScript. If you need different counter styles, 250 | * override these rules. If you need more nesting, create your own rules to 251 | * do so. 252 | */ 253 | 254 | .public-DraftStyleDefault-orderedListItem:before { 255 | content: counter(ol0) ". "; 256 | counter-increment: ol0; 257 | } 258 | 259 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth1:before { 260 | content: counter(ol1) ". "; 261 | counter-increment: ol1; 262 | } 263 | 264 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth2:before { 265 | content: counter(ol2) ". "; 266 | counter-increment: ol2; 267 | } 268 | 269 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth3:before { 270 | content: counter(ol3) ". "; 271 | counter-increment: ol3; 272 | } 273 | 274 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth4:before { 275 | content: counter(ol4) ". "; 276 | counter-increment: ol4; 277 | } 278 | 279 | .public-DraftStyleDefault-depth0.public-DraftStyleDefault-reset { 280 | counter-reset: ol0; 281 | } 282 | 283 | .public-DraftStyleDefault-depth1.public-DraftStyleDefault-reset { 284 | counter-reset: ol1; 285 | } 286 | 287 | .public-DraftStyleDefault-depth2.public-DraftStyleDefault-reset { 288 | counter-reset: ol2; 289 | } 290 | 291 | .public-DraftStyleDefault-depth3.public-DraftStyleDefault-reset { 292 | counter-reset: ol3; 293 | } 294 | 295 | .public-DraftStyleDefault-depth4.public-DraftStyleDefault-reset { 296 | counter-reset: ol4; 297 | } -------------------------------------------------------------------------------- /demo/publicTemplate/css/base.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | height: 100%; 4 | } 5 | 6 | *, *:before, *:after { 7 | box-sizing: inherit; 8 | } 9 | 10 | body { 11 | font-size: 15px; 12 | line-height: 1.4; 13 | font-family: 'Open Sans', sans-serif; 14 | color: #555; 15 | height: 100%; 16 | } 17 | 18 | figure { 19 | margin: 0; 20 | } 21 | 22 | #root { 23 | height: 100%; 24 | } 25 | -------------------------------------------------------------------------------- /demo/publicTemplate/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v4.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Change the default font family in all browsers (opinionated). 5 | * 2. Prevent adjustments of font size after orientation changes in IE and iOS. 6 | */ 7 | 8 | html { 9 | font-family: sans-serif; /* 1 */ 10 | -ms-text-size-adjust: 100%; /* 2 */ 11 | -webkit-text-size-adjust: 100%; /* 2 */ 12 | } 13 | 14 | /** 15 | * Remove the margin in all browsers (opinionated). 16 | */ 17 | 18 | body { 19 | margin: 0; 20 | } 21 | 22 | /* HTML5 display definitions 23 | ========================================================================== */ 24 | 25 | /** 26 | * Add the correct display in IE 9-. 27 | * 1. Add the correct display in Edge, IE, and Firefox. 28 | * 2. Add the correct display in IE. 29 | */ 30 | 31 | article, 32 | aside, 33 | details, /* 1 */ 34 | figcaption, 35 | figure, 36 | footer, 37 | header, 38 | main, /* 2 */ 39 | menu, 40 | nav, 41 | section, 42 | summary { /* 1 */ 43 | display: block; 44 | } 45 | 46 | /** 47 | * Add the correct display in IE 9-. 48 | */ 49 | 50 | audio, 51 | canvas, 52 | progress, 53 | video { 54 | display: inline-block; 55 | } 56 | 57 | /** 58 | * Add the correct display in iOS 4-7. 59 | */ 60 | 61 | audio:not([controls]) { 62 | display: none; 63 | height: 0; 64 | } 65 | 66 | /** 67 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 68 | */ 69 | 70 | progress { 71 | vertical-align: baseline; 72 | } 73 | 74 | /** 75 | * Add the correct display in IE 10-. 76 | * 1. Add the correct display in IE. 77 | */ 78 | 79 | template, /* 1 */ 80 | [hidden] { 81 | display: none; 82 | } 83 | 84 | /* Links 85 | ========================================================================== */ 86 | 87 | /** 88 | * Remove the gray background on active links in IE 10. 89 | */ 90 | 91 | a { 92 | background-color: transparent; 93 | } 94 | 95 | /** 96 | * Remove the outline on focused links when they are also active or hovered 97 | * in all browsers (opinionated). 98 | */ 99 | 100 | a:active, 101 | a:hover { 102 | outline-width: 0; 103 | } 104 | 105 | /* Text-level semantics 106 | ========================================================================== */ 107 | 108 | /** 109 | * 1. Remove the bottom border in Firefox 39-. 110 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 111 | */ 112 | 113 | abbr[title] { 114 | border-bottom: none; /* 1 */ 115 | text-decoration: underline; /* 2 */ 116 | text-decoration: underline dotted; /* 2 */ 117 | } 118 | 119 | /** 120 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: inherit; 126 | } 127 | 128 | /** 129 | * Add the correct font weight in Chrome, Edge, and Safari. 130 | */ 131 | 132 | b, 133 | strong { 134 | font-weight: bolder; 135 | } 136 | 137 | /** 138 | * Add the correct font style in Android 4.3-. 139 | */ 140 | 141 | dfn { 142 | font-style: italic; 143 | } 144 | 145 | /** 146 | * Correct the font size and margin on `h1` elements within `section` and 147 | * `article` contexts in Chrome, Firefox, and Safari. 148 | */ 149 | 150 | h1 { 151 | font-size: 2em; 152 | margin: 0.67em 0; 153 | } 154 | 155 | /** 156 | * Add the correct background and color in IE 9-. 157 | */ 158 | 159 | mark { 160 | background-color: #ff0; 161 | color: #000; 162 | } 163 | 164 | /** 165 | * Add the correct font size in all browsers. 166 | */ 167 | 168 | small { 169 | font-size: 80%; 170 | } 171 | 172 | /** 173 | * Prevent `sub` and `sup` elements from affecting the line height in 174 | * all browsers. 175 | */ 176 | 177 | sub, 178 | sup { 179 | font-size: 75%; 180 | line-height: 0; 181 | position: relative; 182 | vertical-align: baseline; 183 | } 184 | 185 | sub { 186 | bottom: -0.25em; 187 | } 188 | 189 | sup { 190 | top: -0.5em; 191 | } 192 | 193 | /* Embedded content 194 | ========================================================================== */ 195 | 196 | /** 197 | * Remove the border on images inside links in IE 10-. 198 | */ 199 | 200 | img { 201 | border-style: none; 202 | } 203 | 204 | /** 205 | * Hide the overflow in IE. 206 | */ 207 | 208 | svg:not(:root) { 209 | overflow: hidden; 210 | } 211 | 212 | /* Grouping content 213 | ========================================================================== */ 214 | 215 | /** 216 | * 1. Correct the inheritance and scaling of font size in all browsers. 217 | * 2. Correct the odd `em` font sizing in all browsers. 218 | */ 219 | 220 | code, 221 | kbd, 222 | pre, 223 | samp { 224 | font-family: monospace, monospace; /* 1 */ 225 | font-size: 1em; /* 2 */ 226 | } 227 | 228 | /** 229 | * Add the correct margin in IE 8. 230 | */ 231 | 232 | figure { 233 | margin: 1em 40px; 234 | } 235 | 236 | /** 237 | * 1. Add the correct box sizing in Firefox. 238 | * 2. Show the overflow in Edge and IE. 239 | */ 240 | 241 | hr { 242 | box-sizing: content-box; /* 1 */ 243 | height: 0; /* 1 */ 244 | overflow: visible; /* 2 */ 245 | } 246 | 247 | /* Forms 248 | ========================================================================== */ 249 | 250 | /** 251 | * Change font properties to `inherit` in all browsers (opinionated). 252 | */ 253 | 254 | button, 255 | input, 256 | select, 257 | textarea { 258 | font: inherit; 259 | } 260 | 261 | /** 262 | * Restore the font weight unset by the previous rule. 263 | */ 264 | 265 | optgroup { 266 | font-weight: bold; 267 | } 268 | 269 | /** 270 | * Show the overflow in IE. 271 | * 1. Show the overflow in Edge. 272 | * 2. Show the overflow in Edge, Firefox, and IE. 273 | */ 274 | 275 | button, 276 | input, /* 1 */ 277 | select { /* 2 */ 278 | overflow: visible; 279 | } 280 | 281 | /** 282 | * Remove the margin in Safari. 283 | * 1. Remove the margin in Firefox and Safari. 284 | */ 285 | 286 | button, 287 | input, 288 | select, 289 | textarea { /* 1 */ 290 | margin: 0; 291 | } 292 | 293 | /** 294 | * Remove the inheritence of text transform in Edge, Firefox, and IE. 295 | * 1. Remove the inheritence of text transform in Firefox. 296 | */ 297 | 298 | button, 299 | select { /* 1 */ 300 | text-transform: none; 301 | } 302 | 303 | /** 304 | * Change the cursor in all browsers (opinionated). 305 | */ 306 | 307 | button, 308 | [type="button"], 309 | [type="reset"], 310 | [type="submit"] { 311 | cursor: pointer; 312 | } 313 | 314 | /** 315 | * Restore the default cursor to disabled elements unset by the previous rule. 316 | */ 317 | 318 | [disabled] { 319 | cursor: default; 320 | } 321 | 322 | /** 323 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 324 | * controls in Android 4. 325 | * 2. Correct the inability to style clickable types in iOS. 326 | */ 327 | 328 | button, 329 | html [type="button"], /* 1 */ 330 | [type="reset"], 331 | [type="submit"] { 332 | -webkit-appearance: button; /* 2 */ 333 | } 334 | 335 | /** 336 | * Remove the inner border and padding in Firefox. 337 | */ 338 | 339 | button::-moz-focus-inner, 340 | input::-moz-focus-inner { 341 | border: 0; 342 | padding: 0; 343 | } 344 | 345 | /** 346 | * Restore the focus styles unset by the previous rule. 347 | */ 348 | 349 | button:-moz-focusring, 350 | input:-moz-focusring { 351 | outline: 1px dotted ButtonText; 352 | } 353 | 354 | /** 355 | * Change the border, margin, and padding in all browsers (opinionated). 356 | */ 357 | 358 | fieldset { 359 | border: 1px solid #c0c0c0; 360 | margin: 0 2px; 361 | padding: 0.35em 0.625em 0.75em; 362 | } 363 | 364 | /** 365 | * 1. Correct the text wrapping in Edge and IE. 366 | * 2. Correct the color inheritance from `fieldset` elements in IE. 367 | * 3. Remove the padding so developers are not caught out when they zero out 368 | * `fieldset` elements in all browsers. 369 | */ 370 | 371 | legend { 372 | box-sizing: border-box; /* 1 */ 373 | color: inherit; /* 2 */ 374 | display: table; /* 1 */ 375 | max-width: 100%; /* 1 */ 376 | padding: 0; /* 3 */ 377 | white-space: normal; /* 1 */ 378 | } 379 | 380 | /** 381 | * Remove the default vertical scrollbar in IE. 382 | */ 383 | 384 | textarea { 385 | overflow: auto; 386 | } 387 | 388 | /** 389 | * 1. Add the correct box sizing in IE 10-. 390 | * 2. Remove the padding in IE 10-. 391 | */ 392 | 393 | [type="checkbox"], 394 | [type="radio"] { 395 | box-sizing: border-box; /* 1 */ 396 | padding: 0; /* 2 */ 397 | } 398 | 399 | /** 400 | * Correct the cursor style of increment and decrement buttons in Chrome. 401 | */ 402 | 403 | [type="number"]::-webkit-inner-spin-button, 404 | [type="number"]::-webkit-outer-spin-button { 405 | height: auto; 406 | } 407 | 408 | /** 409 | * Correct the odd appearance of search inputs in Chrome and Safari. 410 | */ 411 | 412 | [type="search"] { 413 | -webkit-appearance: textfield; 414 | } 415 | 416 | /** 417 | * Remove the inner padding and cancel buttons in Chrome on OS X and 418 | * Safari on OS X. 419 | */ 420 | 421 | [type="search"]::-webkit-search-cancel-button, 422 | [type="search"]::-webkit-search-decoration { 423 | -webkit-appearance: none; 424 | } 425 | -------------------------------------------------------------------------------- /demo/publicTemplate/css/prism.css: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+abap+actionscript+ada+apacheconf+apl+applescript+asciidoc+aspnet+autoit+autohotkey+bash+basic+batch+c+brainfuck+bro+bison+csharp+cpp+coffeescript+ruby+css-extras+d+dart+diff+docker+eiffel+elixir+erlang+fsharp+fortran+gherkin+git+glsl+go+graphql+groovy+haml+handlebars+haskell+haxe+http+icon+inform7+ini+j+jade+java+jolie+json+julia+keyman+kotlin+latex+less+livescript+lolcode+lua+makefile+markdown+matlab+mel+mizar+monkey+nasm+nginx+nim+nix+nsis+objectivec+ocaml+oz+parigp+parser+pascal+perl+php+php-extras+powershell+processing+prolog+properties+protobuf+puppet+pure+python+q+qore+r+jsx+reason+rest+rip+roboconf+crystal+rust+sas+sass+scss+scala+scheme+smalltalk+smarty+sql+stylus+swift+tcl+textile+twig+typescript+verilog+vhdl+vim+wiki+xojo+yaml */ 2 | /** 3 | * prism.js default theme for JavaScript, CSS and HTML 4 | * Based on dabblet (http://dabblet.com) 5 | * @author Lea Verou 6 | */ 7 | 8 | code[class*="language-"], 9 | pre[class*="language-"] { 10 | color: black; 11 | background: none; 12 | text-shadow: 0 1px white; 13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 14 | text-align: left; 15 | white-space: pre; 16 | word-spacing: normal; 17 | word-break: normal; 18 | word-wrap: normal; 19 | line-height: 1.5; 20 | 21 | -moz-tab-size: 4; 22 | -o-tab-size: 4; 23 | tab-size: 4; 24 | 25 | -webkit-hyphens: none; 26 | -moz-hyphens: none; 27 | -ms-hyphens: none; 28 | hyphens: none; 29 | } 30 | 31 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 32 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 33 | text-shadow: none; 34 | background: #b3d4fc; 35 | } 36 | 37 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 38 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 39 | text-shadow: none; 40 | background: #b3d4fc; 41 | } 42 | 43 | @media print { 44 | code[class*="language-"], 45 | pre[class*="language-"] { 46 | text-shadow: none; 47 | } 48 | } 49 | 50 | /* Code blocks */ 51 | pre[class*="language-"] { 52 | padding: 1em; 53 | margin: .5em 0; 54 | overflow: auto; 55 | } 56 | 57 | :not(pre) > code[class*="language-"], 58 | pre[class*="language-"] { 59 | background: #f5f2f0; 60 | } 61 | 62 | /* Inline code */ 63 | :not(pre) > code[class*="language-"] { 64 | padding: .1em; 65 | border-radius: .3em; 66 | white-space: normal; 67 | } 68 | 69 | .token.comment, 70 | .token.prolog, 71 | .token.doctype, 72 | .token.cdata { 73 | color: slategray; 74 | } 75 | 76 | .token.punctuation { 77 | color: #999; 78 | } 79 | 80 | .namespace { 81 | opacity: .7; 82 | } 83 | 84 | .token.property, 85 | .token.tag, 86 | .token.boolean, 87 | .token.number, 88 | .token.constant, 89 | .token.symbol, 90 | .token.deleted { 91 | color: #905; 92 | } 93 | 94 | .token.selector, 95 | .token.attr-name, 96 | .token.string, 97 | .token.char, 98 | .token.builtin, 99 | .token.inserted { 100 | color: #690; 101 | } 102 | 103 | .token.operator, 104 | .token.entity, 105 | .token.url, 106 | .language-css .token.string, 107 | .style .token.string { 108 | color: #a67f59; 109 | background: hsla(0, 0%, 100%, .5); 110 | } 111 | 112 | .token.atrule, 113 | .token.attr-value, 114 | .token.keyword { 115 | color: #07a; 116 | } 117 | 118 | .token.function { 119 | color: #DD4A68; 120 | } 121 | 122 | .token.regex, 123 | .token.important, 124 | .token.variable { 125 | color: #e90; 126 | } 127 | 128 | .token.important, 129 | .token.bold { 130 | font-weight: bold; 131 | } 132 | .token.italic { 133 | font-style: italic; 134 | } 135 | 136 | .token.entity { 137 | cursor: help; 138 | } 139 | 140 | -------------------------------------------------------------------------------- /demo/publicTemplate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | draft-js-markdown-shortcuts-plugin Demo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: { 6 | app: path.join(__dirname, 'client/index.js'), 7 | }, 8 | output: { 9 | path: path.resolve(__dirname, 'public'), 10 | filename: '[name].js', 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | }, 19 | ], 20 | }, 21 | devServer: { 22 | contentBase: [path.join(__dirname, 'public'), path.join(__dirname, 'publicTemplate')], 23 | port: process.env.PORT || 3001, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /demo/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: { 6 | app: path.join(__dirname, 'client/index.js'), 7 | }, 8 | output: { 9 | path: path.resolve(__dirname, 'public'), 10 | filename: '[name].[hash].js', 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | }, 19 | ], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "draft-js-markdown-shortcuts-plugin", 3 | "version": "0.6.1", 4 | "description": "A DraftJS plugin for supporting Markdown syntax shortcuts", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "eslint": "node_modules/.bin/eslint .", 8 | "build": "npm run clean && npm run build:js", 9 | "build:demo": "rm -rf demo/public/* && NODE_ENV=production npx webpack --config demo/webpack.config.prod.js && rm -rf demo/public/{css,index.html} && cp -R demo/publicTemplate/* demo/public/ && sed \"s/app.js/$(basename $(ls demo/public/app.*.js))/g\" demo/publicTemplate/index.html > demo/public/index.html", 10 | "build:js": "BABEL_DISABLE_CACHE=1 BABEL_ENV=production NODE_ENV=production node_modules/.bin/babel --out-dir='lib' --ignore='**/__test__/*' src", 11 | "clean": "node_modules/.bin/rimraf lib; node_modules/.bin/rimraf demo/public", 12 | "prepare": "npm run build", 13 | "start": "npm run start:dev", 14 | "start:dev": "webpack-dev-server --config demo/webpack.config.dev.js", 15 | "test": "npm run test:coverage", 16 | "test:coverage": "node_modules/.bin/nyc --require @babel/register npm run test:mocha", 17 | "test:mocha": "BABEL_ENV=test mocha", 18 | "test:watch": "npm test | npm run watch", 19 | "watch": "npm-watch" 20 | }, 21 | "watch": { 22 | "test": { 23 | "patterns": ["src/**/*.js"] 24 | } 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/ngs/draft-js-markdown-shortcuts-plugin.git" 29 | }, 30 | "keywords": ["draftjs", "editor", "plugin", "markdown"], 31 | "author": "Atsushi Nagase", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/ngs/draft-js-markdown-shortcuts-plugin/issues" 35 | }, 36 | "homepage": "https://github.com/ngs/draft-js-markdown-shortcuts-plugin#readme", 37 | "devDependencies": { 38 | "@babel/cli": "^7.10.1", 39 | "@babel/core": "^7.10.2", 40 | "@babel/plugin-proposal-class-properties": "^7.10.1", 41 | "@babel/polyfill": "^7.10.1", 42 | "@babel/preset-env": "^7.10.2", 43 | "@babel/preset-es2015": "^7.0.0-beta.53", 44 | "@babel/preset-react": "^7.10.1", 45 | "@babel/preset-stage-0": "^7.8.3", 46 | "@babel/register": "^7.10.1", 47 | "autoprefixer": "^9.8.0", 48 | "babel-eslint": "^10.1.0", 49 | "babel-loader": "^8.1.0", 50 | "babel-plugin-rewire": "^1.2.0", 51 | "chai": "^4.2.0", 52 | "chai-enzyme": "^1.0.0-beta.1", 53 | "cheerio": "^0.22.0", 54 | "coveralls": "^3.1.0", 55 | "css-loader": "^3.5.3", 56 | "css-modules-require-hook": "^4.2.3", 57 | "dirty-chai": "^2.0.1", 58 | "draft-js-plugins-editor": "3.0.0", 59 | "draft-js-prism": "^1.0.6", 60 | "enzyme": "^3.11.0", 61 | "enzyme-adapter-react-16": "^1.15.2", 62 | "eslint": "^7.1.0", 63 | "eslint-config-airbnb": "^18.1.0", 64 | "eslint-config-prettier": "^6.11.0", 65 | "eslint-plugin-import": "^2.20.2", 66 | "eslint-plugin-jsx-a11y": "6.2.3", 67 | "eslint-plugin-mocha": "^7.0.1", 68 | "eslint-plugin-react": "^7.20.0", 69 | "extract-text-webpack-plugin": "^3.0.2", 70 | "file-loader": "^6.0.0", 71 | "flow-bin": "^0.125.1", 72 | "history": "^4.10.1", 73 | "jsdom": "^16.2.2", 74 | "mocha": "^7.2.0", 75 | "mocha-junit-reporter": "^1.23.3", 76 | "mocha-multi-reporters": "^1.1.7", 77 | "npm-watch": "^0.6.0", 78 | "nyc": "^15.1.0", 79 | "postcss-loader": "^3.0.0", 80 | "prettier": "2.0.5", 81 | "prismjs": "^1.20.0", 82 | "raf": "^3.4.1", 83 | "react": "^16.2.0", 84 | "react-addons-pure-render-mixin": "^15.6.2", 85 | "react-addons-test-utils": "^15.6.2", 86 | "react-dom": "^16.2.0", 87 | "react-github-fork-ribbon": "^0.6.0", 88 | "rimraf": "^3.0.2", 89 | "sinon": "^9.0.2", 90 | "sinon-chai": "^3.5.0", 91 | "style-loader": "^1.2.1", 92 | "styled-components": "^5.1.1", 93 | "url-loader": "^4.1.0", 94 | "webpack": "~4.43.0", 95 | "webpack-cli": "^3.3.11", 96 | "webpack-dev-server": "^3.11.0" 97 | }, 98 | "peerDependencies": { 99 | "draft-js-plugins-editor": "^3.0.0", 100 | "react": "^16.13.1", 101 | "react-dom": "^16.13.1" 102 | }, 103 | "contributors": ["Atsushi Nagase "], 104 | "dependencies": { 105 | "decorate-component-with-props": "^1.1.0", 106 | "draft-js": "~0.11.5", 107 | "draft-js-checkable-list-item": "^3.0.4", 108 | "draft-js-prism-plugin": "^0.1.3", 109 | "immutable": "~3.8.2" 110 | }, 111 | "collective": { 112 | "type": "opencollective", 113 | "url": "https://opencollective.com/draft-js-markdown-shortcuts-plugin" 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('autoprefixer')], 3 | }; 4 | -------------------------------------------------------------------------------- /screen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngs/draft-js-markdown-shortcuts-plugin/631f1863341ee57cd3cd93c54d06b364562254da/screen.gif -------------------------------------------------------------------------------- /src/__test__/plugin-test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: 0 */ 2 | import { expect } from 'chai'; 3 | import sinon from 'sinon'; 4 | import { JSDOM, VirtualConsole } from 'jsdom'; 5 | import Draft, { EditorState, SelectionState, ContentBlock } from 'draft-js'; 6 | import { CheckableListItem, CheckableListItemUtils } from 'draft-js-checkable-list-item'; 7 | 8 | import { Map, List } from 'immutable'; 9 | import createMarkdownShortcutsPlugin from '..'; 10 | 11 | const { window } = new JSDOM('', { virtualConsole: new VirtualConsole().sendTo(console) }); 12 | 13 | describe('draft-js-markdown-shortcuts-plugin', () => { 14 | afterEach(() => { 15 | /* eslint-disable no-underscore-dangle */ 16 | createMarkdownShortcutsPlugin.__ResetDependency__('adjustBlockDepth'); 17 | createMarkdownShortcutsPlugin.__ResetDependency__('handleBlockType'); 18 | createMarkdownShortcutsPlugin.__ResetDependency__('handleInlineStyle'); 19 | createMarkdownShortcutsPlugin.__ResetDependency__('handleNewCodeBlock'); 20 | createMarkdownShortcutsPlugin.__ResetDependency__('insertEmptyBlock'); 21 | createMarkdownShortcutsPlugin.__ResetDependency__('handleLink'); 22 | createMarkdownShortcutsPlugin.__ResetDependency__('handleImage'); 23 | createMarkdownShortcutsPlugin.__ResetDependency__('leaveList'); 24 | createMarkdownShortcutsPlugin.__ResetDependency__('changeCurrentBlockType'); 25 | createMarkdownShortcutsPlugin.__ResetDependency__('replaceText'); 26 | createMarkdownShortcutsPlugin.__ResetDependency__('checkReturnForState'); 27 | /* eslint-enable no-underscore-dangle */ 28 | }); 29 | 30 | const createEditorState = (rawContent, rawSelection) => { 31 | const contentState = Draft.convertFromRaw(rawContent); 32 | return EditorState.forceSelection(EditorState.createWithContent(contentState), rawSelection); 33 | }; 34 | 35 | let plugin; 36 | let store; 37 | let currentEditorState; 38 | let newEditorState; 39 | let currentRawContentState; 40 | let newRawContentState; 41 | let currentSelectionState; 42 | let subject; 43 | let event; 44 | 45 | let modifierSpy; 46 | 47 | [[], [{ insertEmptyBlockOnReturnWithModifierKey: false }]].forEach(args => { 48 | beforeEach(() => { 49 | modifierSpy = sinon.spy(() => newEditorState); 50 | 51 | event = new window.KeyboardEvent('keydown'); 52 | sinon.spy(event, 'preventDefault'); 53 | currentSelectionState = new SelectionState({ 54 | anchorKey: 'item1', 55 | anchorOffset: 0, 56 | focusKey: 'item1', 57 | focusOffset: 0, 58 | isBackward: false, 59 | hasFocus: true, 60 | }); 61 | 62 | newRawContentState = { 63 | entityMap: {}, 64 | blocks: [ 65 | { 66 | key: 'item1', 67 | text: 'altered!!', 68 | type: 'unstyled', 69 | depth: 0, 70 | inlineStyleRanges: [], 71 | entityRanges: [], 72 | data: {}, 73 | }, 74 | ], 75 | }; 76 | newEditorState = EditorState.createWithContent(Draft.convertFromRaw(newRawContentState)); 77 | 78 | store = { 79 | setEditorState: sinon.spy(), 80 | getEditorState: sinon.spy(() => { 81 | currentEditorState = createEditorState(currentRawContentState, currentSelectionState); 82 | return currentEditorState; 83 | }), 84 | }; 85 | subject = null; 86 | }); 87 | 88 | describe( 89 | args.length === 0 ? 'without config' : 'with `insertEmptyBlockOnReturnWithModifierKey: false` config', 90 | () => { 91 | beforeEach(() => { 92 | plugin = createMarkdownShortcutsPlugin(...args); 93 | plugin.initialize(store); 94 | }); 95 | 96 | it('is loaded', () => { 97 | expect(createMarkdownShortcutsPlugin).to.be.a('function'); 98 | }); 99 | it('initialize', () => { 100 | plugin.initialize(store); 101 | expect(plugin.store).to.deep.equal(store); 102 | }); 103 | describe('handleReturn', () => { 104 | const expectsHandled = () => { 105 | expect(subject()).to.equal('handled'); 106 | expect(modifierSpy).to.have.been.calledOnce; 107 | expect(store.setEditorState).to.have.been.calledWith(newEditorState); 108 | }; 109 | const expectsNotHandled = () => { 110 | expect(subject()).to.equal('not-handled'); 111 | expect(modifierSpy).not.to.have.been.calledOnce; 112 | expect(store.setEditorState).not.to.have.been.called; 113 | }; 114 | beforeEach(() => { 115 | subject = () => plugin.handleReturn(event, store.getEditorState(), store); 116 | }); 117 | it('does not handle', () => { 118 | currentRawContentState = { 119 | entityMap: {}, 120 | blocks: [ 121 | { 122 | key: 'item1', 123 | text: '', 124 | type: 'unstyled', 125 | depth: 0, 126 | inlineStyleRanges: [], 127 | entityRanges: [], 128 | data: {}, 129 | }, 130 | ], 131 | }; 132 | expectsNotHandled(); 133 | }); 134 | it('leaves from list', () => { 135 | createMarkdownShortcutsPlugin.__Rewire__('leaveList', modifierSpy); // eslint-disable-line no-underscore-dangle 136 | currentRawContentState = { 137 | entityMap: {}, 138 | blocks: [ 139 | { 140 | key: 'item1', 141 | text: '', 142 | type: 'ordered-list-item', 143 | depth: 0, 144 | inlineStyleRanges: [], 145 | entityRanges: [], 146 | data: {}, 147 | }, 148 | ], 149 | }; 150 | expectsHandled(); 151 | }); 152 | const testInsertNewBlock = (type, expects) => () => { 153 | createMarkdownShortcutsPlugin.__Rewire__('insertEmptyBlock', modifierSpy); // eslint-disable-line no-underscore-dangle 154 | currentRawContentState = { 155 | entityMap: {}, 156 | blocks: [ 157 | { 158 | key: 'item1', 159 | text: 'Hello', 160 | type, 161 | depth: 0, 162 | inlineStyleRanges: [], 163 | entityRanges: [], 164 | data: {}, 165 | }, 166 | ], 167 | }; 168 | currentSelectionState = new SelectionState({ 169 | anchorKey: 'item1', 170 | anchorOffset: 5, 171 | focusKey: 'item1', 172 | focusOffset: 5, 173 | isBackward: false, 174 | hasFocus: true, 175 | }); 176 | expects(); 177 | }; 178 | const expects = 179 | args[0] && args[0].insertEmptyBlockOnReturnWithModifierKey === false ? expectsNotHandled : expectsHandled; 180 | ['one', 'two', 'three', 'four', 'five', 'six'].forEach(level => { 181 | describe(`on header-${level}`, () => { 182 | it('inserts new empty block on end of header return', testInsertNewBlock(`header-${level}`, expects)); 183 | }); 184 | }); 185 | ['ctrlKey', 'shiftKey', 'metaKey', 'altKey'].forEach(key => { 186 | describe(`${key} is pressed`, () => { 187 | beforeEach(() => { 188 | const props = {}; 189 | props[key] = true; 190 | event = new window.KeyboardEvent('keydown', props); 191 | }); 192 | it('inserts new empty block', testInsertNewBlock('blockquote', expects)); 193 | }); 194 | }); 195 | it('handles new code block', () => { 196 | createMarkdownShortcutsPlugin.__Rewire__('handleNewCodeBlock', modifierSpy); // eslint-disable-line no-underscore-dangle 197 | currentRawContentState = { 198 | entityMap: {}, 199 | blocks: [ 200 | { 201 | key: 'item1', 202 | text: '```', 203 | type: 'unstyled', 204 | depth: 0, 205 | inlineStyleRanges: [], 206 | entityRanges: [], 207 | data: {}, 208 | }, 209 | ], 210 | }; 211 | expect(subject()).to.equal('handled'); 212 | expect(modifierSpy).to.have.been.calledOnce; 213 | expect(store.setEditorState).to.have.been.calledWith(newEditorState); 214 | }); 215 | it('handle code block closing', () => { 216 | createMarkdownShortcutsPlugin.__Rewire__('changeCurrentBlockType', modifierSpy); // eslint-disable-line no-underscore-dangle 217 | currentRawContentState = { 218 | entityMap: {}, 219 | blocks: [ 220 | { 221 | key: 'item1', 222 | text: 'foo\n```', 223 | type: 'code-block', 224 | depth: 0, 225 | inlineStyleRanges: [], 226 | entityRanges: [], 227 | data: {}, 228 | }, 229 | ], 230 | }; 231 | expect(subject()).to.equal('handled'); 232 | expect(modifierSpy).to.have.been.calledOnce; 233 | }); 234 | it('insert new line char from code-block', () => { 235 | createMarkdownShortcutsPlugin.__Rewire__('insertText', modifierSpy); // eslint-disable-line no-underscore-dangle 236 | currentRawContentState = { 237 | entityMap: {}, 238 | blocks: [ 239 | { 240 | key: 'item1', 241 | text: 'const foo = a => a', 242 | type: 'code-block', 243 | depth: 0, 244 | inlineStyleRanges: [], 245 | entityRanges: [], 246 | data: {}, 247 | }, 248 | ], 249 | }; 250 | expect(subject()).to.equal('handled'); 251 | expect(modifierSpy).to.have.been.calledOnce; 252 | expect(store.setEditorState).to.have.been.calledWith(newEditorState); 253 | }); 254 | }); 255 | describe('blockStyleFn', () => { 256 | let type; 257 | beforeEach(() => { 258 | type = null; 259 | const getType = () => type; 260 | subject = () => plugin.blockStyleFn({ getType }); 261 | }); 262 | it('returns checkable-list-item', () => { 263 | type = 'checkable-list-item'; 264 | expect(subject()).to.equal('checkable-list-item'); 265 | }); 266 | it('returns null', () => { 267 | type = 'ordered-list-item'; 268 | expect(subject()).to.be.null; 269 | }); 270 | }); 271 | describe('blockRendererFn', () => { 272 | let type; 273 | let data; 274 | let block; 275 | let spyOnChangeChecked; 276 | beforeEach(() => { 277 | type = null; 278 | data = {}; 279 | spyOnChangeChecked = sinon.spy(CheckableListItemUtils, 'toggleChecked'); 280 | subject = () => { 281 | block = new ContentBlock({ 282 | type, 283 | data: Map(data), 284 | key: 'item1', 285 | characterList: List(), 286 | }); 287 | return plugin.blockRendererFn(block, store); 288 | }; 289 | }); 290 | afterEach(() => { 291 | CheckableListItemUtils.toggleChecked.restore(); 292 | }); 293 | it('returns renderer', () => { 294 | type = 'checkable-list-item'; 295 | data = { checked: true }; 296 | const renderer = subject(); 297 | expect(renderer).to.be.an('object'); 298 | expect(renderer.component).to.equal(CheckableListItem); 299 | expect(renderer.props.onChangeChecked).to.be.a('function'); 300 | expect(renderer.props.checked).to.be.true; 301 | renderer.props.onChangeChecked(); 302 | expect(spyOnChangeChecked).to.have.been.calledWith(currentEditorState, block); 303 | }); 304 | it('returns null', () => { 305 | type = 'ordered-list-item'; 306 | expect(subject()).to.be.null; 307 | }); 308 | }); 309 | describe('onTab', () => { 310 | beforeEach(() => { 311 | subject = () => { 312 | createMarkdownShortcutsPlugin.__Rewire__('adjustBlockDepth', modifierSpy); // eslint-disable-line no-underscore-dangle 313 | return plugin.onTab(event, store); 314 | }; 315 | }); 316 | describe('no changes', () => { 317 | it('returns handled', () => { 318 | expect(subject()).to.equal('handled'); 319 | }); 320 | it('returns not-handled', () => { 321 | modifierSpy = sinon.spy(() => currentEditorState); 322 | expect(subject()).to.equal('not-handled'); 323 | }); 324 | }); 325 | }); 326 | describe('handleBeforeInput', () => { 327 | let character; 328 | beforeEach(() => { 329 | character = ' '; 330 | subject = () => plugin.handleBeforeInput(character, store.getEditorState(), new Date().getTime(), store); 331 | }); 332 | ['handleBlockType', 'handleImage', 'handleLink', 'handleInlineStyle'].forEach(modifier => { 333 | describe(modifier, () => { 334 | beforeEach(() => { 335 | createMarkdownShortcutsPlugin.__Rewire__(modifier, modifierSpy); // eslint-disable-line no-underscore-dangle 336 | }); 337 | it('returns handled', () => { 338 | expect(subject()).to.equal('handled'); 339 | expect(modifierSpy).to.have.been.calledWith(currentEditorState, ' '); 340 | }); 341 | }); 342 | }); 343 | describe('character is not a space', () => { 344 | beforeEach(() => { 345 | character = 'x'; 346 | }); 347 | it('returns not-handled', () => { 348 | expect(subject()).to.equal('not-handled'); 349 | }); 350 | }); 351 | describe('no matching modifiers', () => { 352 | it('returns not-handled', () => { 353 | expect(subject()).to.equal('not-handled'); 354 | }); 355 | }); 356 | }); 357 | describe('handlePastedText', () => { 358 | let pastedText; 359 | let html; 360 | beforeEach(() => { 361 | pastedText = `_hello world_ 362 | Hello`; 363 | html = undefined; 364 | subject = () => plugin.handlePastedText(pastedText, html, store.getEditorState(), store); 365 | }); 366 | ['replaceText', 'handleBlockType', 'handleImage', 'handleLink', 'handleInlineStyle'].forEach(modifier => { 367 | describe(modifier, () => { 368 | beforeEach(() => { 369 | createMarkdownShortcutsPlugin.__Rewire__(modifier, modifierSpy); // eslint-disable-line no-underscore-dangle 370 | }); 371 | it('returns handled', () => { 372 | expect(subject()).to.equal('handled'); 373 | expect(modifierSpy).to.have.been.called; 374 | }); 375 | }); 376 | }); 377 | describe('nothing in clipboard', () => { 378 | beforeEach(() => { 379 | pastedText = ''; 380 | }); 381 | it('returns not-handled', () => { 382 | expect(subject()).to.equal('not-handled'); 383 | }); 384 | }); 385 | describe('pasted just text', () => { 386 | beforeEach(() => { 387 | pastedText = 'hello'; 388 | createMarkdownShortcutsPlugin.__Rewire__('replaceText', modifierSpy); // eslint-disable-line no-underscore-dangle 389 | }); 390 | it('returns handled', () => { 391 | expect(subject()).to.equal('handled'); 392 | expect(modifierSpy).to.have.been.calledWith(currentEditorState, 'hello'); 393 | }); 394 | }); 395 | describe('non-string empty value in clipboard', () => { 396 | beforeEach(() => { 397 | pastedText = null; 398 | }); 399 | it('returns not-handled', () => { 400 | expect(subject()).to.equal('not-handled'); 401 | }); 402 | }); 403 | describe('non-string value in clipboard', () => { 404 | beforeEach(() => { 405 | pastedText = {}; 406 | }); 407 | it('returns not-handled', () => { 408 | expect(subject()).to.equal('not-handled'); 409 | }); 410 | }); 411 | describe('pasted just text with new line code', () => { 412 | beforeEach(() => { 413 | pastedText = 'hello\nworld'; 414 | const rawContentState = { 415 | entityMap: {}, 416 | blocks: [ 417 | { 418 | key: 'item1', 419 | text: '', 420 | type: 'unstyled', 421 | depth: 0, 422 | inlineStyleRanges: [], 423 | entityRanges: [], 424 | data: {}, 425 | }, 426 | ], 427 | }; 428 | const otherRawContentState = { 429 | entityMap: {}, 430 | blocks: [ 431 | { 432 | key: 'item2', 433 | text: 'H1', 434 | type: 'header-one', 435 | depth: 0, 436 | inlineStyleRanges: [], 437 | entityRanges: [], 438 | data: {}, 439 | }, 440 | ], 441 | }; 442 | /* eslint-disable no-underscore-dangle */ 443 | createMarkdownShortcutsPlugin.__Rewire__('replaceText', () => 444 | createEditorState(rawContentState, currentSelectionState), 445 | ); 446 | createMarkdownShortcutsPlugin.__Rewire__('checkReturnForState', () => 447 | createEditorState(otherRawContentState, currentSelectionState), 448 | ); 449 | /* eslint-enable no-underscore-dangle */ 450 | }); 451 | it('return handled', () => { 452 | expect(subject()).to.equal('handled'); 453 | }); 454 | }); 455 | describe('passed `html` argument', () => { 456 | beforeEach(() => { 457 | pastedText = '# hello'; 458 | html = '

hello

'; 459 | }); 460 | it('returns not-handled', () => { 461 | expect(subject()).to.equal('not-handled'); 462 | }); 463 | }); 464 | }); 465 | }, 466 | ); 467 | }); 468 | }); 469 | -------------------------------------------------------------------------------- /src/__test__/utils-test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import Draft, { EditorState, SelectionState } from 'draft-js'; 3 | import insertEmptyBlock from '../modifiers/insertEmptyBlock'; 4 | import { addText, replaceText } from '../utils'; 5 | 6 | describe('utils test', () => { 7 | it('is loaded', () => { 8 | expect(addText).to.be.a('function'); 9 | expect(replaceText).to.be.a('function'); 10 | }); 11 | 12 | const newRawContentState = { 13 | entityMap: {}, 14 | blocks: [ 15 | { 16 | key: 'item1', 17 | text: 'altered!!', 18 | type: 'unstyled', 19 | depth: 0, 20 | inlineStyleRanges: [], 21 | entityRanges: [], 22 | data: {}, 23 | }, 24 | ], 25 | }; 26 | 27 | it('should addText', () => { 28 | let newEditorState = EditorState.createWithContent(Draft.convertFromRaw(newRawContentState)); 29 | const randomText = Date.now().toString(32); 30 | newEditorState = insertEmptyBlock(newEditorState); 31 | newEditorState = addText(newEditorState, randomText); 32 | const currentContent = newEditorState.getCurrentContent(); 33 | expect(currentContent.hasText()).to.equal(true); 34 | const lastBlock = currentContent.getLastBlock(); 35 | expect(lastBlock.getText()).to.equal(randomText); 36 | }); 37 | 38 | it('should replaceText', () => { 39 | let newEditorState = EditorState.createWithContent(Draft.convertFromRaw(newRawContentState)); 40 | const randomText = Date.now().toString(32); 41 | let currentContent = newEditorState.getCurrentContent(); 42 | let lastBlock = currentContent.getLastBlock(); 43 | const newSelection = new SelectionState({ 44 | anchorKey: lastBlock.getKey(), 45 | anchorOffset: 0, 46 | focusKey: lastBlock.getKey(), 47 | focusOffset: lastBlock.getText().length, 48 | }); 49 | newEditorState = EditorState.forceSelection(newEditorState, newSelection); 50 | 51 | newEditorState = replaceText(newEditorState, randomText); 52 | currentContent = newEditorState.getCurrentContent(); 53 | expect(currentContent.hasText()).to.equal(true); 54 | lastBlock = currentContent.getLastBlock(); 55 | expect(lastBlock.getText()).to.equal(randomText); 56 | const firstBlock = currentContent.getFirstBlock(); 57 | expect(firstBlock.getText()).to.equal(randomText); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/components/Image/__test__/Image-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ContentState } from 'draft-js'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import { shallow, configure } from 'enzyme'; 5 | import chai, { expect } from 'chai'; 6 | import chaiEnzyme from 'chai-enzyme'; 7 | 8 | import Image from ".."; 9 | 10 | configure({ adapter: new Adapter() }); 11 | chai.use(chaiEnzyme()); 12 | 13 | describe('', () => { 14 | it('renders anchor tag', () => { 15 | const contentState = ContentState.createFromText('').createEntity('IMG', 'MUTABLE', { 16 | alt: 'alt', 17 | src: 'http://cultofthepartyparrot.com/parrots/aussieparrot.gif', 18 | title: 'parrot', 19 | }); 20 | const entityKey = contentState.getLastCreatedEntityKey(); 21 | expect( 22 | shallow( 23 | 24 |   25 | , 26 | ).html(), 27 | ).to.equal( 28 | ' alt', 29 | ); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/components/Image/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Image = ({ entityKey, children, contentState }) => { 4 | const { src, alt, title } = contentState.getEntity(entityKey).getData(); 5 | return ( 6 | 7 | {children} 8 | {alt} 9 | 10 | ); 11 | }; 12 | 13 | export default Image; 14 | -------------------------------------------------------------------------------- /src/components/Link/__test__/Link-test.js: -------------------------------------------------------------------------------- 1 | /* eslint jsx-a11y/anchor-is-valid: 0 */ 2 | import React from 'react'; 3 | import { ContentState } from 'draft-js'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import { shallow, configure } from 'enzyme'; 6 | import chai, { expect } from 'chai'; 7 | import chaiEnzyme from 'chai-enzyme'; 8 | 9 | import Link from '..'; 10 | 11 | configure({ adapter: new Adapter() }); 12 | chai.use(chaiEnzyme()); 13 | 14 | describe('', () => { 15 | it('renders anchor tag', () => { 16 | const contentState = ContentState.createFromText('').createEntity('LINK', 'MUTABLE', { 17 | href: 'http://cultofthepartyparrot.com/', 18 | title: 'parrot', 19 | }); 20 | const entityKey = contentState.getLastCreatedEntityKey(); 21 | expect( 22 | shallow( 23 | 24 | Hello 25 | , 26 | ).html(), 27 | ).to.equal('Hello'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/components/Link/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Link = props => { 4 | const { contentState, children, entityKey } = props; 5 | const { href, title } = contentState.getEntity(entityKey).getData(); 6 | return ( 7 | 8 | {children} 9 | 10 | ); 11 | }; 12 | 13 | export default Link; 14 | -------------------------------------------------------------------------------- /src/decorators/image/__test__/imageStrategy-test.js: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import sinonChai from 'sinon-chai'; 4 | import Draft from 'draft-js'; 5 | import createImageStrategy from '../imageStrategy'; 6 | 7 | chai.use(sinonChai); 8 | 9 | describe('imageStrategy', () => { 10 | const contentState = Draft.convertFromRaw({ 11 | entityMap: { 12 | 0: { 13 | type: 'IMG', 14 | mutability: 'IMMUTABLE', 15 | data: { 16 | alt: 'alt', 17 | src: 'http://cultofthepartyparrot.com/parrots/aussieparrot.gif', 18 | title: 'parrot', 19 | }, 20 | }, 21 | }, 22 | blocks: [ 23 | { 24 | key: 'dtehj', 25 | text: ' ', 26 | type: 'unstyled', 27 | depth: 0, 28 | inlineStyleRanges: [], 29 | entityRanges: [ 30 | { 31 | offset: 0, 32 | length: 1, 33 | key: 0, 34 | }, 35 | ], 36 | data: {}, 37 | }, 38 | ], 39 | }); 40 | it('callbacks range', () => { 41 | const block = contentState.getBlockForKey('dtehj'); 42 | const strategy = createImageStrategy(); 43 | const cb = sinon.spy(); 44 | expect(block).to.be.an('object'); 45 | strategy(block, cb, contentState); 46 | expect(cb).to.have.been.calledWith(0, 1); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/decorators/image/imageStrategy.js: -------------------------------------------------------------------------------- 1 | const createImageStrategy = () => { 2 | const findImageEntities = (contentBlock, callback, contentState) => { 3 | contentBlock.findEntityRanges(character => { 4 | const entityKey = character.getEntity(); 5 | return entityKey !== null && contentState.getEntity(entityKey).getType() === 'IMG'; 6 | }, callback); 7 | }; 8 | return findImageEntities; 9 | }; 10 | 11 | export default createImageStrategy; 12 | -------------------------------------------------------------------------------- /src/decorators/image/index.js: -------------------------------------------------------------------------------- 1 | import createImageStrategy from './imageStrategy'; 2 | import Image from '../../components/Image'; 3 | 4 | const createImageDecorator = (config, store) => ({ 5 | strategy: createImageStrategy(config, store), 6 | component: Image, 7 | }); 8 | 9 | export default createImageDecorator; 10 | -------------------------------------------------------------------------------- /src/decorators/link/__test__/linkStrategy-test.js: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import sinonChai from 'sinon-chai'; 4 | import Draft from 'draft-js'; 5 | import createLinkStrategy from '../linkStrategy'; 6 | 7 | chai.use(sinonChai); 8 | 9 | describe('linkStrategy', () => { 10 | const contentState = Draft.convertFromRaw({ 11 | entityMap: { 12 | 0: { 13 | type: 'LINK', 14 | mutability: 'MUTABLE', 15 | data: { 16 | href: 'http://cultofthepartyparrot.com/', 17 | title: 'parrot', 18 | }, 19 | }, 20 | }, 21 | blocks: [ 22 | { 23 | key: 'dtehj', 24 | text: 'parrot click me', 25 | type: 'unstyled', 26 | depth: 0, 27 | inlineStyleRanges: [], 28 | entityRanges: [ 29 | { 30 | offset: 7, 31 | length: 5, 32 | key: 0, 33 | }, 34 | ], 35 | data: {}, 36 | }, 37 | ], 38 | }); 39 | it('callbacks range', () => { 40 | const block = contentState.getBlockForKey('dtehj'); 41 | const strategy = createLinkStrategy(); 42 | const cb = sinon.spy(); 43 | expect(block).to.be.an('object'); 44 | strategy(block, cb, contentState); 45 | expect(cb).to.have.been.calledWith(7, 12); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/decorators/link/index.js: -------------------------------------------------------------------------------- 1 | import createLinkStrategy from './linkStrategy'; 2 | import Link from '../../components/Link'; 3 | 4 | const createLinkDecorator = (config, store) => ({ 5 | strategy: createLinkStrategy(config, store), 6 | component: Link, 7 | }); 8 | 9 | export default createLinkDecorator; 10 | -------------------------------------------------------------------------------- /src/decorators/link/linkStrategy.js: -------------------------------------------------------------------------------- 1 | const createLinkStrategy = () => { 2 | const findLinkEntities = (contentBlock, callback, contentState) => { 3 | contentBlock.findEntityRanges(character => { 4 | const entityKey = character.getEntity(); 5 | return entityKey !== null && contentState.getEntity(entityKey).getType() === 'LINK'; 6 | }, callback); 7 | }; 8 | return findLinkEntities; 9 | }; 10 | 11 | export default createLinkStrategy; 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | blockRenderMap as checkboxBlockRenderMap, 4 | CheckableListItem, 5 | CheckableListItemUtils, 6 | CHECKABLE_LIST_ITEM, 7 | } from 'draft-js-checkable-list-item'; 8 | 9 | import { Map } from 'immutable'; 10 | 11 | import adjustBlockDepth from './modifiers/adjustBlockDepth'; 12 | import handleBlockType from './modifiers/handleBlockType'; 13 | import handleInlineStyle from './modifiers/handleInlineStyle'; 14 | import handleNewCodeBlock from './modifiers/handleNewCodeBlock'; 15 | import insertEmptyBlock from './modifiers/insertEmptyBlock'; 16 | import handleLink from './modifiers/handleLink'; 17 | import handleImage from './modifiers/handleImage'; 18 | import leaveList from './modifiers/leaveList'; 19 | import insertText from './modifiers/insertText'; 20 | import changeCurrentBlockType from './modifiers/changeCurrentBlockType'; 21 | import createLinkDecorator from './decorators/link'; 22 | import createImageDecorator from './decorators/image'; 23 | import { replaceText } from './utils'; 24 | 25 | function checkCharacterForState(editorState, character) { 26 | let newEditorState = handleBlockType(editorState, character); 27 | const contentState = editorState.getCurrentContent(); 28 | const selection = editorState.getSelection(); 29 | const key = selection.getStartKey(); 30 | const currentBlock = contentState.getBlockForKey(key); 31 | const type = currentBlock.getType(); 32 | if (editorState === newEditorState) { 33 | newEditorState = handleImage(editorState, character); 34 | } 35 | if (editorState === newEditorState) { 36 | newEditorState = handleLink(editorState, character); 37 | } 38 | if (editorState === newEditorState && type !== 'code-block') { 39 | newEditorState = handleInlineStyle(editorState, character); 40 | } 41 | return newEditorState; 42 | } 43 | 44 | function checkReturnForState(editorState, ev, { insertEmptyBlockOnReturnWithModifierKey }) { 45 | let newEditorState = editorState; 46 | const contentState = editorState.getCurrentContent(); 47 | const selection = editorState.getSelection(); 48 | const key = selection.getStartKey(); 49 | const currentBlock = contentState.getBlockForKey(key); 50 | const type = currentBlock.getType(); 51 | const text = currentBlock.getText(); 52 | if (/-list-item$/.test(type) && text === '') { 53 | newEditorState = leaveList(editorState); 54 | } 55 | if ( 56 | newEditorState === editorState && 57 | insertEmptyBlockOnReturnWithModifierKey && 58 | (ev.ctrlKey || 59 | ev.shiftKey || 60 | ev.metaKey || 61 | ev.altKey || 62 | (/^header-/.test(type) && selection.isCollapsed() && selection.getEndOffset() === text.length)) 63 | ) { 64 | newEditorState = insertEmptyBlock(editorState); 65 | } 66 | if (newEditorState === editorState && type !== 'code-block' && /^```([\w-]+)?$/.test(text)) { 67 | newEditorState = handleNewCodeBlock(editorState); 68 | } 69 | if (newEditorState === editorState && type === 'code-block') { 70 | if (/```\s*$/.test(text)) { 71 | newEditorState = changeCurrentBlockType(newEditorState, type, text.replace(/\n```\s*$/, '')); 72 | newEditorState = insertEmptyBlock(newEditorState); 73 | } else { 74 | newEditorState = insertText(editorState, '\n'); 75 | } 76 | } 77 | if (editorState === newEditorState) { 78 | newEditorState = handleInlineStyle(editorState, '\n'); 79 | } 80 | return newEditorState; 81 | } 82 | 83 | const createMarkdownShortcutsPlugin = (config = { insertEmptyBlockOnReturnWithModifierKey: true }) => { 84 | const store = {}; 85 | return { 86 | store, 87 | blockRenderMap: Map({ 88 | 'code-block': { 89 | element: 'code', 90 | wrapper:
,
 91 |       },
 92 |     }).merge(checkboxBlockRenderMap),
 93 |     decorators: [createLinkDecorator(config, store), createImageDecorator(config, store)],
 94 |     initialize({ setEditorState, getEditorState }) {
 95 |       store.setEditorState = setEditorState;
 96 |       store.getEditorState = getEditorState;
 97 |     },
 98 |     blockStyleFn(block) {
 99 |       switch (block.getType()) {
100 |         case CHECKABLE_LIST_ITEM:
101 |           return CHECKABLE_LIST_ITEM;
102 |         default:
103 |           break;
104 |       }
105 |       return null;
106 |     },
107 | 
108 |     blockRendererFn(block) {
109 |       switch (block.getType()) {
110 |         case CHECKABLE_LIST_ITEM: {
111 |           return {
112 |             component: CheckableListItem,
113 |             props: {
114 |               onChangeChecked: () =>
115 |                 store.setEditorState(CheckableListItemUtils.toggleChecked(store.getEditorState(), block)),
116 |               checked: !!block.getData().get('checked'),
117 |             },
118 |           };
119 |         }
120 |         default:
121 |           return null;
122 |       }
123 |     },
124 |     onTab(ev) {
125 |       const editorState = store.getEditorState();
126 |       const newEditorState = adjustBlockDepth(editorState, ev);
127 |       if (newEditorState !== editorState) {
128 |         store.setEditorState(newEditorState);
129 |         return 'handled';
130 |       }
131 |       return 'not-handled';
132 |     },
133 |     handleReturn(ev, editorState) {
134 |       const newEditorState = checkReturnForState(editorState, ev, config);
135 |       if (editorState !== newEditorState) {
136 |         store.setEditorState(newEditorState);
137 |         return 'handled';
138 |       }
139 |       return 'not-handled';
140 |     },
141 |     handleBeforeInput(character, editorState) {
142 |       if (character.match(/[A-z0-9_*~`]/)) {
143 |         return 'not-handled';
144 |       }
145 |       const newEditorState = checkCharacterForState(editorState, character);
146 |       if (editorState !== newEditorState) {
147 |         store.setEditorState(newEditorState);
148 |         return 'handled';
149 |       }
150 |       return 'not-handled';
151 |     },
152 |     handlePastedText(text, html, editorState) {
153 |       if (html) {
154 |         return 'not-handled';
155 |       }
156 | 
157 |       if (!text) {
158 |         return 'not-handled';
159 |       }
160 | 
161 |       let newEditorState = editorState;
162 |       let buffer = [];
163 |       for (let i = 0; i < text.length; i += 1) {
164 |         // eslint-disable-line no-plusplus
165 |         if (text[i].match(/[^A-z0-9_*~`]/)) {
166 |           newEditorState = replaceText(newEditorState, buffer.join('') + text[i]);
167 |           newEditorState = checkCharacterForState(newEditorState, text[i]);
168 |           buffer = [];
169 |         } else if (text[i].charCodeAt(0) === 10) {
170 |           newEditorState = replaceText(newEditorState, buffer.join(''));
171 |           const tmpEditorState = checkReturnForState(newEditorState, {}, config);
172 |           if (newEditorState === tmpEditorState) {
173 |             newEditorState = insertEmptyBlock(tmpEditorState);
174 |           } else {
175 |             newEditorState = tmpEditorState;
176 |           }
177 |           buffer = [];
178 |         } else if (i === text.length - 1) {
179 |           newEditorState = replaceText(newEditorState, buffer.join('') + text[i]);
180 |           buffer = [];
181 |         } else {
182 |           buffer.push(text[i]);
183 |         }
184 |       }
185 | 
186 |       if (editorState !== newEditorState) {
187 |         store.setEditorState(newEditorState);
188 |         return 'handled';
189 |       }
190 |       return 'not-handled';
191 |     },
192 |   };
193 | };
194 | 
195 | export default createMarkdownShortcutsPlugin;
196 | 


--------------------------------------------------------------------------------
/src/modifiers/__test__/adjustBlockDepth-test.js:
--------------------------------------------------------------------------------
 1 | import chai, { expect } from 'chai';
 2 | import sinon from 'sinon';
 3 | import sinonChai from 'sinon-chai';
 4 | import { JSDOM, VirtualConsole } from 'jsdom';
 5 | import Draft, { EditorState, SelectionState } from 'draft-js';
 6 | import adjustBlockDepth from '../adjustBlockDepth';
 7 | 
 8 | chai.use(sinonChai);
 9 | const { window } = new JSDOM('', { virtualConsole: new VirtualConsole().sendTo(console) });
10 | 
11 | describe('adjustBlockDepth', () => {
12 |   const createEvent = () => {
13 |     const e = new window.KeyboardEvent('keydown', { shiftKey: false });
14 |     sinon.spy(e, 'preventDefault');
15 |     return e;
16 |   };
17 |   const rawContentState = (type, ...depthes) => ({
18 |     entityMap: {},
19 |     blocks: depthes.map((depth, i) => ({
20 |       key: `item${i}`,
21 |       text: `test ${i}`,
22 |       type,
23 |       depth,
24 |       inlineStyleRanges: [],
25 |       entityRanges: [],
26 |       data: {},
27 |     })),
28 |   });
29 |   const selectionState = new SelectionState({
30 |     anchorKey: 'item1',
31 |     anchorOffset: 0,
32 |     focusKey: 'item1',
33 |     focusOffset: 0,
34 |     isBackward: false,
35 |     hasFocus: true,
36 |   });
37 |   const createEditorState = (...args) => {
38 |     const contentState = Draft.convertFromRaw(rawContentState(...args));
39 |     return EditorState.forceSelection(EditorState.createWithContent(contentState), selectionState);
40 |   };
41 |   describe('non list item', () => {
42 |     it('does not add depth', () => {
43 |       const event = createEvent();
44 |       const editorState = createEditorState('unstyled', 0, 0);
45 |       const newEditorState = adjustBlockDepth(editorState, event);
46 |       expect(newEditorState).to.equal(editorState);
47 |       expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(rawContentState('unstyled', 0, 0));
48 |     });
49 |   });
50 |   ['unordered-list-item', 'ordered-list-item', 'checkable-list-item'].forEach(type => {
51 |     describe(type, () => {
52 |       it('adds depth', () => {
53 |         const event = createEvent();
54 |         const editorState = createEditorState(type, 0, 0);
55 |         const newEditorState = adjustBlockDepth(editorState, event);
56 |         expect(newEditorState).not.to.equal(editorState);
57 |         expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(rawContentState(type, 0, 1));
58 |       });
59 |     });
60 |   });
61 | });
62 | 


--------------------------------------------------------------------------------
/src/modifiers/__test__/changeCurrentBlockType-test.js:
--------------------------------------------------------------------------------
 1 | import { expect } from 'chai';
 2 | import Draft, { EditorState, SelectionState } from 'draft-js';
 3 | import changeCurrentBlockType from '../changeCurrentBlockType';
 4 | 
 5 | describe('changeCurrentBlockType', () => {
 6 |   const rawContentState = (text, type, data = {}) => ({
 7 |     entityMap: {},
 8 |     blocks: [
 9 |       {
10 |         key: 'item1',
11 |         text,
12 |         type,
13 |         depth: 0,
14 |         inlineStyleRanges: [],
15 |         entityRanges: [],
16 |         data,
17 |       },
18 |     ],
19 |   });
20 |   const selectionState = new SelectionState({
21 |     anchorKey: 'item1',
22 |     anchorOffset: 0,
23 |     focusKey: 'item1',
24 |     focusOffset: 0,
25 |     isBackward: false,
26 |     hasFocus: true,
27 |   });
28 |   const createEditorState = (...args) => {
29 |     const contentState = Draft.convertFromRaw(rawContentState(...args));
30 |     return EditorState.forceSelection(EditorState.createWithContent(contentState), selectionState);
31 |   };
32 |   it('changes block type', () => {
33 |     const editorState = createEditorState('Yo', 'unstyled');
34 |     const newEditorState = changeCurrentBlockType(editorState, 'header-one', 'Hello world', { foo: 'bar' });
35 |     expect(newEditorState).not.to.equal(editorState);
36 |     expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(
37 |       rawContentState('Hello world', 'header-one', { foo: 'bar' }),
38 |     );
39 |   });
40 |   it('changes block type even if data is null', () => {
41 |     const editorState = createEditorState('Yo', 'unstyled');
42 |     const newEditorState = changeCurrentBlockType(editorState, 'header-one', 'Hello world', null);
43 |     expect(newEditorState).not.to.equal(editorState);
44 |     expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(
45 |       rawContentState('Hello world', 'header-one', {}),
46 |     );
47 |   });
48 | });
49 | 


--------------------------------------------------------------------------------
/src/modifiers/__test__/changeCurrentInlineStyle-test.js:
--------------------------------------------------------------------------------
 1 | import { expect } from 'chai';
 2 | import Draft, { EditorState, SelectionState } from 'draft-js';
 3 | import changeCurrentInlineStyle from '../changeCurrentInlineStyle';
 4 | 
 5 | describe('changeCurrentInlineStyle', () => {
 6 |   const rawContentState = (text, inlineStyleRanges) => ({
 7 |     entityMap: {},
 8 |     blocks: [
 9 |       {
10 |         key: 'item1',
11 |         text,
12 |         type: 'unstyled',
13 |         depth: 0,
14 |         inlineStyleRanges,
15 |         entityRanges: [],
16 |         data: {},
17 |       },
18 |     ],
19 |   });
20 |   const selectionState = new SelectionState({
21 |     anchorKey: 'item1',
22 |     anchorOffset: 5,
23 |     focusKey: 'item1',
24 |     focusOffset: 5,
25 |     isBackward: false,
26 |     hasFocus: true,
27 |   });
28 |   const createEditorState = (...args) => {
29 |     const contentState = Draft.convertFromRaw(rawContentState(...args));
30 |     return EditorState.forceSelection(EditorState.createWithContent(contentState), selectionState);
31 |   };
32 |   it('changes block type', () => {
33 |     const text = 'foo `bar` baz';
34 |     const editorState = createEditorState(text, []);
35 |     const matchArr = ['`bar` ', '`', 'bar', '`', ' '];
36 |     matchArr.index = 4;
37 |     matchArr.input = text;
38 |     const newEditorState = changeCurrentInlineStyle(editorState, matchArr, 'CODE');
39 |     expect(newEditorState).not.to.equal(editorState);
40 |     expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(
41 |       rawContentState(
42 |         'foo bar baz',
43 |         [
44 |           {
45 |             length: 3,
46 |             offset: 4,
47 |             style: 'CODE',
48 |           },
49 |         ],
50 |         'CODE',
51 |       ),
52 |     );
53 |   });
54 | });
55 | 


--------------------------------------------------------------------------------
/src/modifiers/__test__/handleBlockType-test.js:
--------------------------------------------------------------------------------
  1 | import { expect } from 'chai';
  2 | import Draft, { EditorState, SelectionState } from 'draft-js';
  3 | import handleBlockType from '../handleBlockType';
  4 | 
  5 | describe('handleBlockType', () => {
  6 |   describe('no markup', () => {
  7 |     const rawContentState = {
  8 |       entityMap: {},
  9 |       blocks: [
 10 |         {
 11 |           key: 'item1',
 12 |           text: '[ ]',
 13 |           type: 'unstyled',
 14 |           depth: 0,
 15 |           inlineStyleRanges: [],
 16 |           entityRanges: [],
 17 |           data: {},
 18 |         },
 19 |       ],
 20 |     };
 21 |     const contentState = Draft.convertFromRaw(rawContentState);
 22 |     const selection = new SelectionState({
 23 |       anchorKey: 'item1',
 24 |       anchorOffset: 3,
 25 |       focusKey: 'item1',
 26 |       focusOffset: 3,
 27 |       isBackward: false,
 28 |       hasFocus: true,
 29 |     });
 30 |     const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
 31 |     it('does not convert block type', () => {
 32 |       const newEditorState = handleBlockType(editorState, ' ');
 33 |       expect(newEditorState).to.equal(editorState);
 34 |       expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(rawContentState);
 35 |     });
 36 |   });
 37 | 
 38 |   const testCases = {
 39 |     'converts from unstyled to header-one': {
 40 |       before: {
 41 |         entityMap: {},
 42 |         blocks: [
 43 |           {
 44 |             key: 'item1',
 45 |             text: '# Test',
 46 |             type: 'unstyled',
 47 |             depth: 0,
 48 |             inlineStyleRanges: [],
 49 |             entityRanges: [],
 50 |             data: {},
 51 |           },
 52 |         ],
 53 |       },
 54 |       after: {
 55 |         entityMap: {},
 56 |         blocks: [
 57 |           {
 58 |             key: 'item1',
 59 |             text: 'Test ',
 60 |             type: 'header-one',
 61 |             depth: 0,
 62 |             inlineStyleRanges: [],
 63 |             entityRanges: [],
 64 |             data: {},
 65 |           },
 66 |         ],
 67 |       },
 68 |       selection: new SelectionState({
 69 |         anchorKey: 'item1',
 70 |         anchorOffset: 6,
 71 |         focusKey: 'item1',
 72 |         focusOffset: 6,
 73 |         isBackward: false,
 74 |         hasFocus: true,
 75 |       }),
 76 |     },
 77 |     'converts from unstyled to header-two': {
 78 |       before: {
 79 |         entityMap: {},
 80 |         blocks: [
 81 |           {
 82 |             key: 'item1',
 83 |             text: '## Test',
 84 |             type: 'unstyled',
 85 |             depth: 0,
 86 |             inlineStyleRanges: [],
 87 |             entityRanges: [],
 88 |             data: {},
 89 |           },
 90 |         ],
 91 |       },
 92 |       after: {
 93 |         entityMap: {},
 94 |         blocks: [
 95 |           {
 96 |             key: 'item1',
 97 |             text: 'Test ',
 98 |             type: 'header-two',
 99 |             depth: 0,
100 |             inlineStyleRanges: [],
101 |             entityRanges: [],
102 |             data: {},
103 |           },
104 |         ],
105 |       },
106 |       selection: new SelectionState({
107 |         anchorKey: 'item1',
108 |         anchorOffset: 7,
109 |         focusKey: 'item1',
110 |         focusOffset: 7,
111 |         isBackward: false,
112 |         hasFocus: true,
113 |       }),
114 |     },
115 |     'converts from unstyled to header-three': {
116 |       before: {
117 |         entityMap: {},
118 |         blocks: [
119 |           {
120 |             key: 'item1',
121 |             text: '### Test',
122 |             type: 'unstyled',
123 |             depth: 0,
124 |             inlineStyleRanges: [],
125 |             entityRanges: [],
126 |             data: {},
127 |           },
128 |         ],
129 |       },
130 |       after: {
131 |         entityMap: {},
132 |         blocks: [
133 |           {
134 |             key: 'item1',
135 |             text: 'Test ',
136 |             type: 'header-three',
137 |             depth: 0,
138 |             inlineStyleRanges: [],
139 |             entityRanges: [],
140 |             data: {},
141 |           },
142 |         ],
143 |       },
144 |       selection: new SelectionState({
145 |         anchorKey: 'item1',
146 |         anchorOffset: 8,
147 |         focusKey: 'item1',
148 |         focusOffset: 8,
149 |         isBackward: false,
150 |         hasFocus: true,
151 |       }),
152 |     },
153 |     'converts from unstyled to header-four': {
154 |       before: {
155 |         entityMap: {},
156 |         blocks: [
157 |           {
158 |             key: 'item1',
159 |             text: '#### Test',
160 |             type: 'unstyled',
161 |             depth: 0,
162 |             inlineStyleRanges: [],
163 |             entityRanges: [],
164 |             data: {},
165 |           },
166 |         ],
167 |       },
168 |       after: {
169 |         entityMap: {},
170 |         blocks: [
171 |           {
172 |             key: 'item1',
173 |             text: 'Test ',
174 |             type: 'header-four',
175 |             depth: 0,
176 |             inlineStyleRanges: [],
177 |             entityRanges: [],
178 |             data: {},
179 |           },
180 |         ],
181 |       },
182 |       selection: new SelectionState({
183 |         anchorKey: 'item1',
184 |         anchorOffset: 9,
185 |         focusKey: 'item1',
186 |         focusOffset: 9,
187 |         isBackward: false,
188 |         hasFocus: true,
189 |       }),
190 |     },
191 |     'converts from unstyled to header-five': {
192 |       before: {
193 |         entityMap: {},
194 |         blocks: [
195 |           {
196 |             key: 'item1',
197 |             text: '##### Test',
198 |             type: 'unstyled',
199 |             depth: 0,
200 |             inlineStyleRanges: [],
201 |             entityRanges: [],
202 |             data: {},
203 |           },
204 |         ],
205 |       },
206 |       after: {
207 |         entityMap: {},
208 |         blocks: [
209 |           {
210 |             key: 'item1',
211 |             text: 'Test ',
212 |             type: 'header-five',
213 |             depth: 0,
214 |             inlineStyleRanges: [],
215 |             entityRanges: [],
216 |             data: {},
217 |           },
218 |         ],
219 |       },
220 |       selection: new SelectionState({
221 |         anchorKey: 'item1',
222 |         anchorOffset: 10,
223 |         focusKey: 'item1',
224 |         focusOffset: 10,
225 |         isBackward: false,
226 |         hasFocus: true,
227 |       }),
228 |     },
229 |     'converts from unstyled to header-six': {
230 |       before: {
231 |         entityMap: {},
232 |         blocks: [
233 |           {
234 |             key: 'item1',
235 |             text: '###### Test',
236 |             type: 'unstyled',
237 |             depth: 0,
238 |             inlineStyleRanges: [],
239 |             entityRanges: [],
240 |             data: {},
241 |           },
242 |         ],
243 |       },
244 |       after: {
245 |         entityMap: {},
246 |         blocks: [
247 |           {
248 |             key: 'item1',
249 |             text: 'Test ',
250 |             type: 'header-six',
251 |             depth: 0,
252 |             inlineStyleRanges: [],
253 |             entityRanges: [],
254 |             data: {},
255 |           },
256 |         ],
257 |       },
258 |       selection: new SelectionState({
259 |         anchorKey: 'item1',
260 |         anchorOffset: 11,
261 |         focusKey: 'item1',
262 |         focusOffset: 11,
263 |         isBackward: false,
264 |         hasFocus: true,
265 |       }),
266 |     },
267 |     'converts from unstyled to unordered-list-item with hyphen': {
268 |       before: {
269 |         entityMap: {},
270 |         blocks: [
271 |           {
272 |             key: 'item1',
273 |             text: '- Test',
274 |             type: 'unstyled',
275 |             depth: 0,
276 |             inlineStyleRanges: [],
277 |             entityRanges: [],
278 |             data: {},
279 |           },
280 |         ],
281 |       },
282 |       after: {
283 |         entityMap: {},
284 |         blocks: [
285 |           {
286 |             key: 'item1',
287 |             text: 'Test ',
288 |             type: 'unordered-list-item',
289 |             depth: 0,
290 |             inlineStyleRanges: [],
291 |             entityRanges: [],
292 |             data: {},
293 |           },
294 |         ],
295 |       },
296 |       selection: new SelectionState({
297 |         anchorKey: 'item1',
298 |         anchorOffset: 6,
299 |         focusKey: 'item1',
300 |         focusOffset: 6,
301 |         isBackward: false,
302 |         hasFocus: true,
303 |       }),
304 |     },
305 |     'converts from unstyled to unordered-list-item with astarisk': {
306 |       before: {
307 |         entityMap: {},
308 |         blocks: [
309 |           {
310 |             key: 'item1',
311 |             text: '* Test',
312 |             type: 'unstyled',
313 |             depth: 0,
314 |             inlineStyleRanges: [],
315 |             entityRanges: [],
316 |             data: {},
317 |           },
318 |         ],
319 |       },
320 |       after: {
321 |         entityMap: {},
322 |         blocks: [
323 |           {
324 |             key: 'item1',
325 |             text: 'Test ',
326 |             type: 'unordered-list-item',
327 |             depth: 0,
328 |             inlineStyleRanges: [],
329 |             entityRanges: [],
330 |             data: {},
331 |           },
332 |         ],
333 |       },
334 |       selection: new SelectionState({
335 |         anchorKey: 'item1',
336 |         anchorOffset: 6,
337 |         focusKey: 'item1',
338 |         focusOffset: 6,
339 |         isBackward: false,
340 |         hasFocus: true,
341 |       }),
342 |     },
343 |     'converts from unstyled to ordered-list-item': {
344 |       before: {
345 |         entityMap: {},
346 |         blocks: [
347 |           {
348 |             key: 'item1',
349 |             text: '2. Test',
350 |             type: 'unstyled',
351 |             depth: 0,
352 |             inlineStyleRanges: [],
353 |             entityRanges: [],
354 |             data: {},
355 |           },
356 |         ],
357 |       },
358 |       after: {
359 |         entityMap: {},
360 |         blocks: [
361 |           {
362 |             key: 'item1',
363 |             text: 'Test ',
364 |             type: 'ordered-list-item',
365 |             depth: 0,
366 |             inlineStyleRanges: [],
367 |             entityRanges: [],
368 |             data: {},
369 |           },
370 |         ],
371 |       },
372 |       selection: new SelectionState({
373 |         anchorKey: 'item1',
374 |         anchorOffset: 7,
375 |         focusKey: 'item1',
376 |         focusOffset: 7,
377 |         isBackward: false,
378 |         hasFocus: true,
379 |       }),
380 |     },
381 |     'converts from unordered-list-item to unchecked checkable-list-item': {
382 |       before: {
383 |         entityMap: {},
384 |         blocks: [
385 |           {
386 |             key: 'item1',
387 |             text: '[] Test',
388 |             type: 'unordered-list-item',
389 |             depth: 0,
390 |             inlineStyleRanges: [],
391 |             entityRanges: [],
392 |             data: {},
393 |           },
394 |         ],
395 |       },
396 |       after: {
397 |         entityMap: {},
398 |         blocks: [
399 |           {
400 |             key: 'item1',
401 |             text: 'Test',
402 |             type: 'checkable-list-item',
403 |             depth: 0,
404 |             inlineStyleRanges: [],
405 |             entityRanges: [],
406 |             data: {
407 |               checked: false,
408 |             },
409 |           },
410 |         ],
411 |       },
412 |       selection: new SelectionState({
413 |         anchorKey: 'item1',
414 |         anchorOffset: 1,
415 |         focusKey: 'item1',
416 |         focusOffset: 1,
417 |         isBackward: false,
418 |         hasFocus: true,
419 |       }),
420 |     },
421 |     'converts from unordered-list-item to checked checkable-list-item': {
422 |       before: {
423 |         entityMap: {},
424 |         blocks: [
425 |           {
426 |             key: 'item1',
427 |             text: '[x]Test',
428 |             type: 'unordered-list-item',
429 |             depth: 0,
430 |             inlineStyleRanges: [],
431 |             entityRanges: [],
432 |             data: {},
433 |           },
434 |         ],
435 |       },
436 |       after: {
437 |         entityMap: {},
438 |         blocks: [
439 |           {
440 |             key: 'item1',
441 |             text: 'Test',
442 |             type: 'checkable-list-item',
443 |             depth: 0,
444 |             inlineStyleRanges: [],
445 |             entityRanges: [],
446 |             data: {
447 |               checked: true,
448 |             },
449 |           },
450 |         ],
451 |       },
452 |       selection: new SelectionState({
453 |         anchorKey: 'item1',
454 |         anchorOffset: 3,
455 |         focusKey: 'item1',
456 |         focusOffset: 3,
457 |         isBackward: false,
458 |         hasFocus: true,
459 |       }),
460 |     },
461 |     'converts from unstyled to blockquote': {
462 |       before: {
463 |         entityMap: {},
464 |         blocks: [
465 |           {
466 |             key: 'item1',
467 |             text: '> Test',
468 |             type: 'unstyled',
469 |             depth: 0,
470 |             inlineStyleRanges: [],
471 |             entityRanges: [],
472 |             data: {},
473 |           },
474 |         ],
475 |       },
476 |       after: {
477 |         entityMap: {},
478 |         blocks: [
479 |           {
480 |             key: 'item1',
481 |             text: 'Test ',
482 |             type: 'blockquote',
483 |             depth: 0,
484 |             inlineStyleRanges: [],
485 |             entityRanges: [],
486 |             data: {},
487 |           },
488 |         ],
489 |       },
490 |       selection: new SelectionState({
491 |         anchorKey: 'item1',
492 |         anchorOffset: 6,
493 |         focusKey: 'item1',
494 |         focusOffset: 6,
495 |         isBackward: false,
496 |         hasFocus: true,
497 |       }),
498 |     },
499 |   };
500 |   Object.keys(testCases).forEach(k => {
501 |     describe(k, () => {
502 |       const testCase = testCases[k];
503 |       const { before, after, selection, character = ' ' } = testCase;
504 |       const contentState = Draft.convertFromRaw(before);
505 |       const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
506 |       it('converts block type', () => {
507 |         const newEditorState = handleBlockType(editorState, character);
508 |         expect(newEditorState).not.to.equal(editorState);
509 |         expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(after);
510 |       });
511 |     });
512 |   });
513 | });
514 | 


--------------------------------------------------------------------------------
/src/modifiers/__test__/handleImage-test.js:
--------------------------------------------------------------------------------
 1 | /* eslint no-unused-expressions: 0 */
 2 | import sinon from 'sinon';
 3 | import { expect } from 'chai';
 4 | import Draft, { EditorState, SelectionState } from 'draft-js';
 5 | import handleImage from '../handleImage';
 6 | 
 7 | describe('handleImage', () => {
 8 |   let beforeRawContentState;
 9 |   let afterRawContentState;
10 |   let selection;
11 |   let fakeInsertImage;
12 | 
13 |   after(() => {
14 |     handleImage.__ResetDependency__('insertImage'); // eslint-disable-line no-underscore-dangle
15 |   });
16 | 
17 |   const createEditorState = text => {
18 |     afterRawContentState = {
19 |       entityMap: {},
20 |       blocks: [
21 |         {
22 |           key: 'item1',
23 |           text: 'Test',
24 |           type: 'unstyled',
25 |           depth: 0,
26 |           inlineStyleRanges: [],
27 |           entityRanges: [],
28 |           data: {},
29 |         },
30 |       ],
31 |     };
32 | 
33 |     beforeRawContentState = {
34 |       entityMap: {},
35 |       blocks: [
36 |         {
37 |           key: 'item1',
38 |           text,
39 |           type: 'unstyled',
40 |           depth: 0,
41 |           inlineStyleRanges: [],
42 |           entityRanges: [],
43 |           data: {},
44 |         },
45 |       ],
46 |     };
47 | 
48 |     selection = new SelectionState({
49 |       anchorKey: 'item1',
50 |       anchorOffset: text.length - 1,
51 |       focusKey: 'item1',
52 |       focusOffset: text.length - 1,
53 |       isBackward: false,
54 |       hasFocus: true,
55 |     });
56 | 
57 |     const contentState = Draft.convertFromRaw(beforeRawContentState);
58 |     const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
59 |     const newContentState = Draft.convertFromRaw(afterRawContentState);
60 |     const newEditorState = EditorState.push(editorState, newContentState, 'insert-image');
61 | 
62 |     fakeInsertImage = sinon.spy(() => newEditorState);
63 | 
64 |     handleImage.__Rewire__('insertImage', fakeInsertImage); // eslint-disable-line no-underscore-dangle
65 | 
66 |     return editorState;
67 |   };
68 | 
69 |   [
70 |     ['if matches src only', '![](http://cultofthepartyparrot.com/parrots/aussieparrot.gif)'],
71 |     ['if matches src and alt', '![alt](http://cultofthepartyparrot.com/parrots/aussieparrot.gif)'],
72 |     ['if matches src, alt and title', '![alt](http://cultofthepartyparrot.com/parrots/aussieparrot.gif "party")'],
73 |   ].forEach(([condition, text]) => {
74 |     describe(condition, () => {
75 |       it('returns new editor state', () => {
76 |         const editorState = createEditorState(text);
77 |         const newEditorState = handleImage(editorState, ' ');
78 |         expect(newEditorState).not.to.equal(editorState);
79 |         expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(afterRawContentState);
80 |         expect(fakeInsertImage).to.have.callCount(1);
81 |       });
82 |     });
83 |   });
84 |   describe('if does not match', () => {
85 |     it('returns old editor state', () => {
86 |       const editorState = createEditorState('yo');
87 |       const newEditorState = handleImage(editorState, ' ');
88 |       expect(newEditorState).to.equal(editorState);
89 |       expect(fakeInsertImage).not.to.have.been.called;
90 |     });
91 |   });
92 | });
93 | 


--------------------------------------------------------------------------------
/src/modifiers/__test__/handleInlineStyle-test.js:
--------------------------------------------------------------------------------
  1 | /* eslint-disable no-unused-vars */
  2 | 
  3 | import { expect } from 'chai';
  4 | import Draft, { EditorState, SelectionState } from 'draft-js';
  5 | import handleInlineStyle from '../handleInlineStyle';
  6 | 
  7 | describe('handleInlineStyle', () => {
  8 |   describe('no markup', () => {
  9 |     const rawContentState = {
 10 |       entityMap: {},
 11 |       blocks: [
 12 |         {
 13 |           key: 'item1',
 14 |           text: 'Test',
 15 |           type: 'unstyled',
 16 |           depth: 0,
 17 |           inlineStyleRanges: [],
 18 |           entityRanges: [],
 19 |           data: {},
 20 |         },
 21 |       ],
 22 |     };
 23 |     const contentState = Draft.convertFromRaw(rawContentState);
 24 |     const selection = new SelectionState({
 25 |       anchorKey: 'item1',
 26 |       anchorOffset: 6,
 27 |       focusKey: 'item1',
 28 |       focusOffset: 6,
 29 |       isBackward: false,
 30 |       hasFocus: true,
 31 |     });
 32 |     const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
 33 |     it('does not convert block type', () => {
 34 |       const newEditorState = handleInlineStyle(editorState, ' ');
 35 |       expect(newEditorState).to.equal(editorState);
 36 |       expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(rawContentState);
 37 |     });
 38 |   });
 39 | 
 40 |   const testCases = {
 41 |     'converts to bold with astarisks': {
 42 |       before: {
 43 |         entityMap: {},
 44 |         blocks: [
 45 |           {
 46 |             key: 'item1',
 47 |             text: 'hello **inline** style',
 48 |             type: 'unstyled',
 49 |             depth: 0,
 50 |             inlineStyleRanges: [],
 51 |             entityRanges: [],
 52 |             data: {},
 53 |           },
 54 |         ],
 55 |       },
 56 |       after: {
 57 |         entityMap: {},
 58 |         blocks: [
 59 |           {
 60 |             key: 'item1',
 61 |             text: 'hello inline style',
 62 |             type: 'unstyled',
 63 |             depth: 0,
 64 |             inlineStyleRanges: [
 65 |               {
 66 |                 length: 6,
 67 |                 offset: 6,
 68 |                 style: 'BOLD',
 69 |               },
 70 |             ],
 71 |             entityRanges: [],
 72 |             data: {},
 73 |           },
 74 |         ],
 75 |       },
 76 |       selection: new SelectionState({
 77 |         anchorKey: 'item1',
 78 |         anchorOffset: 14,
 79 |         focusKey: 'item1',
 80 |         focusOffset: 14,
 81 |         isBackward: false,
 82 |         hasFocus: true,
 83 |       }),
 84 |     },
 85 |     'converts to bold with underscores': {
 86 |       before: {
 87 |         entityMap: {},
 88 |         blocks: [
 89 |           {
 90 |             key: 'item1',
 91 |             text: 'hello __inline__ style',
 92 |             type: 'unstyled',
 93 |             depth: 0,
 94 |             inlineStyleRanges: [],
 95 |             entityRanges: [],
 96 |             data: {},
 97 |           },
 98 |         ],
 99 |       },
100 |       after: {
101 |         entityMap: {},
102 |         blocks: [
103 |           {
104 |             key: 'item1',
105 |             text: 'hello inline style',
106 |             type: 'unstyled',
107 |             depth: 0,
108 |             inlineStyleRanges: [
109 |               {
110 |                 length: 6,
111 |                 offset: 6,
112 |                 style: 'BOLD',
113 |               },
114 |             ],
115 |             entityRanges: [],
116 |             data: {},
117 |           },
118 |         ],
119 |       },
120 |       selection: new SelectionState({
121 |         anchorKey: 'item1',
122 |         anchorOffset: 14,
123 |         focusKey: 'item1',
124 |         focusOffset: 14,
125 |         isBackward: false,
126 |         hasFocus: true,
127 |       }),
128 |     },
129 |     'converts to italic with astarisk': {
130 |       before: {
131 |         entityMap: {},
132 |         blocks: [
133 |           {
134 |             key: 'item1',
135 |             text: 'hello *inline* style',
136 |             type: 'unstyled',
137 |             depth: 0,
138 |             inlineStyleRanges: [],
139 |             entityRanges: [],
140 |             data: {},
141 |           },
142 |         ],
143 |       },
144 |       after: {
145 |         entityMap: {},
146 |         blocks: [
147 |           {
148 |             key: 'item1',
149 |             text: 'hello inline style',
150 |             type: 'unstyled',
151 |             depth: 0,
152 |             inlineStyleRanges: [
153 |               {
154 |                 length: 6,
155 |                 offset: 6,
156 |                 style: 'ITALIC',
157 |               },
158 |             ],
159 |             entityRanges: [],
160 |             data: {},
161 |           },
162 |         ],
163 |       },
164 |       selection: new SelectionState({
165 |         anchorKey: 'item1',
166 |         anchorOffset: 14,
167 |         focusKey: 'item1',
168 |         focusOffset: 14,
169 |         isBackward: false,
170 |         hasFocus: true,
171 |       }),
172 |     },
173 |     'converts to italic with underscore': {
174 |       before: {
175 |         entityMap: {},
176 |         blocks: [
177 |           {
178 |             key: 'item1',
179 |             text: 'hello _inline_ style',
180 |             type: 'unstyled',
181 |             depth: 0,
182 |             inlineStyleRanges: [],
183 |             entityRanges: [],
184 |             data: {},
185 |           },
186 |         ],
187 |       },
188 |       after: {
189 |         entityMap: {},
190 |         blocks: [
191 |           {
192 |             key: 'item1',
193 |             text: 'hello inline style',
194 |             type: 'unstyled',
195 |             depth: 0,
196 |             inlineStyleRanges: [
197 |               {
198 |                 length: 6,
199 |                 offset: 6,
200 |                 style: 'ITALIC',
201 |               },
202 |             ],
203 |             entityRanges: [],
204 |             data: {},
205 |           },
206 |         ],
207 |       },
208 |       selection: new SelectionState({
209 |         anchorKey: 'item1',
210 |         anchorOffset: 14,
211 |         focusKey: 'item1',
212 |         focusOffset: 14,
213 |         isBackward: false,
214 |         hasFocus: true,
215 |       }),
216 |     },
217 |     'combines to italic and bold with astarisks': {
218 |       before: {
219 |         entityMap: {},
220 |         blocks: [
221 |           {
222 |             key: 'item1',
223 |             text: 'hello **inline** style',
224 |             type: 'unstyled',
225 |             depth: 0,
226 |             inlineStyleRanges: [
227 |               {
228 |                 length: 3,
229 |                 offset: 5,
230 |                 style: 'ITALIC',
231 |               },
232 |             ],
233 |             entityRanges: [],
234 |             data: {},
235 |           },
236 |         ],
237 |       },
238 |       after: {
239 |         entityMap: {},
240 |         blocks: [
241 |           {
242 |             key: 'item1',
243 |             text: 'hello inline style',
244 |             type: 'unstyled',
245 |             depth: 0,
246 |             inlineStyleRanges: [
247 |               {
248 |                 length: 7, // FIXME
249 |                 offset: 5,
250 |                 style: 'ITALIC',
251 |               },
252 |               {
253 |                 length: 6,
254 |                 offset: 6,
255 |                 style: 'BOLD',
256 |               },
257 |             ],
258 |             entityRanges: [],
259 |             data: {},
260 |           },
261 |         ],
262 |       },
263 |       selection: new SelectionState({
264 |         anchorKey: 'item1',
265 |         anchorOffset: 14,
266 |         focusKey: 'item1',
267 |         focusOffset: 14,
268 |         isBackward: false,
269 |         hasFocus: true,
270 |       }),
271 |     },
272 |     'converts to code with backquote': {
273 |       before: {
274 |         entityMap: {},
275 |         blocks: [
276 |           {
277 |             key: 'item1',
278 |             text: 'hello `inline` style',
279 |             type: 'unstyled',
280 |             depth: 0,
281 |             inlineStyleRanges: [],
282 |             entityRanges: [],
283 |             data: {},
284 |           },
285 |         ],
286 |       },
287 |       after: {
288 |         entityMap: {},
289 |         blocks: [
290 |           {
291 |             key: 'item1',
292 |             text: 'hello inline style',
293 |             type: 'unstyled',
294 |             depth: 0,
295 |             inlineStyleRanges: [
296 |               {
297 |                 length: 6,
298 |                 offset: 6,
299 |                 style: 'CODE',
300 |               },
301 |             ],
302 |             entityRanges: [],
303 |             data: {},
304 |           },
305 |         ],
306 |       },
307 |       selection: new SelectionState({
308 |         anchorKey: 'item1',
309 |         anchorOffset: 14,
310 |         focusKey: 'item1',
311 |         focusOffset: 14,
312 |         isBackward: false,
313 |         hasFocus: true,
314 |       }),
315 |     },
316 |     'converts to strikethrough with tildes': {
317 |       before: {
318 |         entityMap: {},
319 |         blocks: [
320 |           {
321 |             key: 'item1',
322 |             text: 'hello ~~inline~~ style',
323 |             type: 'unstyled',
324 |             depth: 0,
325 |             inlineStyleRanges: [],
326 |             entityRanges: [],
327 |             data: {},
328 |           },
329 |         ],
330 |       },
331 |       after: {
332 |         entityMap: {},
333 |         blocks: [
334 |           {
335 |             key: 'item1',
336 |             text: 'hello inline style',
337 |             type: 'unstyled',
338 |             depth: 0,
339 |             inlineStyleRanges: [
340 |               {
341 |                 length: 6,
342 |                 offset: 6,
343 |                 style: 'STRIKETHROUGH',
344 |               },
345 |             ],
346 |             entityRanges: [],
347 |             data: {},
348 |           },
349 |         ],
350 |       },
351 |       selection: new SelectionState({
352 |         anchorKey: 'item1',
353 |         anchorOffset: 14,
354 |         focusKey: 'item1',
355 |         focusOffset: 14,
356 |         isBackward: false,
357 |         hasFocus: true,
358 |       }),
359 |     },
360 | 
361 |     // combine tests
362 | 
363 |     'combines to italic and bold with underscores': {
364 |       before: {
365 |         entityMap: {},
366 |         blocks: [
367 |           {
368 |             key: 'item1',
369 |             text: 'hello __inline__ style',
370 |             type: 'unstyled',
371 |             depth: 0,
372 |             inlineStyleRanges: [
373 |               {
374 |                 length: 3,
375 |                 offset: 5,
376 |                 style: 'ITALIC',
377 |               },
378 |             ],
379 |             entityRanges: [],
380 |             data: {},
381 |           },
382 |         ],
383 |       },
384 |       after: {
385 |         entityMap: {},
386 |         blocks: [
387 |           {
388 |             key: 'item1',
389 |             text: 'hello inline style',
390 |             type: 'unstyled',
391 |             depth: 0,
392 |             inlineStyleRanges: [
393 |               {
394 |                 length: 7, // FIXME
395 |                 offset: 5,
396 |                 style: 'ITALIC',
397 |               },
398 |               {
399 |                 length: 6,
400 |                 offset: 6,
401 |                 style: 'BOLD',
402 |               },
403 |             ],
404 |             entityRanges: [],
405 |             data: {},
406 |           },
407 |         ],
408 |       },
409 |       selection: new SelectionState({
410 |         anchorKey: 'item1',
411 |         anchorOffset: 14,
412 |         focusKey: 'item1',
413 |         focusOffset: 14,
414 |         isBackward: false,
415 |         hasFocus: true,
416 |       }),
417 |     },
418 | 
419 |     'combines to bold and italic with underscores': {
420 |       before: {
421 |         entityMap: {},
422 |         blocks: [
423 |           {
424 |             key: 'item1',
425 |             text: 'hello __inline__ style',
426 |             type: 'unstyled',
427 |             depth: 0,
428 |             inlineStyleRanges: [
429 |               {
430 |                 length: 3,
431 |                 offset: 5,
432 |                 style: 'BOLD',
433 |               },
434 |             ],
435 |             entityRanges: [],
436 |             data: {},
437 |           },
438 |         ],
439 |       },
440 |       after: {
441 |         entityMap: {},
442 |         blocks: [
443 |           {
444 |             key: 'item1',
445 |             text: 'hello inline style',
446 |             type: 'unstyled',
447 |             depth: 0,
448 |             inlineStyleRanges: [
449 |               {
450 |                 length: 7, // FIXME
451 |                 offset: 5,
452 |                 style: 'BOLD',
453 |               },
454 |             ],
455 |             entityRanges: [],
456 |             data: {},
457 |           },
458 |         ],
459 |       },
460 |       selection: new SelectionState({
461 |         anchorKey: 'item1',
462 |         anchorOffset: 14,
463 |         focusKey: 'item1',
464 |         focusOffset: 14,
465 |         isBackward: false,
466 |         hasFocus: true,
467 |       }),
468 |     },
469 |   };
470 |   Object.keys(testCases).forEach(k => {
471 |     describe(k, () => {
472 |       const testCase = testCases[k];
473 |       const { before, after, selection, character = ' ' } = testCase;
474 |       const contentState = Draft.convertFromRaw(before);
475 |       const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
476 |       it('converts block type', () => {
477 |         const newEditorState = handleInlineStyle(editorState, character);
478 |         expect(newEditorState).not.to.equal(editorState);
479 |         expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(after);
480 |       });
481 |     });
482 |   });
483 | });
484 | 


--------------------------------------------------------------------------------
/src/modifiers/__test__/handleLink-test.js:
--------------------------------------------------------------------------------
 1 | /* eslint no-unused-expressions: 0 */
 2 | import sinon from 'sinon';
 3 | import { expect } from 'chai';
 4 | import Draft, { EditorState, SelectionState } from 'draft-js';
 5 | import handleLink from '../handleLink';
 6 | 
 7 | describe('handleLink', () => {
 8 |   let beforeRawContentState;
 9 |   let afterRawContentState;
10 |   let selection;
11 |   let fakeInsertLink;
12 | 
13 |   after(() => {
14 |     handleLink.__ResetDependency__('../insertLink'); // eslint-disable-line no-underscore-dangle
15 |   });
16 | 
17 |   const createEditorState = text => {
18 |     afterRawContentState = {
19 |       entityMap: {},
20 |       blocks: [
21 |         {
22 |           key: 'item1',
23 |           text: 'Test',
24 |           type: 'unstyled',
25 |           depth: 0,
26 |           inlineStyleRanges: [],
27 |           entityRanges: [],
28 |           data: {},
29 |         },
30 |       ],
31 |     };
32 | 
33 |     beforeRawContentState = {
34 |       entityMap: {},
35 |       blocks: [
36 |         {
37 |           key: 'item1',
38 |           text,
39 |           type: 'unstyled',
40 |           depth: 0,
41 |           inlineStyleRanges: [],
42 |           entityRanges: [],
43 |           data: {},
44 |         },
45 |       ],
46 |     };
47 | 
48 |     selection = new SelectionState({
49 |       anchorKey: 'item1',
50 |       anchorOffset: text.length - 1,
51 |       focusKey: 'item1',
52 |       focusOffset: text.length - 1,
53 |       isBackward: false,
54 |       hasFocus: true,
55 |     });
56 | 
57 |     const contentState = Draft.convertFromRaw(beforeRawContentState);
58 |     const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
59 |     const newContentState = Draft.convertFromRaw(afterRawContentState);
60 |     const newEditorState = EditorState.push(editorState, newContentState, 'insert-image');
61 | 
62 |     fakeInsertLink = sinon.spy(() => newEditorState);
63 | 
64 |     handleLink.__Rewire__('insertLink', fakeInsertLink); // eslint-disable-line no-underscore-dangle
65 | 
66 |     return editorState;
67 |   };
68 | 
69 |   [
70 |     ['if href only', '[hello](http://cultofthepartyparrot.com/)'],
71 |     ['if href and title', '[hello](http://cultofthepartyparrot.com/ "world")'],
72 |   ].forEach(([condition, text]) => {
73 |     describe(condition, () => {
74 |       it('returns new editor state', () => {
75 |         const editorState = createEditorState(text);
76 |         const newEditorState = handleLink(editorState, ' ');
77 |         expect(newEditorState).not.to.equal(editorState);
78 |         expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(afterRawContentState);
79 |         expect(fakeInsertLink).to.have.callCount(1);
80 |       });
81 |     });
82 |   });
83 |   describe('if does not match', () => {
84 |     it('returns old editor state', () => {
85 |       const editorState = createEditorState('yo');
86 |       const newEditorState = handleLink(editorState, ' ');
87 |       expect(newEditorState).to.equal(editorState);
88 |       expect(fakeInsertLink).not.to.have.been.called;
89 |     });
90 |   });
91 | });
92 | 


--------------------------------------------------------------------------------
/src/modifiers/__test__/handleNewCodeBlock-test.js:
--------------------------------------------------------------------------------
  1 | import { expect } from 'chai';
  2 | import sinon from 'sinon';
  3 | import Draft, { EditorState, SelectionState } from 'draft-js';
  4 | import handleNewCodeBlock from '../handleNewCodeBlock';
  5 | 
  6 | describe('handleNewCodeBlock', () => {
  7 |   describe('in unstyled block with three backquotes', () => {
  8 |     const testNewCodeBlock = (text, data) => () => {
  9 |       const beforeRawContentState = {
 10 |         entityMap: {},
 11 |         blocks: [
 12 |           {
 13 |             key: 'item1',
 14 |             text,
 15 |             type: 'unstyled',
 16 |             depth: 0,
 17 |             inlineStyleRanges: [],
 18 |             entityRanges: [],
 19 |             data: {},
 20 |           },
 21 |         ],
 22 |       };
 23 |       const afterRawContentState = {
 24 |         entityMap: {},
 25 |         blocks: [
 26 |           {
 27 |             key: 'item1',
 28 |             text: '',
 29 |             type: 'code-block',
 30 |             depth: 0,
 31 |             inlineStyleRanges: [],
 32 |             entityRanges: [],
 33 |             data,
 34 |           },
 35 |         ],
 36 |       };
 37 |       const contentState = Draft.convertFromRaw(beforeRawContentState);
 38 |       const selection = new SelectionState({
 39 |         anchorKey: 'item1',
 40 |         anchorOffset: text.length,
 41 |         focusKey: 'item1',
 42 |         focusOffset: text.length,
 43 |         isBackward: false,
 44 |         hasFocus: true,
 45 |       });
 46 |       const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
 47 |       it('creates new code block', () => {
 48 |         const newEditorState = handleNewCodeBlock(editorState);
 49 |         expect(newEditorState).not.to.equal(editorState);
 50 |         expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(afterRawContentState);
 51 |       });
 52 |     };
 53 | 
 54 |     describe('without langugate', testNewCodeBlock('```', {}));
 55 |     describe('with langugate', testNewCodeBlock('```js', { language: 'js' }));
 56 |   });
 57 | 
 58 |   describe('in code block', () => {
 59 |     before(() => {
 60 |       sinon.stub(Draft, 'genKey').returns('item2');
 61 |     });
 62 |     after(() => {
 63 |       Draft.genKey.restore();
 64 |     });
 65 |     const beforeRawContentState = {
 66 |       entityMap: {},
 67 |       blocks: [
 68 |         {
 69 |           key: 'item1',
 70 |           text: 'console.info("hello")',
 71 |           type: 'code-block',
 72 |           depth: 0,
 73 |           inlineStyleRanges: [],
 74 |           entityRanges: [],
 75 |           data: { language: 'js' },
 76 |         },
 77 |       ],
 78 |     };
 79 |     const afterRawContentState = {
 80 |       entityMap: {},
 81 |       blocks: [
 82 |         {
 83 |           key: 'item1',
 84 |           text: 'console.info("hello")',
 85 |           type: 'code-block',
 86 |           depth: 0,
 87 |           inlineStyleRanges: [],
 88 |           entityRanges: [],
 89 |           data: { language: 'js' },
 90 |         },
 91 |         {
 92 |           key: 'item2',
 93 |           text: '',
 94 |           type: 'code-block',
 95 |           depth: 0,
 96 |           inlineStyleRanges: [],
 97 |           entityRanges: [],
 98 |           data: { language: 'js' },
 99 |         },
100 |       ],
101 |     };
102 |     const contentState = Draft.convertFromRaw(beforeRawContentState);
103 |     it('adds new line inside code block', () => {
104 |       const selection = new SelectionState({
105 |         anchorKey: 'item1',
106 |         anchorOffset: 21,
107 |         focusKey: 'item1',
108 |         focusOffset: 21,
109 |         isBackward: false,
110 |         hasFocus: true,
111 |       });
112 |       const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
113 |       const newEditorState = handleNewCodeBlock(editorState);
114 |       expect(newEditorState).not.to.equal(editorState);
115 |       expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(afterRawContentState);
116 |     });
117 |     it('does not add new line even inside code block', () => {
118 |       const selection = new SelectionState({
119 |         anchorKey: 'item1',
120 |         anchorOffset: 10,
121 |         focusKey: 'item1',
122 |         focusOffset: 10,
123 |         isBackward: false,
124 |         hasFocus: true,
125 |       });
126 |       const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
127 |       const newEditorState = handleNewCodeBlock(editorState);
128 |       expect(newEditorState).to.equal(editorState);
129 |       expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(beforeRawContentState);
130 |     });
131 |   });
132 | 
133 |   describe('in unstyled block without three backquotes', () => {
134 |     const rawContentState = {
135 |       entityMap: {},
136 |       blocks: [
137 |         {
138 |           key: 'item1',
139 |           text: '``',
140 |           type: 'unstyled',
141 |           depth: 0,
142 |           inlineStyleRanges: [],
143 |           entityRanges: [],
144 |           data: {},
145 |         },
146 |       ],
147 |     };
148 |     const contentState = Draft.convertFromRaw(rawContentState);
149 |     const selection = new SelectionState({
150 |       anchorKey: 'item1',
151 |       anchorOffset: 2,
152 |       focusKey: 'item1',
153 |       focusOffset: 2,
154 |       isBackward: false,
155 |       hasFocus: true,
156 |     });
157 |     const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
158 |     it('noop', () => {
159 |       const newEditorState = handleNewCodeBlock(editorState);
160 |       expect(newEditorState).to.equal(editorState);
161 |       expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(rawContentState);
162 |     });
163 |   });
164 | });
165 | 


--------------------------------------------------------------------------------
/src/modifiers/__test__/insertEmptyBlock-test.js:
--------------------------------------------------------------------------------
 1 | import { expect } from 'chai';
 2 | import sinon from 'sinon';
 3 | import Draft, { EditorState, SelectionState } from 'draft-js';
 4 | import insertEmptyBlock from '../insertEmptyBlock';
 5 | 
 6 | describe('insertEmptyBlock', () => {
 7 |   before(() => {
 8 |     sinon.stub(Draft, 'genKey').returns('item2');
 9 |   });
10 |   after(() => {
11 |     Draft.genKey.restore();
12 |   });
13 |   const testInsertEmptyBlock = (...args) => () => {
14 |     const [type = 'unstyled', data = {}] = args;
15 |     const firstBlock = {
16 |       key: 'item1',
17 |       text: 'asdf',
18 |       type: 'unstyled',
19 |       depth: 0,
20 |       inlineStyleRanges: [
21 |         {
22 |           length: 2,
23 |           offset: 1,
24 |           style: 'ITALIC',
25 |         },
26 |       ],
27 |       entityRanges: [],
28 |       data: { foo: 'bar' },
29 |     };
30 |     const beforeRawContentState = {
31 |       entityMap: {},
32 |       blocks: [firstBlock],
33 |     };
34 |     const afterRawContentState = {
35 |       entityMap: {},
36 |       blocks: [
37 |         firstBlock,
38 |         {
39 |           key: 'item2',
40 |           text: '',
41 |           type,
42 |           depth: 0,
43 |           inlineStyleRanges: [],
44 |           entityRanges: [],
45 |           data,
46 |         },
47 |       ],
48 |     };
49 |     const contentState = Draft.convertFromRaw(beforeRawContentState);
50 |     const selection = new SelectionState({
51 |       anchorKey: 'item1',
52 |       anchorOffset: 4,
53 |       focusKey: 'item1',
54 |       focusOffset: 4,
55 |       isBackward: false,
56 |       hasFocus: true,
57 |     });
58 |     const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
59 | 
60 |     it('creates new code block', () => {
61 |       const newEditorState = insertEmptyBlock(editorState, ...args);
62 |       expect(newEditorState).not.to.equal(editorState);
63 |       expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(afterRawContentState);
64 |     });
65 |   };
66 | 
67 |   describe('with arguments', testInsertEmptyBlock('header-one', { bar: 'baz' }));
68 |   describe('without arguments', testInsertEmptyBlock());
69 | });
70 | 


--------------------------------------------------------------------------------
/src/modifiers/__test__/insertImage-test.js:
--------------------------------------------------------------------------------
 1 | import { expect } from 'chai';
 2 | import Draft, { EditorState, SelectionState } from 'draft-js';
 3 | import insertImage from '../insertImage';
 4 | 
 5 | describe('insertImage', () => {
 6 |   const markup = '![bar](http://cultofthepartyparrot.com/parrots/aussieparrot.gif "party")';
 7 |   const text = `foo ${markup} baz`;
 8 |   const beforeRawContentState = {
 9 |     entityMap: {},
10 |     blocks: [
11 |       {
12 |         key: 'item1',
13 |         text,
14 |         type: 'unstyled',
15 |         depth: 0,
16 |         inlineStyleRanges: [],
17 |         entityRanges: [],
18 |         data: {},
19 |       },
20 |     ],
21 |   };
22 |   const afterRawContentState = {
23 |     entityMap: {
24 |       0: {
25 |         data: {
26 |           alt: 'bar',
27 |           src: 'http://cultofthepartyparrot.com/parrots/aussieparrot.gif',
28 |           title: 'party',
29 |         },
30 |         mutability: 'IMMUTABLE',
31 |         type: 'IMG',
32 |       },
33 |     },
34 |     blocks: [
35 |       {
36 |         key: 'item1',
37 |         text: 'foo \u200B  baz',
38 |         type: 'unstyled',
39 |         depth: 0,
40 |         inlineStyleRanges: [],
41 |         entityRanges: [
42 |           {
43 |             key: 0,
44 |             length: 1,
45 |             offset: 4,
46 |           },
47 |         ],
48 |         data: {},
49 |       },
50 |     ],
51 |   };
52 |   const selection = new SelectionState({
53 |     anchorKey: 'item1',
54 |     anchorOffset: 6,
55 |     focusKey: 'item1',
56 |     focusOffset: 6,
57 |     isBackward: false,
58 |     hasFocus: true,
59 |   });
60 |   const contentState = Draft.convertFromRaw(beforeRawContentState);
61 |   const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
62 |   it('converts block type', () => {
63 |     const matchArr = [markup, 'bar', 'http://cultofthepartyparrot.com/parrots/aussieparrot.gif', 'party'];
64 |     matchArr.index = 4;
65 |     matchArr.input = text;
66 |     const newEditorState = insertImage(editorState, matchArr);
67 |     expect(newEditorState).not.to.equal(editorState);
68 |     expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(afterRawContentState);
69 |   });
70 | });
71 | 


--------------------------------------------------------------------------------
/src/modifiers/__test__/insertLink-test.js:
--------------------------------------------------------------------------------
 1 | import { expect } from 'chai';
 2 | import Draft, { EditorState, SelectionState } from 'draft-js';
 3 | import insertLink from '../insertLink';
 4 | 
 5 | describe('insertLink', () => {
 6 |   const markup = '[bar](http://cultofthepartyparrot.com/ "party")';
 7 |   const text = `foo ${markup} baz`;
 8 |   const beforeRawContentState = {
 9 |     entityMap: {},
10 |     blocks: [
11 |       {
12 |         key: 'item1',
13 |         text,
14 |         type: 'unstyled',
15 |         depth: 0,
16 |         inlineStyleRanges: [],
17 |         entityRanges: [],
18 |         data: {},
19 |       },
20 |     ],
21 |   };
22 |   const afterRawContentState = {
23 |     entityMap: {
24 |       0: {
25 |         data: {
26 |           href: 'http://cultofthepartyparrot.com/parrots/aussieparrot.gif',
27 |           title: 'party',
28 |         },
29 |         mutability: 'MUTABLE',
30 |         type: 'LINK',
31 |       },
32 |     },
33 |     blocks: [
34 |       {
35 |         key: 'item1',
36 |         text: 'foo bar  baz',
37 |         type: 'unstyled',
38 |         depth: 0,
39 |         inlineStyleRanges: [],
40 |         entityRanges: [
41 |           {
42 |             key: 0,
43 |             length: 3,
44 |             offset: 4,
45 |           },
46 |         ],
47 |         data: {},
48 |       },
49 |     ],
50 |   };
51 |   const selection = new SelectionState({
52 |     anchorKey: 'item1',
53 |     anchorOffset: 6,
54 |     focusKey: 'item1',
55 |     focusOffset: 6,
56 |     isBackward: false,
57 |     hasFocus: true,
58 |   });
59 |   const contentState = Draft.convertFromRaw(beforeRawContentState);
60 |   const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
61 |   it('converts block type', () => {
62 |     const matchArr = [markup, 'bar', 'http://cultofthepartyparrot.com/parrots/aussieparrot.gif', 'party'];
63 |     matchArr.index = 4;
64 |     matchArr.input = text;
65 |     const newEditorState = insertLink(editorState, matchArr);
66 |     expect(newEditorState).not.to.equal(editorState);
67 |     expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(afterRawContentState);
68 |   });
69 | });
70 | 


--------------------------------------------------------------------------------
/src/modifiers/__test__/insertText-test.js:
--------------------------------------------------------------------------------
 1 | import { expect } from 'chai';
 2 | import Draft, { EditorState, SelectionState } from 'draft-js';
 3 | import insertText from '../insertText';
 4 | 
 5 | describe('insertText', () => {
 6 |   const beforeRawContentState = {
 7 |     entityMap: {},
 8 |     blocks: [
 9 |       {
10 |         key: 'item1',
11 |         text: 'text0',
12 |         type: 'unstyled',
13 |         depth: 0,
14 |         inlineStyleRanges: [],
15 |         entityRanges: [],
16 |         data: {},
17 |       },
18 |     ],
19 |   };
20 |   const afterRawContentState = {
21 |     entityMap: {},
22 |     blocks: [
23 |       {
24 |         key: 'item1',
25 |         text: 'text01',
26 |         type: 'unstyled',
27 |         depth: 0,
28 |         inlineStyleRanges: [],
29 |         entityRanges: [],
30 |         data: {},
31 |       },
32 |     ],
33 |   };
34 |   const selection = new SelectionState({
35 |     anchorKey: 'item1',
36 |     anchorOffset: 5,
37 |     focusKey: 'item1',
38 |     focusOffset: 5,
39 |     isBackward: false,
40 |     hasFocus: true,
41 |   });
42 |   const contentState = Draft.convertFromRaw(beforeRawContentState);
43 |   const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
44 |   it('insert text', () => {
45 |     const newEditorState = insertText(editorState, '1');
46 |     expect(newEditorState).not.to.equal(editorState);
47 |     expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(afterRawContentState);
48 |   });
49 | });
50 | 


--------------------------------------------------------------------------------
/src/modifiers/__test__/leaveList-test.js:
--------------------------------------------------------------------------------
 1 | import { expect } from 'chai';
 2 | import sinon from 'sinon';
 3 | import Draft, { EditorState, SelectionState } from 'draft-js';
 4 | import leaveList from '../leaveList';
 5 | 
 6 | describe('leaveList', () => {
 7 |   before(() => {
 8 |     sinon.stub(Draft, 'genKey').returns('item2');
 9 |   });
10 |   after(() => {
11 |     Draft.genKey.restore();
12 |   });
13 |   ['unordered-list-item', 'ordered-list-item', 'checkable-list-item'].forEach(type => {
14 |     const beforeRawContentState = {
15 |       entityMap: {},
16 |       blocks: [
17 |         {
18 |           key: 'item1',
19 |           text: 'piyo',
20 |           type,
21 |           depth: 0,
22 |           inlineStyleRanges: [],
23 |           entityRanges: [],
24 |           data: {},
25 |         },
26 |       ],
27 |     };
28 |     const afterRawContentState = {
29 |       entityMap: {},
30 |       blocks: [
31 |         {
32 |           key: 'item1',
33 |           text: 'piyo',
34 |           type: 'unstyled',
35 |           depth: 0,
36 |           inlineStyleRanges: [],
37 |           entityRanges: [],
38 |           data: {},
39 |         },
40 |       ],
41 |     };
42 |     const selection = new SelectionState({
43 |       anchorKey: 'item1',
44 |       anchorOffset: 6,
45 |       focusKey: 'item1',
46 |       focusOffset: 6,
47 |       isBackward: false,
48 |       hasFocus: true,
49 |     });
50 |     const contentState = Draft.convertFromRaw(beforeRawContentState);
51 |     const editorState = EditorState.forceSelection(EditorState.createWithContent(contentState), selection);
52 |     it('converts block type', () => {
53 |       const newEditorState = leaveList(editorState);
54 |       expect(newEditorState).not.to.equal(editorState);
55 |       expect(Draft.convertToRaw(newEditorState.getCurrentContent())).to.deep.equal(afterRawContentState);
56 |     });
57 |   });
58 | });
59 | 


--------------------------------------------------------------------------------
/src/modifiers/adjustBlockDepth.js:
--------------------------------------------------------------------------------
 1 | import { CheckableListItemUtils } from 'draft-js-checkable-list-item';
 2 | import { RichUtils } from 'draft-js';
 3 | 
 4 | const adjustBlockDepth = (editorState, ev) => {
 5 |   const newEditorState = CheckableListItemUtils.onTab(ev, editorState, 4);
 6 |   if (newEditorState !== editorState) {
 7 |     return newEditorState;
 8 |   }
 9 |   return RichUtils.onTab(ev, editorState, 4);
10 | };
11 | 
12 | export default adjustBlockDepth;
13 | 


--------------------------------------------------------------------------------
/src/modifiers/changeCurrentBlockType.js:
--------------------------------------------------------------------------------
 1 | import { EditorState } from 'draft-js';
 2 | 
 3 | const changeCurrentBlockType = (editorState, type, text, blockMetadata = {}) => {
 4 |   const currentContent = editorState.getCurrentContent();
 5 |   const selection = editorState.getSelection();
 6 |   const key = selection.getStartKey();
 7 |   const blockMap = currentContent.getBlockMap();
 8 |   const block = blockMap.get(key);
 9 |   const data = block.getData().merge(blockMetadata);
10 |   const newBlock = block.merge({ type, data, text: text || '' });
11 |   const newSelection = selection.merge({
12 |     anchorOffset: 0,
13 |     focusOffset: 0,
14 |   });
15 |   const newContentState = currentContent.merge({
16 |     blockMap: blockMap.set(key, newBlock),
17 |     selectionAfter: newSelection,
18 |   });
19 |   return EditorState.push(editorState, newContentState, 'change-block-type');
20 | };
21 | 
22 | export default changeCurrentBlockType;
23 | 


--------------------------------------------------------------------------------
/src/modifiers/changeCurrentInlineStyle.js:
--------------------------------------------------------------------------------
 1 | import { EditorState, SelectionState, Modifier } from 'draft-js';
 2 | 
 3 | const changeCurrentInlineStyle = (editorState, matchArr, style) => {
 4 |   const currentContent = editorState.getCurrentContent();
 5 |   const selection = editorState.getSelection();
 6 |   const key = selection.getStartKey();
 7 |   const { index } = matchArr;
 8 |   const blockMap = currentContent.getBlockMap();
 9 |   const block = blockMap.get(key);
10 |   const currentInlineStyle = block.getInlineStyleAt(index).merge();
11 |   const newStyle = currentInlineStyle.merge([style]);
12 |   const focusOffset = index + matchArr[0].length;
13 |   const wordSelection = SelectionState.createEmpty(key).merge({
14 |     anchorOffset: index + matchArr[0].indexOf(matchArr[1]),
15 |     focusOffset,
16 |   });
17 |   let newContentState = Modifier.replaceText(currentContent, wordSelection, matchArr[2], newStyle);
18 |   newContentState = Modifier.insertText(newContentState, newContentState.getSelectionAfter(), matchArr[4]);
19 |   const newEditorState = EditorState.push(editorState, newContentState, 'change-inline-style');
20 |   return EditorState.forceSelection(newEditorState, newContentState.getSelectionAfter());
21 | };
22 | 
23 | export default changeCurrentInlineStyle;
24 | 


--------------------------------------------------------------------------------
/src/modifiers/handleBlockType.js:
--------------------------------------------------------------------------------
 1 | import { CHECKABLE_LIST_ITEM } from 'draft-js-checkable-list-item';
 2 | import { RichUtils } from 'draft-js';
 3 | import changeCurrentBlockType from './changeCurrentBlockType';
 4 | 
 5 | const sharps = len => {
 6 |   let ret = '';
 7 |   while (ret.length < len) {
 8 |     ret += '#';
 9 |   }
10 |   return ret;
11 | };
12 | 
13 | const blockTypes = [null, 'header-one', 'header-two', 'header-three', 'header-four', 'header-five', 'header-six'];
14 | 
15 | const handleBlockType = (editorState, character) => {
16 |   const currentSelection = editorState.getSelection();
17 |   const key = currentSelection.getStartKey();
18 |   const text = editorState.getCurrentContent().getBlockForKey(key).getText();
19 |   const position = currentSelection.getAnchorOffset();
20 |   const line = [text.slice(0, position), character, text.slice(position)].join('');
21 |   const blockType = RichUtils.getCurrentBlockType(editorState);
22 |   for (let i = 1; i <= 6; i += 1) {
23 |     if (line.indexOf(`${sharps(i)} `) === 0 && blockType !== 'code-block') {
24 |       return changeCurrentBlockType(editorState, blockTypes[i], line.replace(/^#+\s/, ''));
25 |     }
26 |   }
27 |   let matchArr = line.match(/^[*-] (.*)$/);
28 |   if (matchArr) {
29 |     return changeCurrentBlockType(editorState, 'unordered-list-item', matchArr[1]);
30 |   }
31 |   matchArr = line.match(/^[\d]\. (.*)$/);
32 |   if (matchArr) {
33 |     return changeCurrentBlockType(editorState, 'ordered-list-item', matchArr[1]);
34 |   }
35 |   matchArr = line.match(/^> (.*)$/);
36 |   if (matchArr) {
37 |     return changeCurrentBlockType(editorState, 'blockquote', matchArr[1]);
38 |   }
39 |   matchArr = line.match(/^\[([x ])] (.*)$/i);
40 |   if (matchArr && blockType === 'unordered-list-item') {
41 |     return changeCurrentBlockType(editorState, CHECKABLE_LIST_ITEM, matchArr[2], { checked: matchArr[1] !== ' ' });
42 |   }
43 |   return editorState;
44 | };
45 | 
46 | export default handleBlockType;
47 | 


--------------------------------------------------------------------------------
/src/modifiers/handleImage.js:
--------------------------------------------------------------------------------
 1 | import insertImage from './insertImage';
 2 | 
 3 | const handleImage = (editorState, character) => {
 4 |   const re = /!\[([^\]]*)]\(([^)"]+)(?: "([^"]+)")?\)/g;
 5 |   const key = editorState.getSelection().getStartKey();
 6 |   const text = editorState.getCurrentContent().getBlockForKey(key).getText();
 7 |   const line = `${text}${character}`;
 8 |   let newEditorState = editorState;
 9 |   let matchArr;
10 |   do {
11 |     matchArr = re.exec(line);
12 |     if (matchArr) {
13 |       newEditorState = insertImage(newEditorState, matchArr);
14 |     }
15 |   } while (matchArr);
16 |   return newEditorState;
17 | };
18 | 
19 | export default handleImage;
20 | 


--------------------------------------------------------------------------------
/src/modifiers/handleInlineStyle.js:
--------------------------------------------------------------------------------
 1 | import changeCurrentInlineStyle from './changeCurrentInlineStyle';
 2 | 
 3 | const inlineMatchers = {
 4 |   BOLD: /(?:^|\s|\n|[^A-z0-9_*~`])(\*{2}|_{2})((?!\1).*?)(\1)($|\s|\n|[^A-z0-9_*~`])/g,
 5 |   ITALIC: /(?:^|\s|\n|[^A-z0-9_*~`])(\*{1}|_{1})((?!\1).*?)(\1)($|\s|\n|[^A-z0-9_*~`])/g,
 6 |   CODE: /(?:^|\s|\n|[^A-z0-9_*~`])(`)((?!\1).*?)(\1)($|\s|\n|[^A-z0-9_*~`])/g,
 7 |   STRIKETHROUGH: /(?:^|\s|\n|[^A-z0-9_*~`])(~{2})((?!\1).*?)(\1)($|\s|\n|[^A-z0-9_*~`])/g,
 8 | };
 9 | 
10 | const handleInlineStyle = (editorState, character) => {
11 |   const key = editorState.getSelection().getStartKey();
12 |   const text = editorState.getCurrentContent().getBlockForKey(key).getText();
13 |   const line = `${text}${character}`;
14 |   let newEditorState = editorState;
15 |   Object.keys(inlineMatchers).some(k => {
16 |     const re = inlineMatchers[k];
17 |     let matchArr;
18 |     do {
19 |       matchArr = re.exec(line);
20 |       if (matchArr) {
21 |         newEditorState = changeCurrentInlineStyle(newEditorState, matchArr, k);
22 |       }
23 |     } while (matchArr);
24 |     return newEditorState !== editorState;
25 |   });
26 |   return newEditorState;
27 | };
28 | 
29 | export default handleInlineStyle;
30 | 


--------------------------------------------------------------------------------
/src/modifiers/handleLink.js:
--------------------------------------------------------------------------------
 1 | import insertLink from './insertLink';
 2 | 
 3 | const handleLink = (editorState, character) => {
 4 |   const re = /\[([^\]]+)]\(([^)"]+)(?: "([^"]+)")?\)/g;
 5 |   const key = editorState.getSelection().getStartKey();
 6 |   const text = editorState.getCurrentContent().getBlockForKey(key).getText();
 7 |   const line = `${text}${character}`;
 8 |   let newEditorState = editorState;
 9 |   let matchArr;
10 |   do {
11 |     matchArr = re.exec(line);
12 |     if (matchArr) {
13 |       newEditorState = insertLink(newEditorState, matchArr);
14 |     }
15 |   } while (matchArr);
16 |   return newEditorState;
17 | };
18 | 
19 | export default handleLink;
20 | 


--------------------------------------------------------------------------------
/src/modifiers/handleNewCodeBlock.js:
--------------------------------------------------------------------------------
 1 | import changeCurrentBlockType from './changeCurrentBlockType';
 2 | import insertEmptyBlock from './insertEmptyBlock';
 3 | 
 4 | const handleNewCodeBlock = editorState => {
 5 |   const contentState = editorState.getCurrentContent();
 6 |   const selection = editorState.getSelection();
 7 |   const key = selection.getStartKey();
 8 |   const currentBlock = contentState.getBlockForKey(key);
 9 |   const matchData = /^```([\w-]+)?$/.exec(currentBlock.getText());
10 |   const isLast = selection.getEndOffset() === currentBlock.getLength();
11 |   if (matchData && isLast) {
12 |     const data = {};
13 |     const language = matchData[1];
14 |     if (language) {
15 |       data.language = language;
16 |     }
17 |     return changeCurrentBlockType(editorState, 'code-block', '', data);
18 |   }
19 |   const type = currentBlock.getType();
20 |   if (type === 'code-block' && isLast) {
21 |     return insertEmptyBlock(editorState, 'code-block', currentBlock.getData());
22 |   }
23 |   return editorState;
24 | };
25 | 
26 | export default handleNewCodeBlock;
27 | 


--------------------------------------------------------------------------------
/src/modifiers/insertEmptyBlock.js:
--------------------------------------------------------------------------------
 1 | import { genKey, ContentBlock, EditorState } from 'draft-js';
 2 | import { List, Map } from 'immutable';
 3 | 
 4 | const insertEmptyBlock = (editorState, blockType = 'unstyled', data = {}) => {
 5 |   const contentState = editorState.getCurrentContent();
 6 |   const selection = editorState.getSelection();
 7 |   const key = selection.getStartKey();
 8 |   const currentBlock = contentState.getBlockForKey(key);
 9 |   const emptyBlockKey = genKey();
10 |   const emptyBlock = new ContentBlock({
11 |     characterList: List(),
12 |     depth: 0,
13 |     key: emptyBlockKey,
14 |     text: '',
15 |     type: blockType,
16 |     data: Map().merge(data),
17 |   });
18 |   const blockMap = contentState.getBlockMap();
19 |   const blocksBefore = blockMap.toSeq().takeUntil(value => value === currentBlock);
20 |   const blocksAfter = blockMap
21 |     .toSeq()
22 |     .skipUntil(value => value === currentBlock)
23 |     .rest();
24 |   const augmentedBlocks = [
25 |     [currentBlock.getKey(), currentBlock],
26 |     [emptyBlockKey, emptyBlock],
27 |   ];
28 |   const newBlocks = blocksBefore.concat(augmentedBlocks, blocksAfter).toOrderedMap();
29 |   const focusKey = emptyBlockKey;
30 |   const newContentState = contentState.merge({
31 |     blockMap: newBlocks,
32 |     selectionBefore: selection,
33 |     selectionAfter: selection.merge({
34 |       anchorKey: focusKey,
35 |       anchorOffset: 0,
36 |       focusKey,
37 |       focusOffset: 0,
38 |       isBackward: false,
39 |     }),
40 |   });
41 |   return EditorState.push(editorState, newContentState, 'split-block');
42 | };
43 | 
44 | export default insertEmptyBlock;
45 | 


--------------------------------------------------------------------------------
/src/modifiers/insertImage.js:
--------------------------------------------------------------------------------
 1 | import { EditorState, RichUtils, SelectionState, Modifier } from 'draft-js';
 2 | 
 3 | const insertImage = (editorState, matchArr) => {
 4 |   const currentContent = editorState.getCurrentContent();
 5 |   const selection = editorState.getSelection();
 6 |   const key = selection.getStartKey();
 7 |   const [matchText, alt, src, title] = matchArr;
 8 |   const { index } = matchArr;
 9 |   const focusOffset = index + matchText.length;
10 |   const wordSelection = SelectionState.createEmpty(key).merge({
11 |     anchorOffset: index,
12 |     focusOffset,
13 |   });
14 |   const nextContent = currentContent.createEntity('IMG', 'IMMUTABLE', { alt, src, title });
15 |   const entityKey = nextContent.getLastCreatedEntityKey();
16 |   let newContentState = Modifier.replaceText(nextContent, wordSelection, '\u200B', null, entityKey);
17 |   newContentState = Modifier.insertText(newContentState, newContentState.getSelectionAfter(), ' ');
18 |   const newWordSelection = wordSelection.merge({
19 |     focusOffset: index + 1,
20 |   });
21 |   let newEditorState = EditorState.push(editorState, newContentState, 'insert-image');
22 |   newEditorState = RichUtils.toggleLink(newEditorState, newWordSelection, entityKey);
23 |   return EditorState.forceSelection(newEditorState, newContentState.getSelectionAfter());
24 | };
25 | 
26 | export default insertImage;
27 | 


--------------------------------------------------------------------------------
/src/modifiers/insertLink.js:
--------------------------------------------------------------------------------
 1 | import { EditorState, RichUtils, SelectionState, Modifier } from 'draft-js';
 2 | 
 3 | const insertLink = (editorState, matchArr) => {
 4 |   const currentContent = editorState.getCurrentContent();
 5 |   const selection = editorState.getSelection();
 6 |   const key = selection.getStartKey();
 7 |   const [matchText, text, href, title] = matchArr;
 8 |   const { index } = matchArr;
 9 |   const focusOffset = index + matchText.length;
10 |   const wordSelection = SelectionState.createEmpty(key).merge({
11 |     anchorOffset: index,
12 |     focusOffset,
13 |   });
14 |   const nextContent = currentContent.createEntity('LINK', 'MUTABLE', { href, title });
15 |   const entityKey = nextContent.getLastCreatedEntityKey();
16 |   let newContentState = Modifier.replaceText(nextContent, wordSelection, text, null, entityKey);
17 |   newContentState = Modifier.insertText(newContentState, newContentState.getSelectionAfter(), ' ');
18 |   const newWordSelection = wordSelection.merge({
19 |     focusOffset: index + text.length,
20 |   });
21 |   let newEditorState = EditorState.push(editorState, newContentState, 'insert-link');
22 |   newEditorState = RichUtils.toggleLink(newEditorState, newWordSelection, entityKey);
23 |   return EditorState.forceSelection(newEditorState, newContentState.getSelectionAfter());
24 | };
25 | 
26 | export default insertLink;
27 | 


--------------------------------------------------------------------------------
/src/modifiers/insertText.js:
--------------------------------------------------------------------------------
 1 | import { EditorState, Modifier } from 'draft-js';
 2 | 
 3 | const insertText = (editorState, text) => {
 4 |   const selection = editorState.getSelection();
 5 |   const content = editorState.getCurrentContent();
 6 |   const newContentState = Modifier.insertText(content, selection, text, editorState.getCurrentInlineStyle());
 7 |   return EditorState.push(editorState, newContentState, 'insert-fragment');
 8 | };
 9 | 
10 | export default insertText;
11 | 


--------------------------------------------------------------------------------
/src/modifiers/leaveList.js:
--------------------------------------------------------------------------------
 1 | import { RichUtils } from 'draft-js';
 2 | 
 3 | const leaveList = editorState => {
 4 |   const contentState = editorState.getCurrentContent();
 5 |   const selection = editorState.getSelection();
 6 |   const key = selection.getStartKey();
 7 |   const currentBlock = contentState.getBlockForKey(key);
 8 |   const type = currentBlock.getType();
 9 |   return RichUtils.toggleBlockType(editorState, type);
10 | };
11 | 
12 | export default leaveList;
13 | 


--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
 1 | import { Modifier, EditorState } from 'draft-js';
 2 | 
 3 | export function addText(editorState, bufferText) {
 4 |   const contentState = Modifier.insertText(editorState.getCurrentContent(), editorState.getSelection(), bufferText);
 5 |   return EditorState.push(editorState, contentState, 'insert-characters');
 6 | }
 7 | 
 8 | export function replaceText(editorState, bufferText) {
 9 |   const contentState = Modifier.replaceText(editorState.getCurrentContent(), editorState.getSelection(), bufferText);
10 |   return EditorState.push(editorState, contentState, 'insert-characters');
11 | }
12 | 


--------------------------------------------------------------------------------
/testHelper.js:
--------------------------------------------------------------------------------
 1 | import chai from 'chai';
 2 | import dirtyChai from 'dirty-chai';
 3 | import hook from 'css-modules-require-hook';
 4 | import { jsdom } from 'jsdom';
 5 | import Enzyme from 'enzyme';
 6 | import Adapter from 'enzyme-adapter-react-16';
 7 | 
 8 | process.env.NODE_ENV = 'test';
 9 | 
10 | hook({
11 |   generateScopedName: '[name]__[local]___[hash:base64:5]',
12 | });
13 | 
14 | const exposedProperties = ['window', 'navigator', 'document'];
15 | 
16 | global.document = jsdom('');
17 | global.window = document.defaultView;
18 | Object.keys(document.defaultView).forEach(property => {
19 |   if (typeof global[property] === 'undefined') {
20 |     exposedProperties.push(property);
21 |     global[property] = document.defaultView[property];
22 |   }
23 | });
24 | 
25 | Enzyme.configure({ adapter: new Adapter() });
26 | 
27 | // chaiEnzyme needs to be initialised here, so that canUseDOM is set
28 | // to true when react-dom initialises (which chai-enzyme depends upon)
29 | const chaiEnzyme = require('chai-enzyme');
30 | 
31 | chai.use(dirtyChai);
32 | chai.use(chaiEnzyme());
33 | 


--------------------------------------------------------------------------------