Skip to content
This repository was archived by the owner on Oct 11, 2022. It is now read-only.

Commit dc86d63

Browse files
authored
Merge pull request #39 from juliankrispel/code-block-switch
codeblock language switch
2 parents 117ffd5 + 721981e commit dc86d63

File tree

10 files changed

+362
-23
lines changed

10 files changed

+362
-23
lines changed

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,58 @@ A [DraftJS] plugin for supporting Markdown syntax shortcuts in DraftJS. This plu
1717
npm i --save draft-js-markdown-plugin
1818
```
1919

20+
## Options
21+
The `draft-js-markdown-plugin` is configurable. Just pass a config object. Here are the available options:
22+
23+
24+
### `renderLanguageSelect`
25+
26+
```js
27+
renderLanguageSelect = ({
28+
// Array of language options
29+
options: Array<{ label, value }>,
30+
// Callback to select an option
31+
onChange: (selectedValue: string) => void,
32+
// Value of selected option
33+
selectedValue: string,
34+
// Label of selected option
35+
selectedLabel: string
36+
}) => React.Node
37+
```
38+
39+
Code blocks render a select to switch syntax highlighting - `renderLanguageSelect` is a render function that lets you override how this is rendered.
40+
41+
#### Example:
42+
43+
```
44+
import createMarkdownPlugin from 'draft-js-markdown-plugin';
45+
46+
const renderLanguageSelect = ({ options, onChange, selectedValue }) => (
47+
<select value={selectedValue} onChange={onChange}>
48+
{options.map(({ label, value }) => (
49+
<option key={value} value={value}>
50+
{label}
51+
</option>
52+
))}
53+
</select>
54+
);
55+
56+
const markdownPlugin = createMarkdownPlugin({ renderLanguageSelect })
57+
```
58+
59+
### `languages`
60+
Dictionary for languages available to code block switcher
61+
62+
#### Example:
63+
64+
```js
65+
const languages = {
66+
js: 'JavaScript'
67+
}
68+
69+
const markdownPlugin = createMarkdownPlugin({ languages })
70+
```
71+
2072
## Usage
2173

2274
```js

demo/client/components/DemoEditor/index.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,42 @@ const prismPlugin = createPrismPlugin({
2727
prism: Prism,
2828
});
2929

30-
const plugins = [prismPlugin, createMarkdownShortcutsPlugin()];
30+
const renderLanguageSelect = ({
31+
options,
32+
onChange,
33+
selectedValue,
34+
selectedLabel,
35+
}) => (
36+
<div className={styles.switcherContainer}>
37+
<div className={styles.switcher}>
38+
<select
39+
className={styles.switcherSelect}
40+
value={selectedValue}
41+
onChange={onChange}
42+
>
43+
{options.map(({ label, value }) => (
44+
<option key={value} value={value}>
45+
{label}
46+
</option>
47+
))}
48+
</select>
49+
<div className={styles.switcherLabel}>
50+
{selectedLabel} {String.fromCharCode(9662)}
51+
</div>
52+
</div>
53+
</div>
54+
);
55+
56+
const languages = {
57+
js: "JavaScript",
58+
kotlin: "Kotlin",
59+
mathml: "MathML",
60+
};
61+
62+
const plugins = [
63+
prismPlugin,
64+
createMarkdownShortcutsPlugin({ renderLanguageSelect }),
65+
];
3166

3267
const initialEditorState = EditorState.createWithContent(
3368
convertFromRaw(initialState)

demo/client/components/DemoEditor/styles.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,37 @@
1111
height: 100%;
1212
width: 100%;
1313
}
14+
15+
pre {
16+
background: #f9f9f9;
17+
padding: 0.2em 0.5em;
18+
position: relative;
19+
}
20+
21+
.switcherContainer {
22+
position: absolute;
23+
text-align: right;
24+
bottom: -23px;
25+
right: 3px;
26+
}
27+
28+
.switcher {
29+
display: inline-block;
30+
font-family: sans-serif;
31+
color: #ccc;
32+
letter-spacing: 0.05em;
33+
font-size: 12px;
34+
padding: 0.3em;
35+
cursor: pointer;
36+
position: relative;
37+
}
38+
39+
.switcherSelect {
40+
position: absolute;
41+
top: 0;
42+
cursor: pointer;
43+
opacity: 0;
44+
left: 0;
45+
width: 100%;
46+
height: 100%;
47+
}

package.json

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"clean": "node_modules/.bin/rimraf lib; node_modules/.bin/rimraf demo/public",
1111
"dev": "node_modules/.bin/babel-node ./demo/server.js",
1212
"test": "jest",
13-
"precommit": "lint-staged"
13+
"precommit": "jest && lint-staged"
1414
},
1515
"repository": {
1616
"type": "git",
@@ -65,6 +65,7 @@
6565
"flow-bin": "^0.36.0",
6666
"history": "^2.0.0",
6767
"husky": "^0.14.3",
68+
"jest": "^21.1.0",
6869
"jsdom": "^9.8.3",
6970
"lint-staged": "^4.2.1",
7071
"mocha": "^3.2.0",
@@ -81,30 +82,31 @@
8182
"react-dom": "^15.4.1",
8283
"react-github-corner": "^2.0.0",
8384
"react-github-fork-ribbon": "^0.4.4",
85+
"react-test-renderer": "^16.2.0",
8486
"rimraf": "^2.5.4",
8587
"sinon": "^1.17.6",
8688
"static-site-generator-webpack-plugin": "^3.1.0",
8789
"style-loader": "^0.13.1",
8890
"url-loader": "^0.5.7",
8991
"webpack": "^1.13.3",
9092
"webpack-dev-middleware": "^1.8.4",
91-
"webpack-hot-middleware": "^2.13.2",
92-
"jest": "^21.1.0",
93-
"react-test-renderer": "^16.2.0"
93+
"webpack-hot-middleware": "^2.13.2"
9494
},
9595
"peerDependencies": {
9696
"draft-js-plugins-editor": "~2.0.0-rc.1 || 2.0.0-rc2 || 2.0.0-rc1 || 2.0.0-beta12",
9797
"react": "^15.0.0",
98-
"react-dom": "^15.0.0"
98+
"react-dom": "^15.0.0",
99+
"react-portal": "^4.1.4"
99100
},
100101
"contributors": [
101102
"Atsushi Nagase <a@ngs.io>"
102103
],
103104
"dependencies": {
104105
"decorate-component-with-props": "^1.0.2",
105-
"draft-js": "~0.10.1",
106+
"draft-js": "^0.10.4",
106107
"draft-js-checkable-list-item": "^2.0.5",
107108
"draft-js-prism-plugin": "^0.1.1",
108-
"immutable": "~3.7.4"
109+
"immutable": "~3.7.4",
110+
"react-click-outside": "^3.0.1"
109111
}
110112
}

