├── .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 | 
4 | [](#backers) [](#sponsors) [][npm]
5 | [](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 | 
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 | '
',
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 |
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('