├── .babelrc
├── .eslintrc.json
├── .gitignore
├── README.md
├── demo
├── _prism.scss
├── application.jsx
├── application.scss
├── html-editor.jsx
├── index.html
├── markdown-editor.jsx
└── state-to-gfm.js
├── images
└── demo.gif
├── package.json
└── src
├── create-markless-plugin.js
└── index.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "react",
4 | "es2015"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "extends": "eslint:recommended",
7 | "parserOptions": {
8 | "ecmaFeatures": {
9 | "experimentalObjectRestSpread": true,
10 | "jsx": true
11 | },
12 | "sourceType": "module"
13 | },
14 | "plugins": [
15 | "react"
16 | ],
17 | "rules": {
18 | "indent": [
19 | "error",
20 | 2
21 | ],
22 | "linebreak-style": [
23 | "error",
24 | "unix"
25 | ],
26 | "quotes": [
27 | "error",
28 | "double"
29 | ],
30 | "semi": [
31 | "error",
32 | "always"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /docs
2 | /lib
3 | /node_modules
4 | /npm-debug.log
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # draft-js-markless-plugin
2 |
3 | [](https://www.npmjs.com/package/draft-js-markless-plugin)
4 |
5 | A plugin for draft-js that allows you to create a markdown-like keybinding WYSIWYG editor.
6 |
7 | ## Demo
8 |
9 | https://r7kamura.github.io/draft-js-markless-plugin/
10 |
11 | [](https://r7kamura.github.io/draft-js-markless-plugin/)
12 |
--------------------------------------------------------------------------------
/demo/_prism.scss:
--------------------------------------------------------------------------------
1 | ../node_modules/prismjs/themes/prism.css
--------------------------------------------------------------------------------
/demo/application.jsx:
--------------------------------------------------------------------------------
1 | import { ContentState, EditorState } from "draft-js";
2 | import { stateFromMarkdown } from "@r7kamura/draft-js-import-markdown";
3 | import stateToMarkdown from "./state-to-gfm";
4 | import HtmlEditor from "./html-editor.jsx";
5 | import MarkdownEditor from "./markdown-editor.jsx";
6 | import React from "react";
7 | import ReactDOM from "react-dom";
8 |
9 | class Root extends React.Component {
10 | constructor(...args) {
11 | super(...args);
12 | this.state = {
13 | htmlEditorState: EditorState.createWithContent(
14 | stateFromMarkdown(this.props.initialValue)
15 | ),
16 | htmlActive: true,
17 | };
18 | }
19 |
20 | onHtmlEditorStateChange(editorState) {
21 | this.setState({ htmlEditorState: editorState });
22 | }
23 |
24 | onMarkdownEditorStateChange(editorState) {
25 | this.setState({ markdownEditorState: editorState });
26 | }
27 |
28 | onHtmlTabClicked() {
29 | if (!this.state.htmlActive) {
30 | this.setState({
31 | htmlActive: true,
32 | htmlEditorState: EditorState.createWithContent(
33 | stateFromMarkdown(
34 | this.state.markdownEditorState.getCurrentContent().getPlainText()
35 | )
36 | ),
37 | });
38 | }
39 | }
40 |
41 | onMarkdownTabClicked() {
42 | if (this.state.htmlActive) {
43 | this.setState({
44 | htmlActive: false,
45 | markdownEditorState: EditorState.createWithContent(
46 | ContentState.createFromText(
47 | stateToMarkdown(
48 | this.state.htmlEditorState.getCurrentContent()
49 | )
50 | )
51 | ),
52 | });
53 | }
54 | }
55 |
56 | render() {
57 | return(
58 |
59 |
69 |
70 |
71 |
79 |
80 | {
81 | this.state.htmlActive &&
82 |
86 | }
87 | {
88 | !this.state.htmlActive &&
89 |
93 | }
94 |
95 |
96 |
97 |
98 | );
99 | }
100 | }
101 |
102 | const initialValue = `
103 | # draft-js-markless-plugin
104 |
105 | draft-js-markless-plugin is a plugin for draft-js that allows you to create a markdown-like keybinding WYSIWYG editor.
106 |
107 | 1. Markdown-like keybindings
108 | 2. Nice default behaviors for writing text
109 | 3. Built on draft.js
110 |
111 | 
112 |
113 | ## Repository
114 |
115 | https://github.com/r7kamura/draft-js-markless-plugin
116 |
117 | ## LICENSE
118 |
119 | draft-js-markless-plugin is MIT licensed.
120 | `;
121 |
122 | ReactDOM.render(
123 | ,
124 | document.getElementById("root")
125 | );
126 |
--------------------------------------------------------------------------------
/demo/application.scss:
--------------------------------------------------------------------------------
1 | @import "./prism";
2 |
3 | body {
4 | background-color: #F7F7F7;
5 | }
6 |
7 | .container {
8 | max-width: 980px;
9 | }
10 |
11 | .header {
12 | background-color: #222;
13 | padding: 10px 20px 100px;
14 |
15 | &-description {
16 | color: rgba(255, 255, 255, .54);
17 | }
18 | }
19 |
20 | .markdown-body {
21 | font-size: 15px;
22 |
23 | h1 {
24 | border-bottom: solid 1px #DDD;
25 | padding-bottom: 5px;
26 | font-size: 22px;
27 | }
28 |
29 | h2 {
30 | font-size: 20px;
31 | }
32 |
33 | h3 {
34 | font-size: 18px;
35 | }
36 |
37 | h4 {
38 | font-size: 16px;
39 | }
40 |
41 | code {
42 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
43 | font-size: 12px;
44 | }
45 |
46 | pre {
47 | background-color: #f7f7f7;
48 | border-radius: 3px;
49 | font-size: 85%;
50 | font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
51 | line-height: 1.45;
52 | margin-bottom: 0;
53 | margin-top: 0;
54 | overflow: auto;
55 | padding: 16px;
56 | word-wrap: normal;
57 | }
58 |
59 | ul {
60 | margin-left: 2.7em;
61 |
62 | li {
63 | list-style-type: disc;
64 | }
65 | }
66 |
67 | img {
68 | max-width: 100%;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/demo/html-editor.jsx:
--------------------------------------------------------------------------------
1 | import { Entity } from "draft-js";
2 | import createAutoListPlugin from "draft-js-autolist-plugin";
3 | import createBlockBreakoutPlugin from "draft-js-block-breakout-plugin";
4 | import createLinkifyPlugin from "draft-js-linkify-plugin";
5 | import createMarklessPlugin from "../src/index.js";
6 | import Editor from "draft-js-plugins-editor";
7 | import Prism from "prismjs";
8 | import React from "react";
9 |
10 | const autoListPlugin = createAutoListPlugin();
11 | const blockBreakoutPlugin = createBlockBreakoutPlugin({
12 | breakoutBlocks: [
13 | "blockquote",
14 | "header-five",
15 | "header-four",
16 | "header-one",
17 | "header-six",
18 | "header-three",
19 | "header-two",
20 | ]
21 | });
22 | const linkifyPlugin = createLinkifyPlugin();
23 | const marklessPlugin = createMarklessPlugin();
24 | const plugins = [
25 | autoListPlugin,
26 | blockBreakoutPlugin,
27 | linkifyPlugin,
28 | marklessPlugin,
29 | ];
30 |
31 | const ImageComponent = (props) => {
32 | const { alt, src } = Entity.get(props.entityKey).getData();
33 | return
;
34 | };
35 |
36 | const LinkComponent = (props) => {
37 | const { url } = Entity.get(props.entityKey).getData();
38 | return(
39 |
40 | {props.children}
41 |
42 | );
43 | };
44 |
45 | const TokenComponent = (props) => {
46 | const sections = props.offsetKey.split("-");
47 | const blockKey = sections[0];
48 | const offset = sections[1];
49 | const tokenType = tokensCache[blockKey][offset];
50 | const block = props.getEditorState().getCurrentContent().getBlockForKey(blockKey);
51 | return(
52 |
53 | {props.children}
54 |
55 | );
56 | };
57 |
58 | const tokensCache = {};
59 |
60 | const decorators = [
61 | {
62 | strategy: function (contentBlock, callback) {
63 | contentBlock.findEntityRanges(
64 | (character) => {
65 | const entityKey = character.getEntity();
66 | return entityKey !== null && Entity.get(entityKey).getType() === "LINK";
67 | },
68 | callback,
69 | );
70 | },
71 | component: LinkComponent,
72 | },
73 | {
74 | strategy: function (contentBlock, callback) {
75 | contentBlock.findEntityRanges(
76 | (character) => {
77 | const entityKey = character.getEntity();
78 | return entityKey !== null && Entity.get(entityKey).getType() === "IMAGE";
79 | },
80 | callback
81 | );
82 | },
83 | component: ImageComponent,
84 | },
85 | {
86 | strategy: function (contentBlock, callback) {
87 | if (contentBlock.getType() === "code-block") {
88 | const languageName = contentBlock.getData().get("languageName");
89 | const language = Prism.languages[languageName];
90 | if (language) {
91 | const tokens = Prism.tokenize(contentBlock.getText(), language);
92 | const blockKey = contentBlock.getKey();
93 | tokensCache[blockKey] = tokensCache[blockKey] || {};
94 | let offset = 0;
95 | tokens.forEach((token) => {
96 | if (typeof token === "string") {
97 | offset += token.length;
98 | } else {
99 | tokensCache[blockKey][offset.toString()] = token.type;
100 | callback(offset, offset + token.content.length);
101 | offset += token.content.length;
102 | }
103 | });
104 | }
105 | }
106 | },
107 | component: TokenComponent,
108 | },
109 | ];
110 |
111 | export default class HtmlEditor extends React.Component {
112 | render() {
113 | return(
114 |
115 |
121 |
122 | );
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | draft-js-markless-plugin
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/demo/markdown-editor.jsx:
--------------------------------------------------------------------------------
1 | import { Editor } from "draft-js";
2 | import React from "react";
3 |
4 | export default class MarkdownEditor extends React.Component {
5 | render() {
6 | return(
7 |
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/demo/state-to-gfm.js:
--------------------------------------------------------------------------------
1 | import {
2 | BLOCK_TYPE,
3 | ENTITY_TYPE,
4 | getEntityRanges,
5 | INLINE_STYLE,
6 | } from "draft-js-utils";
7 | import { Entity } from "draft-js";
8 | import {
9 | ContentBlock,
10 | ContentState,
11 | } from "draft-js";
12 |
13 | const {
14 | BOLD,
15 | CODE,
16 | ITALIC,
17 | STRIKETHROUGH,
18 | UNDERLINE,
19 | } = INLINE_STYLE;
20 |
21 | class MarkdownGeneration {
22 | /**
23 | * @param {ContentState} contentState
24 | */
25 | constructor(contentState) {
26 | this.contentState = contentState;
27 | this.blocks = this.contentState.getBlockMap().toArray();
28 | this.currentBlockIndex = 0;
29 | this.listItemCounts = {};
30 | this.output = [];
31 | this.totalBlocks = this.blocks.length;
32 | }
33 |
34 | /**
35 | * @returns {String}
36 | */
37 | generate() {
38 | this.blocks.map((block, index) => {
39 | this.currentBlockIndex = index;
40 | this.consumeBlock(block);
41 | });
42 | return this.output.join("");
43 | }
44 |
45 | onBlockHeader(block, level) {
46 | this.pushLineBreak();
47 | this.output.push(`${"#".repeat(level)} ${this.renderBlockContent(block)}\n`);
48 | }
49 |
50 | onBlockListItemUnordered(block) {
51 | const blockDepth = block.getDepth();
52 | const blockType = block.getType();
53 | const lastBlock = this.getLastBlock();
54 | const lastBlockType = lastBlock ? lastBlock.getType() : null;
55 | const lastBlockDepth = lastBlock && checkNestableBlockType(lastBlockType) ? lastBlock.getDepth() : null;
56 | if (lastBlockType !== blockType && lastBlockDepth !== blockDepth - 1) {
57 | this.pushLineBreak();
58 | if (lastBlockType === BLOCK_TYPE.ORDERED_LIST_ITEM) {
59 | this.pushLineBreak();
60 | }
61 | }
62 | const listMarker = "-";
63 | const indent = " ".repeat(block.depth * 2);
64 | this.output.push(`${indent}${listMarker} ${this.renderBlockContent(block)}\n`);
65 | }
66 |
67 | onBlockListItemOrdered(block) {
68 | const blockDepth = block.getDepth();
69 | const blockType = block.getType();
70 | const lastBlock = this.getLastBlock();
71 | const lastBlockType = lastBlock ? lastBlock.getType() : null;
72 | const lastBlockDepth = lastBlock && checkNestableBlockType(lastBlockType) ? lastBlock.getDepth() : null;
73 | if (lastBlockType !== blockType && lastBlockDepth !== blockDepth - 1) {
74 | this.pushLineBreak();
75 | if (lastBlockType === BLOCK_TYPE.UNORDERED_LIST_ITEM) {
76 | this.pushLineBreak();
77 | }
78 | }
79 | const listMarker = `${this.getListItemCount(block) % 1000000000}.`;
80 | const indent = " ".repeat(block.depth * 2);
81 | this.output.push(`${indent}${listMarker} ${this.renderBlockContent(block)}\n`);
82 | }
83 |
84 | onBlockQuote(block) {
85 | this.pushLineBreak();
86 | this.output.push(`> ${this.renderBlockContent(block)}\n`);
87 | }
88 |
89 | onBlockCode(block) {
90 | this.pushLineBreak();
91 | const languageName = block.getData().get("languageName") || "";
92 | this.output.push("```" + languageName + "\n");
93 | this.output.push(`${this.renderBlockContent(block)}\n`);
94 | this.output.push("```\n");
95 | }
96 |
97 | onBlockUnknown(block) {
98 | this.pushLineBreak();
99 | this.output.push(`${this.renderBlockContent(block)}\n`);
100 | }
101 |
102 | /**
103 | * @param {ContentBlock} block
104 | */
105 | consumeBlock(block) {
106 | switch (block.getType()) {
107 | case "header-one": {
108 | this.onBlockHeader(block, 1);
109 | break;
110 | }
111 | case "header-two": {
112 | this.onBlockHeader(block, 2);
113 | break;
114 | }
115 | case "header-three": {
116 | this.onBlockHeader(block, 3);
117 | break;
118 | }
119 | case "header-four": {
120 | this.onBlockHeader(block, 4);
121 | break;
122 | }
123 | case "header-five": {
124 | this.onBlockHeader(block, 5);
125 | break;
126 | }
127 | case "header-six": {
128 | this.onBlockHeader(block, 6);
129 | break;
130 | }
131 | case "ordered-list-item": {
132 | this.onBlockListItemOrdered(block);
133 | break;
134 | }
135 | case "unordered-list-item": {
136 | this.onBlockListItemUnordered(block);
137 | break;
138 | }
139 | case "blockquote": {
140 | this.onBlockQuote(block);
141 | break;
142 | }
143 | case "code-block": {
144 | this.onBlockCode(block);
145 | break;
146 | }
147 | default: {
148 | this.onBlockUnknown(block);
149 | break;
150 | }
151 | }
152 | }
153 |
154 | /**
155 | * @returns {ContentBlock}
156 | */
157 | getLastBlock() {
158 | return this.blocks[this.currentBlockIndex - 1];
159 | }
160 |
161 | /**
162 | * @returns {ContentBlock}
163 | */
164 | getNextBlock() {
165 | return this.blocks[this.currentBlockIndex + 1];
166 | }
167 |
168 | /**
169 | * @param {ContentBlock} block
170 | * @returns {Number}
171 | */
172 | getListItemCount(block) {
173 | let blockType = block.getType();
174 | let blockDepth = block.getDepth();
175 | // To decide if we need to start over we need to backtrack (skipping list
176 | // items that are of greater depth)
177 | let index = this.currentBlockIndex - 1;
178 | let prevBlock = this.blocks[index];
179 | while (
180 | prevBlock &&
181 | checkNestableBlockType(prevBlock.getType()) &&
182 | prevBlock.getDepth() > blockDepth
183 | ) {
184 | index -= 1;
185 | prevBlock = this.blocks[index];
186 | }
187 | if (
188 | !prevBlock ||
189 | prevBlock.getType() !== blockType ||
190 | prevBlock.getDepth() !== blockDepth
191 | ) {
192 | this.listItemCounts[blockDepth] = 0;
193 | }
194 | return (
195 | this.listItemCounts[blockDepth] = this.listItemCounts[blockDepth] + 1
196 | );
197 | }
198 |
199 | pushLineBreak() {
200 | if (this.currentBlockIndex > 0) {
201 | this.output.push("\n");
202 | }
203 | }
204 |
205 | /**
206 | * @param {ContentBlock} block
207 | * @returns {String}
208 | */
209 | renderBlockContent(block) {
210 | const text = block.getText();
211 | const zeroWidthSpace = "\u200B";
212 | if (text === "") {
213 | return zeroWidthSpace;
214 | }
215 | let charMetaList = block.getCharacterList();
216 | let entityPieces = getEntityRanges(text, charMetaList);
217 | return entityPieces.map(([entityKey, stylePieces]) => {
218 | let content = stylePieces.map(([text, style]) => {
219 | const encodedText = encodeContent(text || "");
220 | if (encodedText === "") {
221 | return "";
222 | } else if (style.has(BOLD)) {
223 | return `**${encodedText}**`;
224 | } else if (style.has(UNDERLINE)) {
225 | return `++${encodedText}++`; // TODO: encode `+`?
226 | } else if (style.has(ITALIC)) {
227 | return `_${encodedText}_`;
228 | } else if (style.has(STRIKETHROUGH)) {
229 | return `~~${encodedText}~~`; // TODO: encode `~`?
230 | } else if (style.has(CODE)) {
231 | if (block.getType() === "code-block") {
232 | return encodedText;
233 | } else {
234 | return "`" + encodedText + "`";
235 | }
236 | } else {
237 | return encodedText;
238 | }
239 | }).join("");
240 | let entity = entityKey ? Entity.get(entityKey) : null;
241 | if (entity !== null && entity.getType() === ENTITY_TYPE.LINK) {
242 | let data = entity.getData();
243 | let url = data.url || '';
244 | let title = data.title ? ` "${escapeTitle(data.title)}"` : '';
245 | return `[${content}](${encodeURL(url)}${title})`;
246 | } else if (entity != null && entity.getType() === ENTITY_TYPE.IMAGE) {
247 | let data = entity.getData();
248 | return `})`;
249 | } else {
250 | return content;
251 | }
252 | }).join("");
253 | }
254 | }
255 |
256 | /**
257 | * @param {String} blockType
258 | * @returns {Boolean}
259 | */
260 | function checkNestableBlockType(blockType) {
261 | return ["ordered-list-item", "unordered-list-item"].indexOf(blockType) === 0;
262 | }
263 |
264 | /**
265 | * @param {String} text (e.g. "1 * 1")
266 | * @returns {String} (e.g. "1 \\* 1")
267 | */
268 | function encodeContent(text) {
269 | return text.replace(/[*_`]/g, '\\$&');
270 | }
271 |
272 | /**
273 | * @param {String} url (e.g. "https://example.com/\\")
274 | * @returns {String} (e.g. "https://example.com/%29")
275 | */
276 | function encodeURL(url) {
277 | return url.replace(/\)/g, '%29');
278 | }
279 |
280 | /**
281 | * @param {String} text (e.g. "\"foo\"")
282 | * @returns {String} (e.g. "\\\"foo\\\"")
283 | */
284 | function escapeTitle(text) {
285 | return text.replace(/"/g, '\\"');
286 | }
287 |
288 | /**
289 | * @param {ContentState} contentState
290 | * @returns {String}
291 | */
292 | export default function stateToGfm(contentState) {
293 | return new MarkdownGeneration(contentState).generate();
294 | }
295 |
--------------------------------------------------------------------------------
/images/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r7kamura/draft-js-markless-plugin/a8162987b4378abbb6f7065cf9036cef3e3b7551/images/demo.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "draft-js-markless-plugin",
3 | "description": "A plugin for draft-js that allows you to create a markdown-like keybinding WYSIWYG editor.",
4 | "version": "0.0.1",
5 | "author": "Ryo Nakamura (https://github.com/r7kamura)",
6 | "bugs": "https://github.com/r7kamura/draft-js-markless-plugin/issues",
7 | "devDependencies": {
8 | "@r7kamura/draft-js-import-markdown": "^0.1.6",
9 | "babel": "^6.5.2",
10 | "babel-preset-es2015": "^6.14.0",
11 | "babel-preset-react": "^6.11.1",
12 | "babelify": "^7.3.0",
13 | "browserify": "^13.1.0",
14 | "draft-js-autolist-plugin": "0.0.3",
15 | "draft-js-block-breakout-plugin": "0.0.2",
16 | "draft-js-emoji-plugin": "^1.2.3",
17 | "draft-js-linkify-plugin": "^1.0.1",
18 | "draft-js-plugins-editor": "^1.1.0",
19 | "draft-js-utils": "^0.1.5",
20 | "eslint": "^3.4.0",
21 | "eslint-plugin-react": "^6.2.0",
22 | "fixpack": "^2.3.1",
23 | "gh-pages": "^0.11.0",
24 | "node-sass": "^3.8.0",
25 | "prismjs": "^1.5.1",
26 | "react": "15.2.1",
27 | "react-dom": "15.2.1",
28 | "watchify": "^3.7.0"
29 | },
30 | "engines": {
31 | "node": ">= 6.0.0"
32 | },
33 | "homepage": "https://github.com/r7kamura/draft-js-markless-plugin",
34 | "keywords": [
35 | "draft-js",
36 | "draft-js-plugins",
37 | "editor",
38 | "markdown",
39 | "wysiwyg"
40 | ],
41 | "license": "MIT",
42 | "main": "lib/index.js",
43 | "repository": {
44 | "type": "git",
45 | "url": "https://github.com/r7kamura/draft-js-markless-plugin.git"
46 | },
47 | "scripts": {
48 | "build": "npm run build:rm && npm run build:mkdir && npm run build:js && npm run build:demo",
49 | "build:demo": "rm -rf docs && mkdir docs && cp demo/index.html docs && browserify demo/application.jsx -t babelify -o docs/application.js && npm run build:sass",
50 | "build:js": "browserify src/index.js -t babelify -o lib/index.js",
51 | "build:mkdir": "mkdir lib",
52 | "build:rm": "rm -rf lib",
53 | "build:sass": "node-sass demo/application.scss docs/application.css",
54 | "lint": "npm run lint:fixpack && npm run lint:eslint",
55 | "lint:eslint": "eslint src/**/*.{js,jsx}",
56 | "lint:fixpack": "fixpack",
57 | "publish": "gh-pages -d docs",
58 | "test": "npm run lint && npm run build",
59 | "watch": "npm run build && npm run watch:js & npm run watch:demo",
60 | "watch:demo": "watchify demo/application.jsx -t babelify -o docs/application.js -v",
61 | "watch:js": "watchify src/index.js -t babelify -o lib/index.js -v"
62 | },
63 | "dependencies": {
64 | "draft-js": "^0.8.1",
65 | "immutable": "^3.8.1"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/create-markless-plugin.js:
--------------------------------------------------------------------------------
1 | import { genKey, ContentBlock, EditorState, RichUtils } from "draft-js";
2 | import { List } from "immutable";
3 |
4 | /**
5 | * @param {EditorState} editorState
6 | * @param {String} type
7 | * @returns {EditorState}
8 | */
9 | const changeCurrentBlockType = (editorState, type, blockMetadata = {}) => {
10 | const currentContent = editorState.getCurrentContent();
11 | const selection = editorState.getSelection();
12 | const key = selection.getStartKey();
13 | const blockMap = currentContent.getBlockMap();
14 | const block = blockMap.get(key);
15 | const newBlock = block.merge({
16 | type,
17 | data: block.getData().merge(blockMetadata),
18 | text: "",
19 | });
20 | const newSelection = selection.merge({
21 | anchorOffset: 0,
22 | focusOffset: 0,
23 | });
24 | const newContentState = currentContent.merge({
25 | blockMap: blockMap.set(key, newBlock),
26 | selectionAfter: newSelection,
27 | });
28 | return EditorState.push(
29 | editorState,
30 | newContentState,
31 | "change-block-type"
32 | );
33 | };
34 |
35 | export default function createMarklessPlugin () {
36 | return {
37 | handleBeforeInput(character, { getEditorState, setEditorState }) {
38 | const editorState = getEditorState();
39 | const key = editorState.getSelection().getStartKey();
40 | const text = editorState.getCurrentContent().getBlockForKey(key).getText();
41 | switch (`${text}${character}`) {
42 | case "# ":
43 | setEditorState(changeCurrentBlockType(editorState, "header-one"));
44 | return true;
45 | case "## ":
46 | setEditorState(changeCurrentBlockType(editorState, "header-two"));
47 | return true;
48 | case "### ":
49 | setEditorState(changeCurrentBlockType(editorState, "header-three"));
50 | return true;
51 | case "#### ":
52 | setEditorState(changeCurrentBlockType(editorState, "header-four"));
53 | return true;
54 | case "##### ":
55 | setEditorState(changeCurrentBlockType(editorState, "header-five"));
56 | return true;
57 | case "###### ":
58 | setEditorState(changeCurrentBlockType(editorState, "header-six"));
59 | return true;
60 | case "> ":
61 | setEditorState(changeCurrentBlockType(editorState, "blockquote"));
62 | return true;
63 | default:
64 | return false;
65 | }
66 | },
67 |
68 | handleReturn(event, { getEditorState, setEditorState }) {
69 | const editorState = getEditorState();
70 | const contentState = editorState.getCurrentContent();
71 | const selection = editorState.getSelection();
72 | const key = selection.getStartKey();
73 | const currentBlock = contentState.getBlockForKey(key);
74 | const targetString = "```";
75 | const matchData = /^```([\w-]+)?$/.exec(currentBlock.getText());
76 | if (matchData && selection.getEndOffset() === currentBlock.getText().length) {
77 | setEditorState(changeCurrentBlockType(editorState, "code-block", { languageName: matchData[1] }));
78 | return true;
79 | }
80 | const currentBlockType = RichUtils.getCurrentBlockType(editorState);
81 | if (["blockquote", "code-block"].indexOf(currentBlockType) !== -1) {
82 | if (event.ctrlKey) {
83 | const emptyBlockKey = genKey();
84 | const emptyBlock = new ContentBlock({
85 | characterList: List(),
86 | depth: 0,
87 | key: emptyBlockKey,
88 | text: "",
89 | type: "unstyled",
90 | })
91 | const blockMap = contentState.getBlockMap();
92 | const blocksBefore = blockMap.toSeq().takeUntil((value) => value === currentBlock);
93 | const blocksAfter = blockMap.toSeq().skipUntil((value) => value === currentBlock).rest();
94 | const augmentedBlocks = [
95 | [
96 | currentBlock.getKey(),
97 | currentBlock,
98 | ],
99 | [
100 | emptyBlockKey,
101 | emptyBlock,
102 | ],
103 | ];
104 | const newBlocks = blocksBefore.concat(augmentedBlocks, blocksAfter).toOrderedMap();
105 | const focusKey = emptyBlockKey;
106 | const newContentState = contentState.merge({
107 | blockMap: newBlocks,
108 | selectionBefore: selection,
109 | selectionAfter: selection.merge({
110 | anchorKey: focusKey,
111 | anchorOffset: 0,
112 | focusKey: focusKey,
113 | focusOffset: 0,
114 | isBackward: false,
115 | }),
116 | });
117 | setEditorState(
118 | EditorState.push(
119 | editorState,
120 | newContentState,
121 | "split-block"
122 | )
123 | );
124 | return true;
125 | } else {
126 | setEditorState(RichUtils.insertSoftNewline(editorState));
127 | return true;
128 | }
129 | }
130 | }
131 | };
132 | }
133 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import createMarklessPlugin from "./create-markless-plugin";
2 |
3 | export default createMarklessPlugin;
4 |
--------------------------------------------------------------------------------