src/components/Code/index.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import React, { PureComponent } from "react";
2+
import { Map } from "immutable";
3+
import { EditorState, EditorBlock, Modifier } from "draft-js";
4+
import enhanceWithClickOutside from "react-click-outside";
5+
6+
const alias = {
7+
javascript: "js",
8+
jsx: "js",
9+
};
10+
11+
const CodeSwitchContainer = enhanceWithClickOutside(
12+
class SwitchContainer extends PureComponent {
13+
handleClickOutside() {
14+
this.props.onClickOutside();
15+
}
16+
17+
render() {
18+
return (
19+
<div contentEditable={false} onClick={this.props.onClick}>
20+
{this.props.children}
21+
</div>
22+
);
23+
}
24+
}
25+
);
26+
27+
class CodeBlock extends PureComponent {
28+
state = {
29+
isOpen: false,
30+
};
31+
32+
onChange = ev => {
33+
ev.preventDefault();
34+
ev.stopPropagation();
35+
const blockKey = this.props.block.getKey();
36+
const {
37+
getEditorState,
38+
setReadOnly,
39+
setEditorState,
40+
} = this.props.blockProps;
41+
42+
const editorState = getEditorState();
43+
const selection = editorState.getSelection();
44+
const language = ev.currentTarget.value;
45+
const blockSelection = selection.merge({
46+
anchorKey: blockKey,
47+
focusKey: blockKey,
48+
});
49+
50+
let content = editorState.getCurrentContent();
51+
content = Modifier.mergeBlockData(
52+
content,
53+
blockSelection,
54+
Map({ language })
55+
);
56+
setReadOnly(false);
57+
58+
const newEditorState = EditorState.push(
59+
editorState,
60+
content,
61+
"change-block-data"
62+
);
63+
64+
setEditorState(EditorState.forceSelection(newEditorState, selection));
65+
};
66+
67+
cancelClicks = event => event.preventDefault();
68+
69+
onSelectClick = event => {
70+
const { setReadOnly } = this.props.blockProps;
71+
event.stopPropagation();
72+
setReadOnly(true);
73+
};
74+
75+
onClickOutside = () => {
76+
const {
77+
getEditorState,
78+
setReadOnly,
79+
setEditorState,
80+
} = this.props.blockProps;
81+
82+
setReadOnly(false);
83+
84+
const editorState = getEditorState();
85+
const selection = editorState.getSelection();
86+
87+
setEditorState(EditorState.forceSelection(editorState, selection));
88+
};
89+
90+
render() {
91+
const {
92+
languages,
93+
renderLanguageSelect,
94+
language: _language,
95+
} = this.props.blockProps;
96+
97+
const language = alias[_language] || _language;
98+
const selectedLabel = languages[language];
99+
const selectedValue = language;
100+
101+
const options = Object.keys(languages).reduce(
102+
(acc, val) => [
103+
...acc,
104+
{
105+
label: languages[val],
106+
value: val,
107+
},
108+
],
109+
[]
110+
);
111+
112+
return (
113+
<div>
114+
<EditorBlock {...this.props} />
115+
<CodeSwitchContainer
116+
onClickOutside={this.onClickOutside}
117+
onClick={this.onSelectClick}
118+
>
119+
{renderLanguageSelect({
120+
selectedLabel,
121+
selectedValue,
122+
onChange: this.onChange,
123+
options,
124+
})}
125+
</CodeSwitchContainer>
126+
</div>
127+
);
128+
}
129+
}
130+
131+
export default CodeBlock;

src/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ export const inlineMatchers = {
66
CODE: [/`([^`]+)`$/g],
77
STRIKETHROUGH: [/~~([^(?:~~)]+)~~$/g],
88
};
9+
10+
export const CODE_BLOCK_TYPE = "code-block";

src/decorators/image/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import createImageStrategy from "./imageStrategy";
22
import Image from "../../components/Image";
33

4-
const createImageDecorator = (config, store) => ({
5-
strategy: createImageStrategy(config, store),
4+
const createImageDecorator = () => ({
5+
strategy: createImageStrategy(),
66
component: Image,
77
});
88

src/decorators/link/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import createLinkStrategy from "./linkStrategy";
22
import Link from "../../components/Link";
33

4-
const createLinkDecorator = (config, store) => ({
5-
strategy: createLinkStrategy(config, store),
4+
const createLinkDecorator = () => ({
5+
strategy: createLinkStrategy(),
66
component: Link,
77
});
88

0 commit comments

Comments
 (0)