├── .npmignore
├── src
├── MarkdownEditor
│ ├── LinkDialog
│ │ ├── index.js
│ │ └── LinkDialog.js
│ ├── index.js
│ ├── ToolbarPanel
│ │ ├── index.js
│ │ ├── FlexWrapper.js
│ │ ├── Button.js
│ │ ├── ToolbarSection.js
│ │ ├── ToolbarPanel.js
│ │ ├── DropDown.js
│ │ └── buttonsSchema.js
│ ├── codemirrorOverride.css
│ ├── MarkdownEditor.js
│ └── formatting.js
└── example
│ └── index.js
├── .gitignore
├── test
├── .eslintrc
├── helpers
│ └── setup-browser-env.js
├── MarkdownEditor.js
└── formatting.js
├── .eslintrc
├── webpack-ava.config.js
├── .babelrc
├── example
└── index.html
├── webpack-example.config.js
├── webpack-dev-server.config.js
├── LICENSE
├── webpack-prod.config.js
├── README.md
└── package.json
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | !dist/**/*
3 | !README.md
4 |
--------------------------------------------------------------------------------
/src/MarkdownEditor/LinkDialog/index.js:
--------------------------------------------------------------------------------
1 | import LinkDialog from './LinkDialog'
2 |
3 | export default LinkDialog
4 |
--------------------------------------------------------------------------------
/src/MarkdownEditor/index.js:
--------------------------------------------------------------------------------
1 | import MarkdownEditor from './MarkdownEditor'
2 |
3 | export default MarkdownEditor
4 |
--------------------------------------------------------------------------------
/src/MarkdownEditor/ToolbarPanel/index.js:
--------------------------------------------------------------------------------
1 | import ToolbarPanel from './ToolbarPanel'
2 |
3 | export default ToolbarPanel
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Dependency directory
6 | node_modules
7 |
8 | bundle
9 | dist
10 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "import/no-extraneous-dependencies": ["error", {"devDependencies": true}]
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true
4 | },
5 | "extends": "airbnb",
6 | "rules": {
7 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
8 | "comma-dangle": ["error", "never"],
9 | "semi" : [2, "never"],
10 | "import/no-unresolved": [0],
11 | "quotes": ["error", "single", { "allowTemplateLiterals": true }]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/webpack-ava.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | output: {
3 | libraryTarget: 'commonjs2'
4 | },
5 | module: {
6 | debug: true,
7 | loaders: [
8 | {
9 | test: /\.css$/,
10 | loaders: [
11 | 'style-loader',
12 | 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]--[hash:base64:5]'
13 | ]
14 | }
15 | ]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/MarkdownEditor/ToolbarPanel/FlexWrapper.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 |
3 | const FlexWrapper = ({ children }) =>
4 |
5 | {children}
6 |
7 |
8 | export default FlexWrapper
9 |
10 | FlexWrapper.propTypes = {
11 | children: PropTypes.oneOfType([
12 | PropTypes.element,
13 | PropTypes.arrayOf(
14 | PropTypes.element
15 | )
16 | ])
17 | }
18 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "plugins": [
4 | "babel-plugin-transform-class-properties",
5 | "transform-object-rest-spread"
6 | ],
7 | "env": {
8 | "AVA": {
9 | "plugins": [
10 | [
11 | "babel-plugin-webpack-loaders",
12 | {
13 | "config": "${CONFIG}",
14 | "verbose": false,
15 | }
16 | ]
17 | ]
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/test/helpers/setup-browser-env.js:
--------------------------------------------------------------------------------
1 | import jsdom from 'jsdom'
2 | import noop from 'lodash/noop'
3 | import constant from 'lodash/constant'
4 |
5 | global.window = jsdom.jsdom().defaultView
6 | global.navigator = window.navigator
7 | window.document.createRange = constant(
8 | {
9 | setEnd: noop,
10 | setStart: noop,
11 | getBoundingClientRect: constant({}),
12 | getClientRects: constant({})
13 | })
14 | global.document = window.document
15 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Material-UI Markdown Editor
4 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/MarkdownEditor/ToolbarPanel/Button.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import FlatButton from 'material-ui/FlatButton'
3 |
4 | const Button = ({ onClick, style, icon, openDialog, isImageDialog }, { toggleDialog }) => (
5 |
10 | )
11 |
12 | Button.propTypes = {
13 | icon: PropTypes.element,
14 | onClick: PropTypes.func,
15 | isImageDialog: PropTypes.bool,
16 | style: PropTypes.object, //eslint-disable-line
17 | openDialog: PropTypes.bool
18 | }
19 |
20 | Button.contextTypes = {
21 | toggleDialog: PropTypes.func
22 | }
23 |
24 | export default Button
25 |
--------------------------------------------------------------------------------
/webpack-example.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const srcPath = path.join(__dirname, 'src', 'example')
4 | const buildPath = path.join(__dirname, 'example')
5 | const config = {
6 | entry: path.join(srcPath, 'index.js'),
7 | output: {
8 | path: buildPath,
9 | filename: 'bundle.js'
10 | },
11 | module: {
12 | loaders: [
13 | {
14 | test: /\.js$/,
15 | exclude: /(node_modules)/,
16 | loader: 'babel',
17 | query: {
18 | presets: ['react', 'es2015'],
19 | plugins: ['babel-plugin-transform-class-properties']
20 | }
21 | },
22 | {
23 | test: /\.css$/,
24 | loader: 'style-loader!css-loader'
25 | }
26 | ]
27 | }
28 | }
29 |
30 | module.exports = config
31 |
--------------------------------------------------------------------------------
/src/MarkdownEditor/codemirrorOverride.css:
--------------------------------------------------------------------------------
1 | .CodeMirror{
2 | font-family: Roboto, sans-serif;
3 | font-weight: 300;
4 | font-size:18px;
5 | line-height: 26px;
6 | }
7 | .CodeMirror-lines{
8 | padding: 10px;
9 | }
10 |
11 | .CodeMirror-gutters{
12 | height: 258px;
13 | color: #212121;
14 | background-color: #F5F5F5;
15 | }
16 |
17 | .CodeMirror-linenumber{
18 | left: -15px !important;
19 | }
20 | .cm-s-default .cm-header {color: #2196F3;}
21 | .cm-s-default .cm-quote {color: #4CAF50;}
22 |
23 | .cm-s-default .cm-variable,
24 | .cm-s-default .cm-punctuation,
25 | .cm-s-default .cm-property,
26 | .cm-s-default .cm-operator {}
27 | .cm-s-default .cm-variable-2 {color: #03A9F4;}
28 | .cm-s-default .cm-variable-3 {color: #085;}
29 | .cm-s-default .cm-comment {color: #795548;}
30 | .cm-s-default .cm-string {color: #FF5722;}
31 | .cm-s-default .cm-link {color: #3F51B5;}
32 |
--------------------------------------------------------------------------------
/src/MarkdownEditor/ToolbarPanel/ToolbarSection.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import { ToolbarSeparator } from 'material-ui/Toolbar'
3 | import Button from './Button'
4 | import DropDown from './DropDown'
5 |
6 | const ToolbarSection = ({ items }) => (
7 |
8 | {
9 | items.map((item, key) => (
10 | item.isDropDown
11 | ?
12 | :
13 | ))
14 | }
15 |
16 |
17 | )
18 |
19 | ToolbarSection.propTypes = {
20 | items: PropTypes.arrayOf(
21 | PropTypes.shape({
22 | style: PropTypes.object,
23 | onClick: PropTypes.func,
24 | icon: PropTypes.element,
25 | getContext: PropTypes.bool
26 | })
27 | )
28 | }
29 |
30 | export default ToolbarSection
31 |
--------------------------------------------------------------------------------
/src/MarkdownEditor/ToolbarPanel/ToolbarPanel.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import { Toolbar, ToolbarGroup, ToolbarTitle } from 'material-ui/Toolbar'
3 | import ToolbarSection from './ToolbarSection'
4 | import getButtonsSchema from './buttonsSchema'
5 |
6 | const ToolbarPanel = ({ cm, tokens, title }) => (
7 |
8 |
9 | {
10 | getButtonsSchema(cm, tokens).map((section, i) =>
11 |
12 | )
13 | }
14 |
15 |
16 |
17 |
18 |
19 | )
20 |
21 |
22 | ToolbarPanel.propTypes = {
23 | cm: PropTypes.object, //eslint-disable-line
24 | tokens: PropTypes.arrayOf(PropTypes.string),
25 | title: PropTypes.string
26 | }
27 |
28 | export default ToolbarPanel
29 |
--------------------------------------------------------------------------------
/src/MarkdownEditor/ToolbarPanel/DropDown.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import MenuItem from 'material-ui/MenuItem'
3 | import IconButton from 'material-ui/IconButton'
4 | import IconMenu from 'material-ui/IconMenu'
5 |
6 | const DropDown = ({ icon, style, options, onItemTouchTap }) => (
7 |
11 | { icon }
12 |
13 | }
14 | >
15 | {
16 | options.map((option, i) => )
17 | }
18 |
19 | )
20 |
21 | DropDown.propTypes = {
22 | icon: PropTypes.element,
23 | onItemTouchTap: PropTypes.func,
24 | style: PropTypes.object, //eslint-disable-line
25 | options: PropTypes.arrayOf(
26 | PropTypes.shape({
27 | style: PropTypes.object, //eslint-disable-line
28 | primaryText: PropTypes.string
29 | })
30 | )
31 | }
32 |
33 | export default DropDown
34 |
--------------------------------------------------------------------------------
/webpack-dev-server.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const srcPath = path.join(__dirname, 'src', 'example')
4 | const buildPath = path.join(__dirname, 'example')
5 |
6 | const config = {
7 | entry: [
8 | 'webpack/hot/dev-server',
9 | 'webpack/hot/only-dev-server',
10 | path.join(srcPath, 'index.js')
11 | ],
12 | output: {
13 | path: buildPath,
14 | filename: 'bundle.js'
15 | },
16 | devServer: {
17 | contentBase: 'example',
18 | devtool: 'eval',
19 | hot: true,
20 | inline: true,
21 | port: 3000,
22 | outputPath: buildPath
23 | },
24 | devtool: 'source-map',
25 | module: {
26 | loaders: [
27 | {
28 | test: /\.js$/,
29 | exclude: /(node_modules)/,
30 | loaders: [
31 | 'react-hot-loader/webpack',
32 | 'babel?presets[]=react,presets[]=es2015,plugins[]=babel-plugin-transform-class-properties'
33 | ]
34 | },
35 | {
36 | test: /\.css$/,
37 | loader: 'style-loader!css-loader'
38 | }
39 | ]
40 | }
41 | }
42 |
43 | module.exports = config
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Jed Watson
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 |
--------------------------------------------------------------------------------
/src/example/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import AppBar from 'material-ui/AppBar'
4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
5 | import injectTapEventPlugin from 'react-tap-event-plugin' // eslint-disable-line
6 | import 'codemirror/lib/codemirror.css' // import codemirror styles
7 | import MarkdownEditor from '../MarkdownEditor'
8 | import '../MarkdownEditor/codemirrorOverride.css' //
9 |
10 | injectTapEventPlugin()
11 |
12 | const GithubIcon = () =>
13 |
22 |
23 |
24 |
25 | const Example = () => (
26 |
27 |
28 |
}
32 | />
33 |
34 |
38 |
39 |
40 |
41 | )
42 |
43 | ReactDOM.render(
44 | ,
45 | document.getElementById('root')
46 | )
47 |
--------------------------------------------------------------------------------
/webpack-prod.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack'); //eslint-disable-line
3 |
4 | const srcPath = path.join(__dirname, 'src', 'MarkdownEditor')
5 | const buildPath = path.join(__dirname, 'dist')
6 | const filename = 'MarkdownEditor.js'
7 | const config = {
8 | entry: path.join(srcPath, filename),
9 | output: {
10 | path: buildPath,
11 | filename,
12 | library: 'materialUiMarkdownEditor',
13 | libraryTarget: 'umd'
14 | },
15 | externals: {
16 | react: {
17 | root: 'React',
18 | commonjs2: 'react',
19 | commonjs: 'react',
20 | amd: 'react',
21 | umd: 'react'
22 | },
23 | 'react-dom': {
24 | root: 'ReactDOM',
25 | commonjs2: 'react-dom',
26 | commonjs: 'react-dom',
27 | amd: 'react-dom',
28 | umd: 'react-dom'
29 | }
30 | },
31 | module: {
32 | loaders: [
33 | {
34 | test: /\.js$/,
35 | exclude: /(node_modules)/,
36 | loader: 'babel',
37 | query: {
38 | presets: ['react', 'es2015'],
39 | plugins: ['babel-plugin-transform-class-properties']
40 | }
41 | },
42 | {
43 | test: /\.css$/,
44 | loader: 'style-loader!css-loader'
45 | }
46 | ]
47 | },
48 | plugins: [
49 | new webpack.optimize.UglifyJsPlugin({
50 | sourceMap: true,
51 | compress: {
52 | screw_ie8: true,
53 | warnings: false
54 | }
55 | })
56 | ]
57 | }
58 | module.exports = config
59 |
--------------------------------------------------------------------------------
/test/MarkdownEditor.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import test from 'ava'
3 | import CM from 'codemirror'
4 | import Codemirror from 'react-codemirror'
5 | import { mount } from 'enzyme'
6 | import getMuiTheme from 'material-ui/styles/getMuiTheme'
7 | import Markdown from '../src/MarkdownEditor/MarkdownEditor'
8 | import ToolbarPanel from '../src/MarkdownEditor/ToolbarPanel/ToolbarPanel'
9 |
10 | let wrapper
11 |
12 | test.beforeEach(() => {
13 | wrapper = mount(, {
14 | context: {
15 | muiTheme: getMuiTheme()
16 | },
17 | childContextTypes: {
18 | muiTheme: React.PropTypes.object.isRequired
19 | }
20 | })
21 | })
22 |
23 | test('Should render Toolbar component', t =>
24 | t.true(wrapper.containsMatchingElement())
25 | )
26 |
27 | test('Should render component', t =>
28 | t.true(wrapper.containsMatchingElement())
29 | )
30 |
31 | test('Should add CodeMirror reference to the state', t =>
32 | t.true(wrapper.state().cm.codeMirror instanceof CM)
33 | )
34 |
35 | test('Should update state on CodeMirror change', (t) => {
36 | const { codeMirror } = wrapper.state().cm
37 | codeMirror.setValue('foo bar')
38 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
39 | t.is(wrapper.state().code, 'foo bar')
40 | })
41 |
42 | test('Should set CodeMirror mode as "markdown" and lineNumbers opion', (t) => {
43 | const { options } = wrapper.state().cm.codeMirror
44 | t.is(options.mode, 'markdown')
45 | t.true(options.lineNumbers)
46 | })
47 |
48 | test('Should set token on cursor activity', (t) => {
49 | const { codeMirror } = wrapper.state().cm
50 | t.deepEqual(wrapper.state().tokens, [])
51 |
52 | codeMirror.setValue('** foo **')
53 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
54 | CM.signal(codeMirror, 'cursorActivity', codeMirror, {})
55 | t.deepEqual(wrapper.state().tokens, ['strong'])
56 | })
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Material-UI Markdown Editor :writing_hand:
2 | This is a [React.js](https://github.com/facebook/react) Markdown editor component based on [material-ui](https://github.com/callemall/material-ui), built with [CodeMirror](https://github.com/codemirror/codemirror).
3 |
4 | **It is alpha version yet**, any feedback is welcome!
5 |
6 | # Demo & Example
7 |
8 | **Live demo** can be found [here](https://diedsmiling.github.io/material-ui-markdown-editor/).
9 | To build the examples locally, run:
10 | ```
11 | npm install
12 | npm start
13 | ```
14 |
15 | Then open [localhost:3000](http://localhost:3000/) in your browser.
16 |
17 | To test application, run:
18 |
19 | ```
20 | npm test
21 | ```
22 |
23 | # Installation
24 | Via npm:
25 |
26 | ```
27 | npm i material-ui-markdown-editor
28 | ```
29 |
30 | # Usage
31 |
32 | *NOTE*: Codemirror styles and their *overrirde* should be included in the project keeping global selectors. In the following example, styles are included with the help of [css modules](https://github.com/css-modules/css-modules). Style's are not isolated by scope due to the reason, that rules generated by [CodeMirror](https://github.com/codemirror/codemirror) are global. Might be solved in the feature.
33 |
34 | ```js
35 | import React from 'react'
36 | import ReactDOM from 'react-dom'
37 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
38 | import injectTapEventPlugin from 'react-tap-event-plugin'
39 | import MarkdownEditor from 'material-ui-markdown-editor'
40 | import 'codemirror/lib/codemirror.css' // import codemirror styles
41 | import 'material-ui-markdown-editor/dist/MarkdownEditor/codemirrorOverride.css' // import override styles
42 |
43 | injectTapEventPlugin()
44 |
45 | const Example = () => (
46 |
47 |
51 |
52 | )
53 |
54 | ReactDOM.render(
55 | ,
56 | document.getElementById('root')
57 | )
58 | ```
59 |
60 | *PS*:
61 | This [README.md](https://github.com/diedsmiling/material-ui-markdown-editor/blob/master/README.md) was written with this editor :new_moon_with_face:
62 |
--------------------------------------------------------------------------------
/src/MarkdownEditor/LinkDialog/LinkDialog.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import Dialog from 'material-ui/Dialog'
3 | import FlatButton from 'material-ui/FlatButton'
4 | import TextField from 'material-ui/TextField'
5 |
6 | import { getUrl, isUrl, setUrl, updateUrl } from '../formatting'
7 |
8 | export default class LinkDialog extends React.Component {
9 | static propTypes = {
10 | isDialogOpen: PropTypes.bool.isRequired,
11 | cm: PropTypes.object, //eslint-disable-line
12 | isImageDialog: PropTypes.bool,
13 | tokens: PropTypes.arrayOf(PropTypes.string)
14 | }
15 |
16 | static contextTypes = {
17 | toggleDialog: PropTypes.func
18 | }
19 |
20 | constructor() {
21 | super()
22 | this.state = {
23 | url: ''
24 | }
25 | this.onChange = this.onChange.bind(this)
26 | this.insertLink = this.insertLink.bind(this)
27 | }
28 |
29 | componentWillReceiveProps({ tokens, cm }) {
30 | if (tokens[1] && tokens[1] === 'url') {
31 | this.setState({ url: getUrl(cm.codeMirror) })
32 | }
33 | }
34 |
35 | onChange(e) {
36 | this.setState({
37 | url: e.target.value
38 | })
39 | }
40 |
41 | insertLink() {
42 | const { state: { url } } = this
43 | const { props: { cm, tokens, isImageDialog } } = this
44 |
45 | this.setState({ url: '' })
46 | this.context.toggleDialog()()
47 |
48 | return isUrl(tokens) ? updateUrl(cm, url) : setUrl(cm, url, isImageDialog)
49 | }
50 |
51 | render() {
52 | const { isDialogOpen } = this.props
53 | const { toggleDialog } = this.context
54 | return (
55 | ,
63 |
70 | ]}
71 | modal={false}
72 | open={isDialogOpen}
73 | onRequestClose={toggleDialog()}
74 | >
75 |
84 |
85 | )
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/MarkdownEditor/MarkdownEditor.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import Codemirror from 'react-codemirror'
3 | import LinkDialog from './LinkDialog'
4 | import ToolbarPanel from './ToolbarPanel'
5 | import codemirrorMd from 'codemirror/mode/markdown/markdown' // eslint-disable-line
6 | import { getCurrentFormat } from './formatting'
7 |
8 | export default class MarkdownEditor extends React.Component {
9 | static childContextTypes = {
10 | toggleDialog: PropTypes.func
11 | }
12 |
13 | static propTypes = {
14 | code: PropTypes.string,
15 | title: PropTypes.string
16 | }
17 |
18 | constructor() {
19 | super()
20 | this.state = {
21 | tokens: [],
22 | code: '',
23 | isDialogOpen: false,
24 | isImageDialog: false
25 | }
26 | this.updateCode = this.updateCode.bind(this)
27 | this.toggleDialog = this.toggleDialog.bind(this)
28 | }
29 |
30 | getChildContext() {
31 | return {
32 | toggleDialog: this.toggleDialog
33 | }
34 | }
35 |
36 | componentDidMount() {
37 | /* need to trigger rerender since tooblbar is rendered before
38 | Codemirror textarea and we can't get it's ref at first render */
39 | this.setState({
40 | cm: this.cm,
41 | code: this.props.code,
42 | title: this.props.title
43 | }) //eslint-disable-line
44 | this.cm.codeMirror.on('cursorActivity', this.updateTokens.bind(this))
45 | }
46 |
47 | toggleDialog(isImageDialog) {
48 | return () => {
49 | this.setState({ isDialogOpen: !this.state.isDialogOpen, isImageDialog })
50 | }
51 | }
52 |
53 | updateTokens() {
54 | const tokens = getCurrentFormat(this.cm)
55 | this.setState({ tokens })
56 | }
57 |
58 | updateCode(newCode) {
59 | this.setState({
60 | code: newCode
61 | })
62 | }
63 |
64 | render() {
65 | const options = {
66 | lineNumbers: true,
67 | mode: 'markdown'
68 | }
69 |
70 | return (
71 |
72 |
79 |
84 | { this.cm = ref })}
86 | value={this.state.code}
87 | onChange={this.updateCode}
88 | options={options}
89 | />
90 |
91 | )
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "material-ui-markdown-editor",
3 | "version": "0.0.10",
4 | "description": "React.js Markdown editor component based on material-ui",
5 | "main": "dist/MarkdownEditor/index.js",
6 | "module": "src/MarkdownEditor/index.js",
7 | "scripts": {
8 | "test": "CONFIG=$(pwd)/webpack-ava.config.js BABEL_DISABLE_CACHE=1 NODE_ENV=AVA ava",
9 | "prepublish": "npm run build:babel && cp src/MarkdownEditor/codemirrorOverride.css dist/MarkdownEditor/codemirrorOverride.css ",
10 | "test:dev": "CONFIG=$(pwd)/webpack-ava.config.js BABEL_DISABLE_CACHE=1 NODE_ENV=AVA ava --watch --verbose",
11 | "start": "npm run develop",
12 | "develop": "webpack-dev-server --config webpack-dev-server.config.js --progress --hot --colors --inline",
13 | "build": "webpack --config webpack-prod.config.js --progress --colors --inline",
14 | "build:babel": "babel ./src --out-dir ./dist --ignore example",
15 | "build:example": "webpack --config webpack-example.config.js --progress --colors --inline"
16 | },
17 | "ava": {
18 | "require": [
19 | "babel-register",
20 | "./test/helpers/setup-browser-env.js"
21 | ],
22 | "babel": "inherit"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "git+https://github.com/diedsmiling/material-ui-markdown-editor.git"
27 | },
28 | "author": "Lazarev Alexandr",
29 | "license": "ISC",
30 | "bugs": {
31 | "url": "https://github.com/diedsmiling/material-ui-markdown-editor/issues"
32 | },
33 | "homepage": "https://github.com/diedsmiling/material-ui-markdown-editor#readme",
34 | "devDependencies": {
35 | "ava": "^0.16.0",
36 | "babel-cli": "^6.18.0",
37 | "babel-core": "^6.18.2",
38 | "babel-eslint": "^7.1.1",
39 | "babel-loader": "^6.2.7",
40 | "babel-plugin-transform-class-properties": "^6.18.0",
41 | "babel-plugin-transform-object-rest-spread": "^6.20.2",
42 | "babel-plugin-webpack-loaders": "^0.8.0",
43 | "babel-preset-es2015": "^6.18.0",
44 | "babel-preset-react": "^6.16.0",
45 | "browser-env": "^2.0.16",
46 | "css-loader": "^0.25.0",
47 | "enzyme": "^2.6.0",
48 | "eslint": "^3.9.1",
49 | "eslint-config-airbnb": "^13.0.0",
50 | "eslint-plugin-import": "^2.2.0",
51 | "eslint-plugin-jsx-a11y": "^2.2.3",
52 | "eslint-plugin-react": "^6.6.0",
53 | "jsdom": "^9.8.3",
54 | "lodash": "^4.17.2",
55 | "proxyquire": "^1.7.10",
56 | "react-addons-test-utils": "^15.3.2",
57 | "react-hot-loader": "^3.0.0-beta.6",
58 | "sinon": "^1.17.6",
59 | "style-loader": "^0.13.1",
60 | "webpack": "^1.13.3",
61 | "webpack-dev-server": "^1.16.2"
62 | },
63 | "dependencies": {
64 | "codemirror": "^5.21.0",
65 | "jsdom": "^9.8.3",
66 | "lodash": "^4.17.2",
67 | "material-ui": "^0.16.2",
68 | "react": "^15.3.2",
69 | "react-codemirror": "^0.2.6",
70 | "react-dom": "^15.3.2",
71 | "react-tap-event-plugin": "^2.0.1"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/MarkdownEditor/ToolbarPanel/buttonsSchema.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Bold from 'material-ui/svg-icons/editor/format-bold'
3 | import Italic from 'material-ui/svg-icons/editor/format-italic'
4 | import Size from 'material-ui/svg-icons/editor/format-size'
5 | import BulletsList from 'material-ui/svg-icons/editor/format-list-bulleted'
6 | import NumbersList from 'material-ui/svg-icons/editor/format-list-numbered'
7 | import Quote from 'material-ui/svg-icons/editor/format-quote'
8 | import Code from 'material-ui/svg-icons/action/code'
9 | import ImageIcon from 'material-ui/svg-icons/image/image'
10 | import LinkIcon from 'material-ui/svg-icons/editor/insert-link'
11 | import NavigationExpandMoreIcon from 'material-ui/svg-icons/navigation/expand-more'
12 | import { lightBlack } from 'material-ui/styles/colors'
13 | import {
14 | getUrlStyleIfActive,
15 | getStyleIfActive,
16 | isActiveToken,
17 | isNotUrlBorder,
18 | handleHeading,
19 | setBold,
20 | removeBold,
21 | setItalic,
22 | removeItalic,
23 | setUl,
24 | removeUl,
25 | setOl,
26 | removeOl,
27 | setH1,
28 | removeH1,
29 | setH2,
30 | removeH2,
31 | setH3,
32 | removeH3,
33 | setCode,
34 | removeCode,
35 | setQuote,
36 | removeQuote
37 | } from '../formatting'
38 | import FlexWrapper from './FlexWrapper'
39 |
40 | const getSchema = (cm, tokens) => {
41 | const getUrlStyle = getUrlStyleIfActive(cm)
42 | const getActiveStyle = getStyleIfActive(tokens)
43 | const formatBold = setBold(cm)
44 | const cancelBold = removeBold(cm)
45 | const formatItalic = setItalic(cm)
46 | const cancelItalic = removeItalic(cm)
47 | const formatUl = setUl(cm)
48 | const cancelUl = removeUl(cm)
49 | const formatOl = setOl(cm)
50 | const cancelOl = removeOl(cm)
51 | const formatCode = setCode(cm)
52 | const cancelCode = removeCode(cm)
53 | const formatQuote = setQuote(cm)
54 | const cancelQuote = removeQuote(cm)
55 | const handleH1 = isActiveToken('header-1', tokens, 1) ? removeH1(cm) : setH1(cm)
56 | const handleH2 = isActiveToken('header-2', tokens, 1) ? removeH2(cm) : setH2(cm)
57 | const handleH3 = isActiveToken('header-3', tokens, 1) ? removeH3(cm) : setH3(cm)
58 |
59 | return [
60 | [
61 | {
62 | style: {
63 | marginLeft: 24,
64 | height: 'auto',
65 | padding: 6,
66 | ...getActiveStyle('header')
67 | },
68 | isDropDown: true,
69 | onItemTouchTap: handleHeading([handleH1, handleH2, handleH3]),
70 | options: [
71 | {
72 | primaryText: 'Heading 1',
73 | style: { fontSize: 12 }
74 | },
75 | {
76 | primaryText: 'Heading 2',
77 | style: { fontSize: 14 }
78 | },
79 | {
80 | primaryText: 'Heading 3',
81 | style: { fontSize: 16 }
82 | }
83 | ],
84 | icon: (
85 |
86 |
87 |
88 |
89 | )
90 | },
91 | {
92 | style: { ...getActiveStyle('strong') },
93 | icon: ,
94 | onClick: isActiveToken('strong', tokens) ? cancelBold : formatBold
95 | },
96 | {
97 | style: getActiveStyle('em'),
98 | icon: ,
99 | onClick: isActiveToken('em', tokens) ? cancelItalic : formatItalic
100 | }
101 | ],
102 | [
103 | {
104 | style: { marginLeft: 24, ...getActiveStyle('ul') },
105 | icon: ,
106 | onClick: isActiveToken('ul', tokens) ? cancelUl : formatUl
107 | },
108 | {
109 | style: getActiveStyle('ol'),
110 | icon: ,
111 | onClick: isActiveToken('ol', tokens) ? cancelOl : formatOl
112 | }
113 | ],
114 | [
115 | {
116 | style: { marginLeft: 24, ...getActiveStyle('comment') },
117 | icon: ,
118 | onClick: isActiveToken('comment', tokens) ? cancelCode : formatCode
119 | },
120 | {
121 | style: { ...getActiveStyle('quote') },
122 | icon:
,
123 | onClick: isActiveToken('quote', tokens) ? cancelQuote : formatQuote
124 | }
125 | ],
126 | [
127 | {
128 | style: {
129 | marginLeft: 24,
130 | ...(isActiveToken('url', tokens, 1) && isNotUrlBorder(cm.codeMirror) ? getUrlStyle('link') : {})
131 | },
132 | icon: ,
133 | openDialog: true
134 | },
135 | {
136 | style: {
137 | ...(isActiveToken('url', tokens, 1) && isNotUrlBorder(cm.codeMirror) ? getUrlStyle('image') : {})
138 | },
139 | icon: ,
140 | openDialog: true,
141 | isImageDialog: true
142 | }
143 | ]
144 | ]
145 | }
146 |
147 | export default getSchema
148 |
--------------------------------------------------------------------------------
/src/MarkdownEditor/formatting.js:
--------------------------------------------------------------------------------
1 | import { grey400 } from 'material-ui/styles/colors'
2 |
3 | const incrementPosition = (summand, position) => {
4 | const pos = Object.assign({}, position)
5 | pos.ch += summand
6 | return pos
7 | }
8 |
9 | const position = (line, ch) => ({
10 | line, ch
11 | })
12 |
13 | const isEmpty = string => string.length === 0
14 |
15 | const isEmptyOneLineSelection = (line, length) =>
16 | isEmpty(line) && length === 1
17 |
18 | const getPositions = (seekedPosition, positions) =>
19 | positions.filter(pos => seekedPosition > pos[0] && seekedPosition < pos[1])[0]
20 |
21 | const getPlaceholderBySignature = signature => ({
22 | '**': 'Strong text',
23 | '*': 'Emphasized text',
24 | '- ': 'List item',
25 | '#. ': 'List item',
26 | '# ': 'Heading',
27 | '## ': 'Heading',
28 | '### ': 'Heading',
29 | '`': 'Code',
30 | '> ': 'Quote'
31 | }[signature])
32 |
33 | const normalize = (array, signatureLength, accum = []) => {
34 | if (array.length) {
35 | /* Increases end postion with signature length */
36 | const chunk = array.splice(0, 2)
37 | chunk[1] += signatureLength
38 | accum.push(chunk)
39 | normalize(array, signatureLength, accum)
40 | }
41 | return accum
42 | }
43 |
44 | const getMatches = (signature, string, start = 0, accum = []) => {
45 | const index = string.indexOf(signature, start)
46 | if (index > -1) {
47 | accum.push(index)
48 | return getMatches(signature, string, index + signature.length, accum)
49 | }
50 |
51 | return normalize(accum, signature.length)
52 | }
53 |
54 | const getRemovingPartLength = (line, signature) => (
55 | ['- ', '### ', '## ', '# ', '> '].includes(signature) ? signature.length : line.indexOf('.') + 2
56 | )
57 |
58 | const formatLink = pattern => cm => (link) => {
59 | const { codeMirror } = cm
60 | const text = pattern
61 | .replace('##', codeMirror.getSelection())
62 | .replace('$$', link)
63 | codeMirror.replaceSelection(text)
64 | }
65 |
66 | const formatMultiline = signature => cm => () => {
67 | const { codeMirror } = cm
68 | const start = codeMirror.getCursor('start')
69 | const end = codeMirror.getCursor('end')
70 | const length = (end.line - start.line) + 1
71 |
72 | Array(length)
73 | .fill(start.line)
74 | .forEach((from, i) => {
75 | const curentLine = i + 1
76 | const lineNumber = from + i
77 | const line = codeMirror.getLine(lineNumber)
78 | let text = signature +
79 | (isEmptyOneLineSelection(line, length) ? getPlaceholderBySignature(signature) : line)
80 | if (signature === '#. ') {
81 | text = text.replace(signature, `${curentLine}. `)
82 | }
83 | codeMirror.replaceRange(
84 | text,
85 | position(lineNumber, 0),
86 | position(lineNumber, line.length)
87 | )
88 | })
89 |
90 | const lastLineLength = codeMirror.getLine(end.line).length
91 | codeMirror.setSelection(position(start.line, 0), position(end.line, lastLineLength))
92 | codeMirror.focus()
93 | }
94 |
95 | const removeMultiline = signature => cm => () => {
96 | const { codeMirror } = cm
97 | const start = codeMirror.getCursor('start')
98 | const end = codeMirror.getCursor('end')
99 | const length = (end.line - start.line) + 1
100 |
101 | Array(length)
102 | .fill(start.line)
103 | .forEach((from, i) => {
104 | const currentLine = from + i
105 | const line = codeMirror.getLine(currentLine)
106 | codeMirror.replaceRange(
107 | '',
108 | position(currentLine, 0),
109 | position(currentLine, getRemovingPartLength(line, signature))
110 | )
111 | })
112 | }
113 |
114 | const formatInline = signature => cm => () => {
115 | const { codeMirror } = cm
116 |
117 | const selection = codeMirror.getSelection()
118 | const text = isEmpty(selection) ? getPlaceholderBySignature(signature) : selection
119 | const start = codeMirror.getCursor('start')
120 | const end = incrementPosition(text.length, start)
121 |
122 | codeMirror.replaceSelection(signature + text + signature)
123 | codeMirror.setSelection(
124 | ...[start, end].map(pos => incrementPosition(signature.length, pos))
125 | )
126 |
127 | codeMirror.focus()
128 | }
129 |
130 | const removeInline = signature => cm => () => {
131 | const { codeMirror } = cm
132 |
133 | const cursor = codeMirror.getCursor('start')
134 | const line = codeMirror.getLine(cursor.line)
135 |
136 | const [start, end] = getPositions(
137 | cursor.ch,
138 | getMatches(signature, line)
139 | )
140 | const startPoint = position(cursor.line, start)
141 | const endPoint = position(cursor.line, end)
142 | const text =
143 | codeMirror
144 | .getRange(startPoint, endPoint)
145 | .split(signature).join('')
146 |
147 | codeMirror.replaceRange(
148 | text,
149 | startPoint,
150 | endPoint
151 | )
152 | }
153 |
154 | const normalizeList = (types, line) => {
155 | const typesCloned = [...types]
156 | if (typesCloned[0] !== 'variable-2') return typesCloned
157 | typesCloned[0] = line.startsWith('- ') || line.startsWith('* ')
158 | ? typesCloned[0] = 'ul'
159 | : typesCloned[0] = 'ol'
160 |
161 | return typesCloned
162 | }
163 |
164 | export const getCurrentFormat = (cm) => {
165 | const { codeMirror } = cm
166 | const cursor = codeMirror.getCursor('start')
167 | const type = codeMirror.getTokenTypeAt(cursor)
168 | const line = codeMirror.getLine(cursor.line)
169 |
170 | return type ? normalizeList(type.split(' '), line) : []
171 | }
172 |
173 | const findUrlSiblingPosition = (line, pos) => (
174 | line[pos - 1] === ']' || pos === 0
175 | ? (pos - 1)
176 | : findUrlSiblingPosition(line, pos - 1)
177 | )
178 |
179 | export const isNotUrlBorder = (cm) => {
180 | const token = cm.getTokenAt(cm.getCursor())
181 | return token.string !== '(' && token.string !== ')'
182 | }
183 |
184 | export const getUrl = (cm) => {
185 | const token = cm.getTokenAt(cm.getCursor())
186 | return token.string
187 | }
188 |
189 | export const isActiveToken = (token, tokens, index = 0) =>
190 | tokens.length && tokens[index] === token
191 |
192 | export const getStyleIfActive = tokens => token => (
193 | isActiveToken(token, tokens)
194 | ? { backgroundColor: grey400 }
195 | : {}
196 | )
197 |
198 | export const getUrlStyleIfActive = cm => (token) => {
199 | const { codeMirror } = cm
200 | const { line, ch } = codeMirror.getCursor()
201 | const siblingPos = findUrlSiblingPosition(codeMirror.getLine(line), ch)
202 | const tokens = codeMirror.getTokenTypeAt({ line, ch: siblingPos }) || ''
203 | return getStyleIfActive(tokens.split(' '))(token)
204 | }
205 |
206 | export const updateUrl = (cm, url) => {
207 | const { codeMirror } = cm
208 | const cursor = codeMirror.getCursor()
209 | const token = codeMirror.getTokenAt(cursor)
210 | codeMirror.replaceRange(
211 | url,
212 | position(cursor.line, token.start),
213 | position(cursor.line, token.end)
214 | )
215 | }
216 |
217 | export const handleHeading = schema => (e, object) => {
218 | schema[parseInt(object.key, 10)]()
219 | }
220 |
221 | export const isUrl = tokens => tokens[1] && tokens[1] === 'url'
222 |
223 | export const setH1 = formatMultiline('# ')
224 |
225 | export const removeH1 = removeMultiline('# ')
226 |
227 | export const setH2 = formatMultiline('## ')
228 |
229 | export const removeH2 = removeMultiline('## ')
230 |
231 | export const setH3 = formatMultiline('### ')
232 |
233 | export const removeH3 = removeMultiline('### ')
234 |
235 | export const setOl = formatMultiline('#. ')
236 |
237 | export const removeOl = removeMultiline('#. ')
238 |
239 | export const setUl = formatMultiline('- ')
240 |
241 | export const removeUl = removeMultiline('- ')
242 |
243 | export const setBold = formatInline('**')
244 |
245 | export const removeBold = removeInline('**')
246 |
247 | export const setItalic = formatInline('*')
248 |
249 | export const removeItalic = removeInline('*')
250 |
251 | export const setCode = formatInline('`')
252 |
253 | export const removeCode = removeInline('`')
254 |
255 | export const setQuote = formatMultiline('> ')
256 |
257 | export const removeQuote = removeMultiline('> ')
258 |
259 | export const setLink = formatLink('[##]($$)')
260 |
261 | export const setImage = formatLink('')
262 |
263 | export const setUrl = (cm, url, isImageDialog) => (
264 | isImageDialog ? setImage(cm)(url) : setLink(cm)(url)
265 | )
266 |
--------------------------------------------------------------------------------
/test/formatting.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import test from 'ava'
3 | import CM from 'codemirror'
4 | import { mount } from 'enzyme'
5 | import getMuiTheme from 'material-ui/styles/getMuiTheme'
6 | import Markdown from '../src/MarkdownEditor/MarkdownEditor'
7 | import {
8 | getCurrentFormat,
9 | setBold,
10 | setItalic,
11 | removeBold,
12 | removeItalic,
13 | setUl,
14 | removeUl,
15 | setOl,
16 | removeOl,
17 | setH1,
18 | removeH1,
19 | setH2,
20 | removeH2,
21 | setH3,
22 | removeH3
23 | } from '../src/MarkdownEditor/formatting'
24 |
25 | let wrapper
26 | let cm
27 | let codeMirror
28 |
29 | test.beforeEach(() => {
30 | wrapper = mount(, {
31 | context: {
32 | muiTheme: getMuiTheme()
33 | },
34 | childContextTypes: {
35 | muiTheme: React.PropTypes.object.isRequired
36 | }
37 | })
38 | cm = wrapper.state().cm
39 | codeMirror = cm.codeMirror
40 | })
41 |
42 | /* Format recognising */
43 |
44 | test('Should recognise current format and provide it in a form of array', (t) => {
45 | const format = getCurrentFormat(cm)
46 | t.true(Array.isArray(format))
47 | })
48 |
49 | test('Should recognise format', (t) => {
50 | codeMirror.setValue('** Foo **')
51 | t.deepEqual(getCurrentFormat(cm), ['strong'])
52 | })
53 |
54 | test('Should return empty array if no format is recognised', (t) => {
55 | codeMirror.setValue('** Foo ** bar')
56 | codeMirror.setCursor({ line: 0, ch: 11 })
57 | t.deepEqual(getCurrentFormat(cm), [])
58 | })
59 |
60 | test('Should recognise "ul" format', (t) => {
61 | const list = '- foo \n- bar'
62 | codeMirror.setValue(list)
63 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
64 | t.deepEqual(getCurrentFormat(cm), ['ul'])
65 | })
66 |
67 | test('Should recognise "ol" format', (t) => {
68 | const list = '1. foo \n2. bar'
69 | codeMirror.setValue(list)
70 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
71 | t.deepEqual(getCurrentFormat(cm), ['ol'])
72 | })
73 |
74 | /* Bold fomatting */
75 |
76 | test('setBold should return a function', t =>
77 | t.is(typeof setBold(cm), 'function')
78 | )
79 |
80 | test('removeBold should return a function', t =>
81 | t.is(typeof removeBold(cm), 'function')
82 | )
83 |
84 | test('Bold formatting should insert a placeholder when nothing is selected', (t) => {
85 | codeMirror.setValue('')
86 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
87 | setBold(cm)()
88 | t.is(wrapper.state().code, '**Strong text**')
89 | })
90 |
91 | test('Bold formatting should wrap selection in "** **"', (t) => {
92 | codeMirror.setValue('Foo bar')
93 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
94 | codeMirror.setSelection({ line: 0, ch: 4 }, { line: 0, ch: 7 })
95 | setBold(cm)()
96 | t.is(wrapper.state().code, 'Foo **bar**')
97 | })
98 |
99 | test('Bold formatting should select placeholder when it is invoked', (t) => {
100 | codeMirror.setValue('')
101 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
102 | setBold(cm)()
103 | t.is(codeMirror.getSelection(), 'Strong text')
104 | })
105 |
106 | test('Bold formatting should keep selection', (t) => {
107 | codeMirror.setValue('Foo bar')
108 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
109 | codeMirror.setSelection({ line: 0, ch: 4 }, { line: 0, ch: 7 })
110 | setBold(cm)()
111 | t.is(codeMirror.getSelection(), 'bar')
112 | })
113 |
114 | test('Bold "cancellation" should unwrap selection from "**"', (t) => {
115 | codeMirror.setValue('Foo **bar** **baz**')
116 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
117 | codeMirror.setSelection({ line: 0, ch: 6 }, { line: 0, ch: 9 })
118 | removeBold(cm)()
119 | t.is(wrapper.state().code, 'Foo bar **baz**')
120 | })
121 |
122 | test('Bold "cancellation" should unwrap without selection from "**"', (t) => {
123 | codeMirror.setValue('Foo **bar** **baz**')
124 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
125 | codeMirror.setCursor({ line: 0, ch: 8 })
126 | removeBold(cm)()
127 | t.is(wrapper.state().code, 'Foo bar **baz**')
128 | })
129 |
130 | test('Bold "cancellation" should unwrap without selection from "**" on left border', (t) => {
131 | codeMirror.setValue('Foo **bar** **baz**')
132 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
133 | codeMirror.setCursor({ line: 0, ch: 5 })
134 | removeBold(cm)()
135 | t.is(wrapper.state().code, 'Foo bar **baz**')
136 | })
137 |
138 | test('Bold "cancellation" should unwrap without selection from "**" on right border', (t) => {
139 | codeMirror.setValue('Foo **bar** **baz**')
140 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
141 | codeMirror.setCursor({ line: 0, ch: 10 })
142 | removeBold(cm)()
143 | t.is(wrapper.state().code, 'Foo bar **baz**')
144 | })
145 |
146 | /* Italic fomatting */
147 |
148 | test('setItalic should return a function', t =>
149 | t.is(typeof setItalic(cm), 'function')
150 | )
151 |
152 | test('removeItalic should return a function', t =>
153 | t.is(typeof removeItalic(cm), 'function')
154 | )
155 |
156 | test('Italic formatting should insert a placeholder when nothing is selected', (t) => {
157 | codeMirror.setValue('')
158 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
159 | setItalic(cm)()
160 | t.is(wrapper.state().code, '*Emphasized text*')
161 | })
162 |
163 | test('Italic formatting should wrap selection in "* *"', (t) => {
164 | codeMirror.setValue('Foo bar')
165 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
166 | codeMirror.setSelection({ line: 0, ch: 4 }, { line: 0, ch: 7 })
167 | setItalic(cm)()
168 | t.is(wrapper.state().code, 'Foo *bar*')
169 | })
170 |
171 | test('Italic formatting should select placeholder when it is invoked', (t) => {
172 | codeMirror.setValue('')
173 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
174 | setItalic(cm)()
175 | t.is(codeMirror.getSelection(), 'Emphasized text')
176 | })
177 |
178 | test('Italic formatting should keep selection', (t) => {
179 | codeMirror.setValue('Foo bar')
180 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
181 | codeMirror.setSelection({ line: 0, ch: 4 }, { line: 0, ch: 7 })
182 | setItalic(cm)()
183 | t.is(codeMirror.getSelection(), 'bar')
184 | })
185 |
186 | test('Italic "cancellation" should unwrap selection from "*"', (t) => {
187 | codeMirror.setValue('Foo *bar* *baz*')
188 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
189 | codeMirror.setSelection({ line: 0, ch: 5 }, { line: 0, ch: 8 })
190 | removeItalic(cm)()
191 | t.is(wrapper.state().code, 'Foo bar *baz*')
192 | })
193 |
194 | test('Italic "cancellation" should unwrap without selection from "*"', (t) => {
195 | codeMirror.setValue('Foo *bar* *baz*')
196 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
197 | codeMirror.setCursor({ line: 0, ch: 6 })
198 | removeItalic(cm)()
199 | t.is(wrapper.state().code, 'Foo bar *baz*')
200 | })
201 |
202 | /* Nesting fomatting */
203 |
204 | test('Should correctly unformat nesting strings', (t) => {
205 | codeMirror.setValue('***Strong text***')
206 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
207 | codeMirror.setCursor({ line: 0, ch: 6 })
208 | CM.signal(codeMirror, 'cursorActivity', codeMirror, {})
209 | removeItalic(cm)()
210 | t.is(wrapper.state().code, '**Strong text**')
211 | })
212 |
213 | /* Ul fomatting */
214 |
215 | test('setUl should return a function', t =>
216 | t.is(typeof setUl(cm), 'function')
217 | )
218 |
219 | test('removeUl should return a function', t =>
220 | t.is(typeof removeUl(cm), 'function')
221 | )
222 |
223 | test('Ul fomratting should add "- " to every selected line', (t) => {
224 | codeMirror.setValue('foo\nbar')
225 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
226 | codeMirror.setSelection({ line: 0, ch: 0 }, { line: 1, ch: 3 })
227 | setUl(cm)()
228 | t.is(wrapper.state().code, '- foo\n- bar')
229 | })
230 |
231 | test('Ul formatting should select all the lines after formatting', (t) => {
232 | codeMirror.setValue('foo\nbar')
233 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
234 | codeMirror.setSelection({ line: 0, ch: 0 }, { line: 1, ch: 3 })
235 | setUl(cm)()
236 | t.is(codeMirror.getSelection(), '- foo\n- bar')
237 | })
238 |
239 | test('Ul formatting should add "- " to the current line when nothing is selected', (t) => {
240 | codeMirror.setValue('foo')
241 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
242 | setUl(cm)()
243 | t.is(wrapper.state().code, '- foo')
244 | })
245 |
246 | test('Ul formatting should select current line after one line formatting', (t) => {
247 | codeMirror.setValue('foo')
248 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
249 | setUl(cm)()
250 | t.is(codeMirror.getSelection(), '- foo')
251 | })
252 |
253 | test('Ul cancellation should remove "- " from each selected line', (t) => {
254 | codeMirror.setValue('- foo\n- bar')
255 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
256 | codeMirror.setSelection({ line: 0, ch: 0 }, { line: 1, ch: 5 })
257 | removeUl(cm)()
258 | t.is(wrapper.state().code, 'foo\nbar')
259 | })
260 |
261 | test('Ul cancellation should remove "- " from current line if nothing is selected', (t) => {
262 | codeMirror.setValue('- foo\n- bar')
263 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
264 | removeUl(cm)()
265 | t.is(wrapper.state().code, 'foo\n- bar')
266 | })
267 |
268 | test('Ul cancellation should select affected lines after being executed', (t) => {
269 | codeMirror.setValue('- foo\n- bar')
270 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
271 | codeMirror.setSelection({ line: 0, ch: 0 }, { line: 1, ch: 5 })
272 | removeUl(cm)()
273 | t.is(codeMirror.getSelection(), 'foo\nbar')
274 | })
275 |
276 | /* Ol fomatting */
277 |
278 | test('setOl should return a function', t =>
279 | t.is(typeof setOl(cm), 'function')
280 | )
281 |
282 | test('removeOl should return a function', t =>
283 | t.is(typeof removeOl(cm), 'function')
284 | )
285 |
286 | test('Ol fomratting should add a numeric index in front of every selected line', (t) => {
287 | codeMirror.setValue('foo\nbar\nbaz')
288 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
289 | codeMirror.setSelection({ line: 1, ch: 0 }, { line: 2, ch: 3 })
290 | setOl(cm)()
291 | t.is(wrapper.state().code, 'foo\n1. bar\n2. baz')
292 | })
293 |
294 | test('Ol formattings should select all changed the lines after formatting', (t) => {
295 | codeMirror.setValue('foo\nbar')
296 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
297 | codeMirror.setSelection({ line: 0, ch: 0 }, { line: 1, ch: 3 })
298 | setOl(cm)()
299 | t.is(codeMirror.getSelection(), '1. foo\n2. bar')
300 | })
301 |
302 | test('Ol formatting should add "1. " to the current line when nothing is selected', (t) => {
303 | codeMirror.setValue('foo')
304 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
305 | setOl(cm)()
306 | t.is(wrapper.state().code, '1. foo')
307 | })
308 |
309 | test('Ol formatting should select current line after one line formatting', (t) => {
310 | codeMirror.setValue('foo')
311 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
312 | setOl(cm)()
313 | t.is(codeMirror.getSelection(), '1. foo')
314 | })
315 |
316 | test('Ol cancellation should remove numeric index from each selected line', (t) => {
317 | codeMirror.setValue('9. foo\n10. bar')
318 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
319 | codeMirror.setSelection({ line: 0, ch: 0 }, { line: 1, ch: 5 })
320 | removeOl(cm)()
321 | t.is(wrapper.state().code, 'foo\nbar')
322 | })
323 |
324 | test('Ol cancellation should remove numeric index from current line if nothing is selected', (t) => {
325 | codeMirror.setValue('10. foo\n bar')
326 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
327 | removeOl(cm)()
328 | t.is(wrapper.state().code, 'foo\n bar')
329 | })
330 |
331 | test('Ol cancellation should select affected lines after being executed', (t) => {
332 | codeMirror.setValue('9. foo\n10. bar')
333 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
334 | codeMirror.setSelection({ line: 0, ch: 0 }, { line: 1, ch: 7 })
335 | removeOl(cm)()
336 | t.is(codeMirror.getSelection(), 'foo\nbar')
337 | })
338 |
339 | /* setH1 fomatting */
340 |
341 | test('setH1 should be a function', t =>
342 | t.is(typeof setH1(cm), 'function')
343 | )
344 |
345 | test('removeH1 should be a function', t =>
346 | t.is(typeof removeH1(cm), 'function')
347 | )
348 |
349 | test('setH1 fomratting should add a # in front of every selected line', (t) => {
350 | codeMirror.setValue('foo\nbar\nbaz')
351 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
352 | codeMirror.setSelection({ line: 1, ch: 0 }, { line: 2, ch: 3 })
353 | setH1(cm)()
354 | t.is(wrapper.state().code, 'foo\n# bar\n# baz')
355 | })
356 |
357 | test('setH2 should be a function', t =>
358 | t.is(typeof setH2(cm), 'function')
359 | )
360 |
361 | test('removeH2 should be a function', t =>
362 | t.is(typeof removeH2(cm), 'function')
363 | )
364 |
365 | test('setH2 fomratting should add a ## in front of every selected line', (t) => {
366 | codeMirror.setValue('foo\nbar\nbaz')
367 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
368 | codeMirror.setSelection({ line: 1, ch: 0 }, { line: 2, ch: 3 })
369 | setH2(cm)()
370 | t.is(wrapper.state().code, 'foo\n## bar\n## baz')
371 | })
372 |
373 | test('setH3 should be a function', t =>
374 | t.is(typeof setH3(cm), 'function')
375 | )
376 |
377 | test('removeH3 should be a function', t =>
378 | t.is(typeof removeH3(cm), 'function')
379 | )
380 |
381 | test('setH3 fomratting should add a ### in front of every selected line', (t) => {
382 | codeMirror.setValue('foo\nbar\nbaz')
383 | CM.signal(codeMirror, 'change', codeMirror, { origin: '+input' })
384 | codeMirror.setSelection({ line: 1, ch: 0 }, { line: 2, ch: 3 })
385 | setH3(cm)()
386 | t.is(wrapper.state().code, 'foo\n### bar\n### baz')
387 | })
388 |
--------------------------------------------------------------------------------